Skip to content

Commit 8025296

Browse files
committed
itest: add itest for field modification HTLC interception response
Implement an integration test where an HTLC is intercepted and the interception response modifies fields in the resultant p2p message.
1 parent bd5de43 commit 8025296

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ var allTestCases = []*lntest.TestCase{
422422
Name: "forward interceptor",
423423
TestFunc: testForwardInterceptorBasic,
424424
},
425+
{
426+
Name: "forward interceptor modified htlc",
427+
TestFunc: testForwardInterceptorModifiedHtlc,
428+
},
425429
{
426430
Name: "zero conf channel open",
427431
TestFunc: testZeroConfChannelOpen,

itest/lnd_forward_interceptor_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package itest
22

33
import (
4+
"context"
45
"fmt"
56
"strings"
67
"time"
@@ -344,6 +345,140 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) {
344345
ht.CloseChannel(bob, cpBC)
345346
}
346347

348+
// testForwardInterceptorModifiedHtlc tests that the interceptor can modify the
349+
// amount and custom records of an intercepted HTLC and resume it.
350+
func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
351+
// Initialize the test context with 3 connected nodes.
352+
ts := newInterceptorTestScenario(ht)
353+
354+
alice, bob, carol := ts.alice, ts.bob, ts.carol
355+
356+
// Open and wait for channels.
357+
const chanAmt = btcutil.Amount(300000)
358+
p := lntest.OpenChannelParams{Amt: chanAmt}
359+
reqs := []*lntest.OpenChannelRequest{
360+
{Local: alice, Remote: bob, Param: p},
361+
{Local: bob, Remote: carol, Param: p},
362+
}
363+
resp := ht.OpenMultiChannelsAsync(reqs)
364+
cpAB, cpBC := resp[0], resp[1]
365+
366+
// Make sure Alice is aware of channel Bob=>Carol.
367+
ht.AssertTopologyChannelOpen(alice, cpBC)
368+
369+
// Connect an interceptor to Bob's node.
370+
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
371+
372+
// Prepare the test cases.
373+
invoiceValueAmtMsat := int64(1000)
374+
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
375+
addResponse := carol.RPC.AddInvoice(req)
376+
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
377+
tc := &interceptorTestCase{
378+
amountMsat: invoiceValueAmtMsat,
379+
invoice: invoice,
380+
payAddr: invoice.PaymentAddr,
381+
}
382+
383+
// We initiate a payment from Alice.
384+
done := make(chan struct{})
385+
go func() {
386+
// Signal that all the payments have been sent.
387+
defer close(done)
388+
389+
ts.sendPaymentAndAssertAction(tc)
390+
}()
391+
392+
// We start the htlc interceptor with a simple implementation that saves
393+
// all intercepted packets. These packets are held to simulate a
394+
// pending payment.
395+
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
396+
397+
// Resume the intercepted HTLC with a modified amount and custom
398+
// records.
399+
if packet.CustomRecords == nil {
400+
packet.CustomRecords = make(map[uint64][]byte)
401+
}
402+
customRecords := packet.CustomRecords
403+
404+
// Add custom records entry.
405+
crKey := uint64(65537)
406+
crValue := []byte("custom-records-test-value")
407+
customRecords[crKey] = crValue
408+
409+
action := routerrpc.ResolveHoldForwardAction_RESUME_MODIFIED
410+
newOutgoingAmountMsat := packet.OutgoingAmountMsat + 4000
411+
412+
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
413+
IncomingCircuitKey: packet.IncomingCircuitKey,
414+
OutgoingAmountMsat: newOutgoingAmountMsat,
415+
CustomRecords: customRecords,
416+
Action: action,
417+
})
418+
require.NoError(ht, err, "failed to send request")
419+
420+
// Check that the modified UpdateAddHTLC message fields were reported in
421+
// Carol's log.
422+
targetLogPrefixStr := "Received UpdateAddHTLC("
423+
targetOutgoingAmountMsatStr := fmt.Sprintf(
424+
"amt=%d", newOutgoingAmountMsat,
425+
)
426+
427+
// Formulate custom records target log string.
428+
var asciiValues []string
429+
for _, b := range crValue {
430+
asciiValues = append(asciiValues, fmt.Sprintf("%d", b))
431+
}
432+
433+
targetCustomRecordsStr := fmt.Sprintf(
434+
"%d:[%s]", crKey, strings.Join(asciiValues, " "),
435+
)
436+
437+
// logEntryCheck is a helper function that checks if the log entry
438+
// contains the expected strings.
439+
logEntryCheck := func(logEntry string) bool {
440+
return strings.Contains(logEntry, targetLogPrefixStr) &&
441+
strings.Contains(logEntry, targetCustomRecordsStr) &&
442+
strings.Contains(logEntry, targetOutgoingAmountMsatStr)
443+
}
444+
445+
// Wait for the log entry to appear in Carol's log.
446+
require.Eventually(ht, func() bool {
447+
ctx := context.Background()
448+
dbgInfo, err := carol.RPC.LN.GetDebugInfo(
449+
ctx, &lnrpc.GetDebugInfoRequest{},
450+
)
451+
require.NoError(ht, err, "failed to get Carol node debug info")
452+
453+
for _, logEntry := range dbgInfo.Log {
454+
if logEntryCheck(logEntry) {
455+
return true
456+
}
457+
}
458+
459+
return false
460+
}, defaultTimeout, time.Second)
461+
462+
// Cancel the context, which will disconnect Bob's interceptor.
463+
cancelBobInterceptor()
464+
465+
// Make sure all goroutines are finished.
466+
select {
467+
case <-done:
468+
case <-time.After(defaultTimeout):
469+
require.Fail(ht, "timeout waiting for sending payment")
470+
}
471+
472+
// Assert that the payment was successful.
473+
var preimage lntypes.Preimage
474+
copy(preimage[:], invoice.RPreimage)
475+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED)
476+
477+
// Finally, close channels.
478+
ht.CloseChannel(alice, cpAB)
479+
ht.CloseChannel(bob, cpBC)
480+
}
481+
347482
// interceptorTestScenario is a helper struct to hold the test context and
348483
// provide the needed functionality.
349484
type interceptorTestScenario struct {

0 commit comments

Comments
 (0)