Skip to content

Commit a3ff9f2

Browse files
committed
itest: add new strict forwarding test
1 parent 4761fe3 commit a3ff9f2

File tree

3 files changed

+303
-36
lines changed

3 files changed

+303
-36
lines changed

itest/assets_test.go

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -928,13 +928,14 @@ func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode,
928928
}
929929

930930
type payConfig struct {
931-
smallShards bool
932-
errSubStr string
933-
allowOverpay bool
934-
feeLimit lnwire.MilliSatoshi
935-
payStatus lnrpc.Payment_PaymentStatus
936-
failureReason lnrpc.PaymentFailureReason
937-
rfq fn.Option[rfqmsg.ID]
931+
smallShards bool
932+
errSubStr string
933+
allowOverpay bool
934+
feeLimit lnwire.MilliSatoshi
935+
destCustomRecords map[uint64][]byte
936+
payStatus lnrpc.Payment_PaymentStatus
937+
failureReason lnrpc.PaymentFailureReason
938+
rfq fn.Option[rfqmsg.ID]
938939
}
939940

940941
func defaultPayConfig() *payConfig {
@@ -982,6 +983,12 @@ func withFeeLimit(limit lnwire.MilliSatoshi) payOpt {
982983
}
983984
}
984985

986+
func withDestCustomRecords(records map[uint64][]byte) payOpt {
987+
return func(c *payConfig) {
988+
c.destCustomRecords = records
989+
}
990+
}
991+
985992
func withAllowOverpay() payOpt {
986993
return func(c *payConfig) {
987994
c.allowOverpay = true
@@ -1009,9 +1016,10 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
10091016
require.NoError(t, err)
10101017

10111018
sendReq := &routerrpc.SendPaymentRequest{
1012-
PaymentRequest: payReq,
1013-
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
1014-
FeeLimitMsat: int64(cfg.feeLimit),
1019+
PaymentRequest: payReq,
1020+
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
1021+
FeeLimitMsat: int64(cfg.feeLimit),
1022+
DestCustomRecords: cfg.destCustomRecords,
10151023
}
10161024

10171025
if cfg.smallShards {
@@ -2135,3 +2143,69 @@ func newCloseExpiryInfo(t *testing.T, node *HarnessNode) forceCloseExpiryInfo {
21352143
node: node,
21362144
}
21372145
}
2146+
2147+
// AssertHLTCNotActive asserts the node doesn't have a pending HTLC in the
2148+
// given channel, which mean either the HTLC never exists, or it was pending
2149+
// and now settled. Returns the HTLC if found and active.
2150+
func assertHTLCNotActive(t *testing.T, hn *HarnessNode,
2151+
cp *lnrpc.ChannelPoint, payHash []byte) *lnrpc.HTLC {
2152+
2153+
var result *lnrpc.HTLC
2154+
target := hex.EncodeToString(payHash)
2155+
2156+
err := wait.NoError(func() error {
2157+
// We require the RPC call to be succeeded and won't wait for
2158+
// it as it's an unexpected behavior.
2159+
ch := fetchChannel(t, hn, cp)
2160+
2161+
// Check all payment hashes active for this channel.
2162+
for _, htlc := range ch.PendingHtlcs {
2163+
h := hex.EncodeToString(htlc.HashLock)
2164+
2165+
// Break if found the htlc.
2166+
if h == target {
2167+
result = htlc
2168+
break
2169+
}
2170+
}
2171+
2172+
// If we've found nothing, we're done.
2173+
if result == nil {
2174+
return nil
2175+
}
2176+
2177+
// Otherwise return an error.
2178+
return fmt.Errorf("node [%s:%x] still has: the payHash %x",
2179+
hn.Name(), hn.PubKey[:], payHash)
2180+
}, defaultTimeout)
2181+
require.NoError(t, err, "timeout checking pending HTLC")
2182+
2183+
return result
2184+
}
2185+
2186+
func assertInvoiceState(t *testing.T, hn *HarnessNode, payAddr []byte,
2187+
expectedState lnrpc.Invoice_InvoiceState) {
2188+
2189+
msg := &invoicesrpc.LookupInvoiceMsg{
2190+
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
2191+
PaymentAddr: payAddr,
2192+
},
2193+
}
2194+
2195+
err := wait.NoError(func() error {
2196+
invoice, err := hn.InvoicesClient.LookupInvoiceV2(
2197+
context.Background(), msg,
2198+
)
2199+
if err != nil {
2200+
return err
2201+
}
2202+
2203+
if invoice.State == expectedState {
2204+
return nil
2205+
}
2206+
2207+
return fmt.Errorf("%s: invoice with payment address %x not "+
2208+
"in state %s", hn.Name(), payAddr, expectedState)
2209+
}, defaultTimeout)
2210+
require.NoError(t, err, "timeout waiting for invoice settled state")
2211+
}

itest/litd_custom_channels_test.go

Lines changed: 215 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,32 +2026,6 @@ func testCustomChannelsLiquidityEdgeCases(ctx context.Context,
20262026
),
20272027
)
20282028

2029-
// Edge case: Now Dave creates an asset invoice to be paid for by
2030-
// Yara with satoshi. For the last hop we try to settle the invoice in
2031-
// satoshi, where we will check whether Dave's strict forwarding works
2032-
// as expected. Charlie is only used as a dummy RFQ peer in this case,
2033-
// Yara totally ignored the RFQ hint and pays agnostically with sats.
2034-
invoiceResp = createAssetInvoice(t.t, charlie, dave, 22, assetID)
2035-
2036-
stream, err := dave.InvoicesClient.SubscribeSingleInvoice(
2037-
ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{
2038-
RHash: invoiceResp.RHash,
2039-
},
2040-
)
2041-
require.NoError(t.t, err)
2042-
2043-
// Yara pays Dave with enough satoshis, but Charlie will not settle as
2044-
// he expects assets.
2045-
hops := [][]byte{dave.PubKey[:]}
2046-
payInvoiceWithSatoshiLastHop(t.t, yara, invoiceResp, hops, withFailure(
2047-
lnrpc.Payment_FAILED, 0,
2048-
))
2049-
2050-
t.lndHarness.LNDHarness.AssertInvoiceState(stream, lnrpc.Invoice_OPEN)
2051-
2052-
logBalance(t.t, nodes, assetID, "after failed payment (asset "+
2053-
"invoice, strict forwarding)")
2054-
20552029
// Edge case: Check if the RFQ HTLC tracking accounts for cancelled
20562030
// HTLCs. We achieve this by manually creating & using an RFQ quote with
20572031
// a set max amount. We first pay to a hodl invoice that we eventually
@@ -2192,6 +2166,221 @@ func testCustomChannelsLiquidityEdgeCases(ctx context.Context,
21922166
logBalance(t.t, nodes, assetID, "after small manual rfq")
21932167
}
21942168

2169+
// testCustomChannelsStrictForwarding is a test that tests the strict forwarding
2170+
// behavior of a node when it comes to paying asset invoices with assets and
2171+
// BTC invoices with satoshis.
2172+
func testCustomChannelsStrictForwarding(ctx context.Context,
2173+
net *NetworkHarness, t *harnessTest) {
2174+
2175+
lndArgs := slices.Clone(lndArgsTemplate)
2176+
litdArgs := slices.Clone(litdArgsTemplate)
2177+
2178+
// Explicitly set the proof courier as Zane (now has no other role
2179+
// other than proof shuffling), otherwise a hashmail courier will be
2180+
// used. For the funding transaction, we're just posting it and don't
2181+
// expect a true receiver.
2182+
zane, err := net.NewNode(
2183+
t.t, "Zane", lndArgs, false, true, litdArgs...,
2184+
)
2185+
require.NoError(t.t, err)
2186+
2187+
litdArgs = append(litdArgs, fmt.Sprintf(
2188+
"--taproot-assets.proofcourieraddr=%s://%s",
2189+
proof.UniverseRpcCourierType, zane.Cfg.LitAddr(),
2190+
))
2191+
2192+
// The topology we are going for looks like the following:
2193+
//
2194+
// Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia
2195+
// |
2196+
// |
2197+
// [assets]
2198+
// |
2199+
// v
2200+
// Yara
2201+
//
2202+
// With [assets] being a custom channel and [sats] being a normal, BTC
2203+
// only channel.
2204+
// All 5 nodes need to be full litd nodes running in integrated mode
2205+
// with tapd included. We also need specific flags to be enabled, so we
2206+
// create 5 completely new nodes, ignoring the two default nodes that
2207+
// are created by the harness.
2208+
charlie, err := net.NewNode(
2209+
t.t, "Charlie", lndArgs, false, true, litdArgs...,
2210+
)
2211+
require.NoError(t.t, err)
2212+
2213+
dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...)
2214+
require.NoError(t.t, err)
2215+
erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...)
2216+
require.NoError(t.t, err)
2217+
fabia, err := net.NewNode(
2218+
t.t, "Fabia", lndArgs, false, true, litdArgs...,
2219+
)
2220+
require.NoError(t.t, err)
2221+
yara, err := net.NewNode(
2222+
t.t, "Yara", lndArgs, false, true, litdArgs...,
2223+
)
2224+
require.NoError(t.t, err)
2225+
2226+
nodes := []*HarnessNode{charlie, dave, erin, fabia, yara}
2227+
connectAllNodes(t.t, net, nodes)
2228+
fundAllNodes(t.t, net, nodes)
2229+
2230+
// Create the normal channel between Dave and Erin.
2231+
t.Logf("Opening normal channel between Dave and Erin...")
2232+
channelOp := openChannelAndAssert(
2233+
t, net, dave, erin, lntest.OpenChannelParams{
2234+
Amt: 10_000_000,
2235+
SatPerVByte: 5,
2236+
},
2237+
)
2238+
defer closeChannelAndAssert(t, net, dave, channelOp, true)
2239+
2240+
// This is the only public channel, we need everyone to be aware of it.
2241+
assertChannelKnown(t.t, charlie, channelOp)
2242+
assertChannelKnown(t.t, fabia, channelOp)
2243+
2244+
universeTap := newTapClient(t.t, zane)
2245+
charlieTap := newTapClient(t.t, charlie)
2246+
daveTap := newTapClient(t.t, dave)
2247+
erinTap := newTapClient(t.t, erin)
2248+
fabiaTap := newTapClient(t.t, fabia)
2249+
yaraTap := newTapClient(t.t, yara)
2250+
2251+
// Mint an asset on Charlie and sync all nodes to Charlie as the
2252+
// universe.
2253+
mintedAssets := itest.MintAssetsConfirmBatch(
2254+
t.t, t.lndHarness.Miner.Client, charlieTap,
2255+
[]*mintrpc.MintAssetRequest{
2256+
{
2257+
Asset: itestAsset,
2258+
},
2259+
},
2260+
)
2261+
cents := mintedAssets[0]
2262+
assetID := cents.AssetGenesis.AssetId
2263+
2264+
t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount)
2265+
syncUniverses(t.t, charlieTap, dave, erin, fabia, yara)
2266+
t.Logf("Universes synced between all nodes, distributing assets...")
2267+
2268+
const (
2269+
daveFundingAmount = uint64(400_000)
2270+
erinFundingAmount = uint64(200_000)
2271+
)
2272+
charlieFundingAmount := cents.Amount - uint64(2*400_000)
2273+
2274+
_, _, _ = createTestAssetNetwork(
2275+
t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap,
2276+
universeTap, cents, 400_000, charlieFundingAmount,
2277+
daveFundingAmount, erinFundingAmount, 0,
2278+
)
2279+
2280+
// Before we start sending out payments, let's make sure each node can
2281+
// see the other one in the graph and has all required features.
2282+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave))
2283+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie))
2284+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara))
2285+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave))
2286+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia))
2287+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin))
2288+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin))
2289+
2290+
logBalance(t.t, nodes, assetID, "initial")
2291+
2292+
// Do a payment from Charlie to Erin to shift the balances in all
2293+
// channels enough to allow for the following payments in any direction.
2294+
// Pay a normal bolt11 invoice involving RFQ flow.
2295+
_ = createAndPayNormalInvoice(
2296+
t.t, charlie, dave, erin, 500_000, assetID, withSmallShards(),
2297+
)
2298+
2299+
logBalance(t.t, nodes, assetID, "after payment")
2300+
2301+
// Edge case: Now Dave creates an asset invoice to be paid for by Erin
2302+
// with satoshi. For the last hop we try to settle the invoice in
2303+
// satoshi, where we will check whether Daves's strict forwarding
2304+
// works as expected. Charlie is only used as a dummy RFQ peer in this
2305+
// case, Erin totally ignores the RFQ hint and just pays with sats.
2306+
assetInvoice := createAssetInvoice(t.t, charlie, dave, 40, assetID)
2307+
2308+
assetInvoiceStream, err := dave.InvoicesClient.SubscribeSingleInvoice(
2309+
ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{
2310+
RHash: assetInvoice.RHash,
2311+
},
2312+
)
2313+
require.NoError(t.t, err)
2314+
2315+
// Erin pays Dave with enough satoshis, but Charlie will not settle as
2316+
// he expects assets.
2317+
hops := [][]byte{dave.PubKey[:]}
2318+
payInvoiceWithSatoshiLastHop(
2319+
t.t, erin, assetInvoice, hops, withFailure(
2320+
lnrpc.Payment_FAILED, 0,
2321+
),
2322+
)
2323+
2324+
// Make sure the invoice hasn't been settled and there's no HTLC on the
2325+
// channel between Erin and Dave.
2326+
t.lndHarness.LNDHarness.AssertInvoiceState(
2327+
assetInvoiceStream, lnrpc.Invoice_OPEN,
2328+
)
2329+
assertHTLCNotActive(t.t, erin, channelOp, assetInvoice.RHash)
2330+
assertInvoiceState(
2331+
t.t, dave, assetInvoice.PaymentAddr, lnrpc.Invoice_OPEN,
2332+
)
2333+
2334+
logBalance(t.t, nodes, assetID, "after failed payment (asset "+
2335+
"invoice, strict forwarding)")
2336+
2337+
// Now let's make sure that we can actually still pay the invoice with
2338+
// assets from Charlie.
2339+
payInvoiceWithAssets(
2340+
t.t, charlie, dave, assetInvoice.PaymentRequest, assetID,
2341+
)
2342+
t.lndHarness.LNDHarness.AssertInvoiceState(
2343+
assetInvoiceStream, lnrpc.Invoice_SETTLED,
2344+
)
2345+
assertInvoiceState(
2346+
t.t, dave, assetInvoice.PaymentAddr, lnrpc.Invoice_SETTLED,
2347+
)
2348+
2349+
// Edge case: We now try the opposite: Dave creates a BTC invoice but
2350+
// Charlie tries to pay it with assets. This should fail as well.
2351+
btcInvoice := createNormalInvoice(t.t, dave, 1_000)
2352+
btcInvoiceStream, err := dave.InvoicesClient.SubscribeSingleInvoice(
2353+
ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{
2354+
RHash: btcInvoice.RHash,
2355+
},
2356+
)
2357+
require.NoError(t.t, err)
2358+
2359+
payInvoiceWithAssets(
2360+
t.t, charlie, dave, btcInvoice.PaymentRequest, assetID,
2361+
withFailure(lnrpc.Payment_FAILED, failureIncorrectDetails),
2362+
)
2363+
t.lndHarness.LNDHarness.AssertInvoiceState(
2364+
btcInvoiceStream, lnrpc.Invoice_OPEN,
2365+
)
2366+
assertHTLCNotActive(t.t, erin, channelOp, btcInvoice.RHash)
2367+
assertInvoiceState(
2368+
t.t, dave, btcInvoice.PaymentAddr, lnrpc.Invoice_OPEN,
2369+
)
2370+
2371+
// And finally we make sure that we can still pay the invoice with
2372+
// satoshis from Erin, using custom records.
2373+
payInvoiceWithSatoshi(t.t, erin, btcInvoice, withDestCustomRecords(
2374+
map[uint64][]byte{106823: {0x01}},
2375+
))
2376+
t.lndHarness.LNDHarness.AssertInvoiceState(
2377+
btcInvoiceStream, lnrpc.Invoice_SETTLED,
2378+
)
2379+
assertInvoiceState(
2380+
t.t, dave, btcInvoice.PaymentAddr, lnrpc.Invoice_SETTLED,
2381+
)
2382+
}
2383+
21952384
// testCustomChannelsBalanceConsistency is a test that test the balance of nodes
21962385
// under channel opening circumstances.
21972386
func testCustomChannelsBalanceConsistency(ctx context.Context,

itest/litd_test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ var allTestCases = []*testCase{
7272
name: "test custom channels forward bandwidth",
7373
test: testCustomChannelsForwardBandwidth,
7474
},
75+
{
76+
name: "test custom channels strict forwarding",
77+
test: testCustomChannelsStrictForwarding,
78+
},
7579
{
7680
name: "test custom channels decode payreq",
7781
test: testCustomChannelsDecodeAssetInvoice,

0 commit comments

Comments
 (0)