Skip to content

Commit

Permalink
itest: test that RFQ can agree on asset sell quote and verify HTLC
Browse files Browse the repository at this point in the history
  • Loading branch information
ffranr committed Mar 19, 2024
1 parent 692b496 commit d2bc336
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
192 changes: 192 additions & 0 deletions itest/rfq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,198 @@ 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.
waitErr := wait.NoError(func() error {
event, err := aliceEventNtfns.Recv()
require.NoError(t.t, err)

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

return nil
}, defaultWaitTimeout)
require.NoError(t.t, waitErr)

// 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.QueryRfqPeerAcceptedQuotes(
ctxt, &rfqrpc.QueryRfqPeerAcceptedQuotesRequest{},
)
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.
addInvoiceResp := ts.CarolLnd.RPC.AddInvoice(&lnrpc.Invoice{
ValueMsat: int64(askAmt),
})
invoice := ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash)

// Use the agreed upon scid found in the accepted quote to construct a
// route hop hint for the Alice->Bob step of the payment. The route hop
// hint specifies a SCID for use between Alice and Bob. This SCID will
// be used by Bob to identify the HTLC as relating to the accepted
// quote.
scid := lnwire.NewShortChanIDFromInt(acceptedQuote.Scid)
aliceBobHopHint := &lnrpc.HopHint{
NodeId: ts.AliceLnd.PubKeyStr,
ChanId: scid.ToUint64(),
FeeBaseMsat: uint32(chainreg.DefaultBitcoinBaseFeeMSat),
FeeProportionalMillionths: uint32(
chainreg.DefaultBitcoinFeeRate,
),
CltvExpiryDelta: chainreg.DefaultBitcoinTimeLockDelta,
}
routeHints := []*lnrpc.RouteHint{
{
HopHints: []*lnrpc.HopHint{
aliceBobHopHint,
},
},
}

// Alice pays the invoice.
t.Log("Alice paying invoice generated by Carol")
req := &routerrpc.SendPaymentRequest{
PaymentRequest: invoice.PaymentRequest,
RouteHints: routeHints,
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
FeeLimitMsat: math.MaxInt64,
}
ts.AliceLnd.RPC.SendPayment(req)
t.Log("Alice payment sent")

//// Construct a route.
//routeBuildRequest := routerrpc.BuildRouteRequest{
// AmtMsat: int64(askAmt),
// HopPubkeys: [][]byte{
// ts.BobLnd.PubKey[:],
// ts.CarolLnd.PubKey[:],
// },
//}
//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[rfqmsg.LnCustomRecordType] =
// acceptedQuote.Id[:]
//
//// Set the scid for the Alice->Bob channel in the route to the scid
//// specified in the accepted quote.
//scid := lnwire.NewShortChanIDFromInt(acceptedQuote.Scid)
//aliceBobHop.ChanId = scid.ToUint64()
//
//// Update the route with the modified first hop.
//routeBuildResp.Route.Hops[0] = aliceBobHop
//
//// Send the payment to the route.
//t.Log("Alice paying invoice")
//ts.AliceLnd.RPC.SendToRouteV2(&routerrpc.SendToRouteRequest{
// Route: routeBuildResp.Route,
//})

// 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.
waitErr = wait.NoError(func() error {
t.Log("Waiting for Bob to receive HTLC")

event, err := bobEventNtfns.Recv()
require.NoError(t.t, err)

acceptHtlc, ok := event.Event.(*rfqrpc.RfqEvent_AcceptHtlc)
if ok {
require.Equal(
t.t, acceptedQuote.Scid,
acceptHtlc.AcceptHtlc.Scid,
)
t.Log("Bob has accepted the HTLC")
return nil
}

return fmt.Errorf("unexpected event: %v", event)
}, defaultWaitTimeout)
require.NoError(t.t, waitErr)

// TODO(ffranr): Confirm that Carol receives the lightning payment from
// Bob.

// 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
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ var testCases = []*testCase{
name: "rfq asset buy htlc intercept",
test: testRfqAssetBuyHtlcIntercept,
},
{
name: "rfq asset sell htlc intercept",
test: testRfqAssetSellHtlcIntercept,
},
}

var optionalTestCases = []*testCase{
Expand Down
6 changes: 6 additions & 0 deletions rfqmsg/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const (
MsgTypeReject = TapMessageTypeBaseOffset + 4
)

const (
// LnCustomRecordType is a taproot-assets specific lightning payment hop
// custom record K-V value.
LnCustomRecordType = 65536 + uint64(TapMessageTypeBaseOffset)
)

var (
// ErrUnknownMessageType is an error that is returned when an unknown
// message type is encountered.
Expand Down

0 comments on commit d2bc336

Please sign in to comment.