diff --git a/config.go b/config.go index fdb4344d..4125c21d 100644 --- a/config.go +++ b/config.go @@ -311,6 +311,8 @@ type config struct { RejectPush bool `long:"rejectpush" description:"If true, lnd will not accept channel opening requests with non-zero push amounts. This should prevent accidental pushes to merchant nodes."` + RejectHTLC bool `long:"rejecthtlc" description:"If true, lnd will not forward any HTLCs that are meant as onward payments. This option will still allow lnd to send HTLCs and receive HTLCs but lnd won't be used as a hop."` + StaggerInitialReconnect bool `long:"stagger-initial-reconnect" description:"If true, will apply a randomized staggering between 0s and 30s when reconnecting to persistent peers on startup. The first 10 reconnections will be attempted instantly, regardless of the flag's value"` MaxOutgoingCltvExpiry uint32 `long:"max-cltv-expiry" description:"The maximum number of blocks funds could be locked up for when forwarding payments."` diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index 82cdc979..bdc50fe5 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -171,6 +171,10 @@ type Config struct { // the ChannelNotifier when channels become active and inactive. NotifyActiveChannel func(wire.OutPoint) NotifyInactiveChannel func(wire.OutPoint) + + // RejectHTLC is a flag that instructs the htlcswitch to reject any + // HTLCs that are not from the source hop. + RejectHTLC bool } // Switch is the central messaging bus for all incoming/outgoing HTLCs. @@ -1025,6 +1029,15 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error { // payment circuit within our internal state so we can properly forward // the ultimate settle message back latter. case *lnwire.UpdateAddHTLC: + // Check if the node is set to reject all onward HTLCs and also make + // sure that HTLC is not from the source node. + if s.cfg.RejectHTLC && packet.incomingChanID != sourceHop { + failure := &lnwire.FailChannelDisabled{} + addErr := fmt.Errorf("unable to forward any htlcs") + + return s.failAddPacket(packet, failure, addErr) + } + if packet.incomingChanID == sourceHop { // A blank incomingChanID indicates that this is // a pending user-initiated payment. diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index d6a16a9a..0081d0cc 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -8844,6 +8844,169 @@ out: cleanupForceClose(t, net, net.Bob, chanPointBob) } +// testRejectHTLC tests that a node can be created with the flag --rejecthtlc. +// This means that the node will reject all forwarded HTLCs but can still +// accept direct HTLCs as well as send HTLCs. +func testRejectHTLC(net *lntest.NetworkHarness, t *harnessTest) { + // RejectHTLC + // Alice ------> Carol ------> Bob + // + const chanAmt = btcutil.Amount(1000000) + ctxb := context.Background() + timeout := time.Duration(time.Second * 5) + + // Create Carol with reject htlc flag. + carol, err := net.NewNode("Carol", []string{"--rejecthtlc"}) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + // Connect Alice to Carol. + if err := net.ConnectNodes(ctxb, net.Alice, carol); err != nil { + t.Fatalf("unable to connect alice to carol: %v", err) + } + + // Connect Carol to Bob. + if err := net.ConnectNodes(ctxb, carol, net.Bob); err != nil { + t.Fatalf("unable to conenct carol to net.Bob: %v", err) + } + + // Send coins to Carol. + err = net.SendCoins(ctxb, btcutil.SatoshiPerBitcoin, carol) + if err != nil { + t.Fatalf("unable to send coins to carol: %v", err) + } + + // Send coins to Alice. + err = net.SendCoins(ctxb, btcutil.SatoshiPerBitcent, net.Alice) + if err != nil { + t.Fatalf("unable to send coins to alice: %v", err) + } + + // Open a channel between Alice and Carol. + ctxt, _ := context.WithTimeout(ctxb, timeout) + chanPointAlice := openChannelAndAssert( + ctxt, t, net, net.Alice, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + // Open a channel between Carol and Bob. + ctxt, _ = context.WithTimeout(ctxb, timeout) + chanPointCarol := openChannelAndAssert( + ctxt, t, net, carol, net.Bob, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + // Channel should be ready for payments. + const payAmt = 100 + + // Helper closure to generate a random pre image. + genPreImage := func() []byte { + preimage := make([]byte, 32) + + _, err = rand.Read(preimage) + if err != nil { + t.Fatalf("unable to generate preimage: %v", err) + } + + return preimage + } + + // Create an invoice from Carol of 100 satoshis. + // We expect Alice to be able to pay this invoice. + preimage := genPreImage() + + carolInvoice := &lnrpc.Invoice{ + Memo: "testing - alice should pay carol", + RPreimage: preimage, + Value: payAmt, + } + + // Carol adds the invoice to her database. + resp, err := carol.AddInvoice(ctxb, carolInvoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + // Alice pays Carols invoice. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = completePaymentRequests( + ctxt, net.Alice, []string{resp.PaymentRequest}, true, + ) + if err != nil { + t.Fatalf("unable to send payments from alice to carol: %v", err) + } + + // Create an invoice from Bob of 100 satoshis. + // We expect Carol to be able to pay this invoice. + preimage = genPreImage() + + bobInvoice := &lnrpc.Invoice{ + Memo: "testing - carol should pay bob", + RPreimage: preimage, + Value: payAmt, + } + + // Bob adds the invoice to his database. + resp, err = net.Bob.AddInvoice(ctxb, bobInvoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + // Carol pays Bobs invoice. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = completePaymentRequests( + ctxt, carol, []string{resp.PaymentRequest}, true, + ) + if err != nil { + t.Fatalf("unable to send payments from carol to bob: %v", err) + } + + // Create an invoice from Bob of 100 satoshis. + // Alice attempts to pay Bob but this should fail, since we are + // using Carol as a hop and her node will reject onward HTLCs. + preimage = genPreImage() + + bobInvoice = &lnrpc.Invoice{ + Memo: "testing - alice tries to pay bob", + RPreimage: preimage, + Value: payAmt, + } + + // Bob adds the invoice to his database. + resp, err = net.Bob.AddInvoice(ctxb, bobInvoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + // Alice attempts to pay Bobs invoice. This payment should be rejected since + // we are using Carol as an intermediary hop, Carol is running lnd with + // --rejecthtlc. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = completePaymentRequests( + ctxt, net.Alice, []string{resp.PaymentRequest}, true, + ) + if err == nil { + t.Fatalf( + "should have been rejected, carol will not accept forwarded htlcs", + ) + } + if !strings.Contains(err.Error(), lnwire.CodeChannelDisabled.String()) { + t.Fatalf("error returned should have been Channel Disabled") + } + + // Close all channels. + ctxt, _ = context.WithTimeout(ctxb, timeout) + closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false) + ctxt, _ = context.WithTimeout(ctxb, timeout) + closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false) +} + // graphSubscription houses the proxied update and error chans for a node's // graph subscriptions. type graphSubscription struct { @@ -14049,6 +14212,10 @@ var testsCases = []*testCase{ name: "multi-hop htlc error propagation", test: testHtlcErrorPropagation, }, + { + name: "reject onward htlc", + test: testRejectHTLC, + }, // TODO(roasbeef): multi-path integration test { name: "node announcement", diff --git a/server.go b/server.go index 7a37e769..472e866f 100644 --- a/server.go +++ b/server.go @@ -441,6 +441,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, AckEventTicker: ticker.New(htlcswitch.DefaultAckInterval), NotifyActiveChannel: s.channelNotifier.NotifyActiveChannelEvent, NotifyInactiveChannel: s.channelNotifier.NotifyInactiveChannelEvent, + RejectHTLC: cfg.RejectHTLC, }, uint32(currentHeight)) if err != nil { return nil, err