Skip to content

Commit

Permalink
lnrpc+htlcswitch: add invoice settling and canceling
Browse files Browse the repository at this point in the history
  • Loading branch information
joostjager committed Oct 19, 2018
1 parent 878cb1b commit df51265
Show file tree
Hide file tree
Showing 12 changed files with 946 additions and 450 deletions.
107 changes: 107 additions & 0 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2358,6 +2358,113 @@ func addInvoice(ctx *cli.Context) error {
return nil
}

var settleInvoiceCommand = cli.Command{
Name: "settleinvoice",
Category: "Payments",
Usage: "Reveal a preimage and use it to settle the corresponding invoice.",
Description: `
Todo.`,
ArgsUsage: "preimage",
Flags: []cli.Flag{
cli.StringFlag{
Name: "preimage",
Usage: "the hex-encoded preimage (32 byte) which will " +
"allow settling an incoming HTLC payable to this " +
"preimage.",
},
},
Action: actionDecorator(settleInvoice),
}

func settleInvoice(ctx *cli.Context) error {
var (
preimage []byte
err error
)

client, cleanUp := getClient(ctx)
defer cleanUp()

args := ctx.Args()

switch {
case ctx.IsSet("preimage"):
preimage, err = hex.DecodeString(ctx.String("preimage"))
case args.Present():
preimage, err = hex.DecodeString(args.First())
}

if err != nil {
return fmt.Errorf("unable to parse preimage: %v", err)
}

invoice := &lnrpc.SettleInvoiceMsg{
PreImage: preimage,
}

resp, err := client.SettleInvoice(context.Background(), invoice)
if err != nil {
return err
}

printJSON(resp)

return nil
}

var cancelInvoiceCommand = cli.Command{
Name: "cancelinvoice",
Category: "Payments",
Usage: "Cancels a (hold) invoice",
Description: `
Todo.`,
ArgsUsage: "paymenthash",
Flags: []cli.Flag{
cli.StringFlag{
Name: "paymenthash",
Usage: "the hex-encoded payment hash (32 byte) for which the " +
"corresponding invoice will be canceled.",
},
},
Action: actionDecorator(cancelInvoice),
}

func cancelInvoice(ctx *cli.Context) error {
var (
paymentHash []byte
err error
)

client, cleanUp := getClient(ctx)
defer cleanUp()

args := ctx.Args()

switch {
case ctx.IsSet("paymenthash"):
paymentHash, err = hex.DecodeString(ctx.String("paymenthash"))
case args.Present():
paymentHash, err = hex.DecodeString(args.First())
}

if err != nil {
return fmt.Errorf("unable to parse preimage: %v", err)
}

invoice := &lnrpc.CancelInvoiceMsg{
PaymentHash: paymentHash,
}

resp, err := client.CancelInvoice(context.Background(), invoice)
if err != nil {
return err
}

printJSON(resp)

return nil
}

var lookupInvoiceCommand = cli.Command{
Name: "lookupinvoice",
Category: "Payments",
Expand Down
2 changes: 2 additions & 0 deletions cmd/lncli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ func main() {
payInvoiceCommand,
sendToRouteCommand,
addInvoiceCommand,
settleInvoiceCommand,
cancelInvoiceCommand,
lookupInvoiceCommand,
listInvoicesCommand,
subscribeInvoicesCommand,
Expand Down
4 changes: 4 additions & 0 deletions htlcswitch/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type InvoiceDatabase interface {
// SettleInvoice attempts to mark an invoice corresponding to the
// passed payment hash as fully settled.
SettleInvoice(payHash chainhash.Hash, paidAmount lnwire.MilliSatoshi) error

// AcceptInvoice attempts to mark an invoice corresponding to the
// passed payment hash as accepted.
AcceptInvoice(payHash chainhash.Hash, paidAmount lnwire.MilliSatoshi) error
}

// ChannelLink is an interface which represents the subsystem for managing the
Expand Down
133 changes: 133 additions & 0 deletions htlcswitch/invoice_settler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package htlcswitch

import (
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/lnwire"
)

// InvoiceSettler handles settling and failing of invoices.
type InvoiceSettler struct {
// Registry is a sub-system which responsible for managing the invoices
// in thread-safe manner.
registry InvoiceDatabase

HtlcSwitch *Switch
}

// NewInvoiceSettler returns a new invoice settler instance.
func NewInvoiceSettler(registry InvoiceDatabase) *InvoiceSettler {
return &InvoiceSettler{
registry: registry,
}
}

// Settle settles the htlc and marks the invoice as settled.
func (i *InvoiceSettler) Settle(preimage [32]byte) error {
paymentHash := chainhash.Hash(sha256.Sum256(preimage[:]))

invoice, _, err := i.registry.LookupInvoice(
paymentHash,
)
if err != nil {
return fmt.Errorf("unable to query invoice registry: "+
" %v", err)
}

if invoice.Terms.State == channeldb.ContractSettled {
return fmt.Errorf("invoice already settled")
}

// TODO: Implement hodl.BogusSettle?

i.HtlcSwitch.ProcessContractResolution(
contractcourt.ResolutionMsg{
SourceChan: exitHop,
HtlcIndex: invoice.AddIndex,
PreImage: &preimage,
},
)

// TODO: Set amount in accepted stage already
err = i.registry.SettleInvoice(
paymentHash, lnwire.MilliSatoshi(0),
)
if err != nil {
log.Errorf("unable to settle invoice: %v", err)
}

log.Infof("Settled invoice %x", paymentHash)

return nil
}

// Cancel fails the incoming HTLC with unknown payment hash.
func (i *InvoiceSettler) Cancel(paymentHash [32]byte) error {
invoice, _, err := i.registry.LookupInvoice(
paymentHash,
)
if err != nil {
return fmt.Errorf("unable to query invoice registry: "+
" %v", err)
}

if invoice.Terms.State == channeldb.ContractSettled {
return fmt.Errorf("invoice already settled")
}

i.HtlcSwitch.ProcessContractResolution(
contractcourt.ResolutionMsg{
SourceChan: exitHop,
HtlcIndex: invoice.AddIndex,
Failure: lnwire.FailUnknownPaymentHash{},
},
)

// TODO: Move invoice to canceled state / remove

log.Infof("Canceled invoice %x", paymentHash)

return nil
}

// handleIncoming is called from switch when a htlc comes in for which we are
// the exit hop.
func (i *InvoiceSettler) handleIncoming(pkt *htlcPacket) error {
// We're the designated payment destination. Therefore
// we attempt to see if we have an invoice locally
// which'll allow us to settle this htlc.
invoiceHash := pkt.circuit.PaymentHash
invoice, _, err := i.registry.LookupInvoice(
invoiceHash,
)

// TODO: What happens when invoice is already settled?
//
// TODO: Concurrency, merge lookup and accept in single atomic
// operation?

// Notify the invoiceRegistry of the invoices we just
// accepted this latest commitment update.
err = i.registry.AcceptInvoice(
invoiceHash, pkt.incomingAmount,
)
if err != nil {
return fmt.Errorf("unable to accept invoice: %v", err)
}

// TODO: Deal with OutKey collision in case of pay more than once.
i.HtlcSwitch.circuits.OpenCircuits(Keystone{
InKey: pkt.circuit.Incoming,
OutKey: CircuitKey{
ChanID: exitHop,
HtlcID: invoice.AddIndex,
},
})

log.Infof("Accepted invoice %x", invoiceHash)

return nil
}
84 changes: 46 additions & 38 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -2440,49 +2440,57 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
}

preimage := invoice.Terms.PaymentPreimage
err = l.channel.SettleHTLC(
preimage, pd.HtlcIndex, pd.SourceRef, nil, nil,
)
if err != nil {
l.fail(LinkFailureError{code: ErrInternalError},
"unable to settle htlc: %v", err)
return false
}
// If we have the preimage, we can now settle the
// invoice instantly.
if !bytes.Equal(preimage[:], unknownPreimage[:]) {

// Notify the invoiceRegistry of the invoices we just
// settled (with the amount accepted at settle time)
// with this latest commitment update.
err = l.cfg.Registry.SettleInvoice(
invoiceHash, pd.Amount,
)
if err != nil {
l.fail(LinkFailureError{code: ErrInternalError},
"unable to settle invoice: %v", err)
return false
}
err = l.channel.SettleHTLC(
preimage, pd.HtlcIndex, pd.SourceRef, nil, nil,
)
if err != nil {
l.fail(LinkFailureError{code: ErrInternalError},
"unable to settle htlc: %v", err)
return false
}

// Notify the invoiceRegistry of the invoices we just
// settled (with the amount accepted at settle time)
// with this latest commitment update.
err = l.cfg.Registry.SettleInvoice(
invoiceHash, pd.Amount,
)
if err != nil {
l.fail(LinkFailureError{code: ErrInternalError},
"unable to settle invoice: %v", err)
return false
}

l.infof("settling %x as exit hop", pd.RHash)
l.infof("settling %x as exit hop", pd.RHash)

// If the link is in hodl.BogusSettle mode, replace the
// preimage with a fake one before sending it to the
// peer.
if l.cfg.DebugHTLC &&
l.cfg.HodlMask.Active(hodl.BogusSettle) {
l.warnf(hodl.BogusSettle.Warning())
preimage = [32]byte{}
copy(preimage[:], bytes.Repeat([]byte{2}, 32))
}
// If the link is in hodl.BogusSettle mode, replace the
// preimage with a fake one before sending it to the
// peer.
if l.cfg.DebugHTLC &&
l.cfg.HodlMask.Active(hodl.BogusSettle) {
l.warnf(hodl.BogusSettle.Warning())
preimage = [32]byte{}
copy(preimage[:], bytes.Repeat([]byte{2}, 32))
}

// HTLC was successfully settled locally send
// notification about it remote peer.
l.cfg.Peer.SendMessage(false, &lnwire.UpdateFulfillHTLC{
ChanID: l.ChanID(),
ID: pd.HtlcIndex,
PaymentPreimage: preimage,
})
needUpdate = true
// HTLC was successfully settled locally send
// notification about it remote peer.
l.cfg.Peer.SendMessage(false, &lnwire.UpdateFulfillHTLC{
ChanID: l.ChanID(),
ID: pd.HtlcIndex,
PaymentPreimage: preimage,
})
needUpdate = true

continue
continue
}

// Still waiting for preimage, proceed with forwarding
// packet with destination exithop to switch.
}

// There are additional channels left within this route. So
Expand Down
22 changes: 22 additions & 0 deletions htlcswitch/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,28 @@ func (i *mockInvoiceRegistry) SettleInvoice(rhash chainhash.Hash,
return nil
}

func (i *mockInvoiceRegistry) AcceptInvoice(rhash chainhash.Hash,
amt lnwire.MilliSatoshi) error {

i.Lock()
defer i.Unlock()

invoice, ok := i.invoices[rhash]
if !ok {
return fmt.Errorf("can't find mock invoice: %x", rhash[:])
}

if invoice.Terms.State == channeldb.ContractAccepted {
return nil
}

invoice.Terms.State = channeldb.ContractAccepted
invoice.AmtPaid = amt
i.invoices[rhash] = invoice

return nil
}

func (i *mockInvoiceRegistry) AddInvoice(invoice channeldb.Invoice) error {
i.Lock()
defer i.Unlock()
Expand Down
Loading

0 comments on commit df51265

Please sign in to comment.