Merge pull request #2457 from joostjager/cancelinvoice

invoices: CancelInvoice
This commit is contained in:
Olaoluwa Osuntokun 2019-02-06 14:45:16 -08:00 committed by GitHub
commit 27cfdf944e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1470 additions and 811 deletions

View File

@ -62,6 +62,10 @@ var (
// ErrInvoiceAlreadySettled is returned when the invoice is already
// settled.
ErrInvoiceAlreadySettled = errors.New("invoice already settled")
// ErrInvoiceAlreadyCanceled is returned when the invoice is already
// canceled.
ErrInvoiceAlreadyCanceled = errors.New("invoice already canceled")
)
const (
@ -90,6 +94,9 @@ const (
// ContractSettled means the htlc is settled and the invoice has been
// paid.
ContractSettled ContractState = 1
// ContractCanceled means the invoice has been canceled.
ContractCanceled ContractState = 2
)
// String returns a human readable identifier for the ContractState type.
@ -99,6 +106,8 @@ func (c ContractState) String() string {
return "Open"
case ContractSettled:
return "Settled"
case ContractCanceled:
return "Canceled"
}
return "Unknown"
@ -641,6 +650,37 @@ func (d *DB) SettleInvoice(paymentHash [32]byte,
return settledInvoice, err
}
// CancelInvoice attempts to cancel the invoice corresponding to the passed
// payment hash.
func (d *DB) CancelInvoice(paymentHash lntypes.Hash) (*Invoice, error) {
var canceledInvoice *Invoice
err := d.Update(func(tx *bbolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
return err
}
invoiceIndex, err := invoices.CreateBucketIfNotExists(
invoiceIndexBucket,
)
if err != nil {
return err
}
// Check the invoice index to see if an invoice paying to this
// hash exists within the DB.
invoiceNum := invoiceIndex.Get(paymentHash[:])
if invoiceNum == nil {
return ErrInvoiceNotFound
}
canceledInvoice, err = cancelInvoice(invoices, invoiceNum)
return err
})
return canceledInvoice, err
}
// InvoicesSettledSince can be used by callers to catch up any settled invoices
// they missed within the settled invoice time series. We'll return all known
// settled invoice that have a settle index higher than the passed
@ -896,8 +936,11 @@ func settleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
return nil, err
}
if invoice.Terms.State == ContractSettled {
switch invoice.Terms.State {
case ContractSettled:
return &invoice, ErrInvoiceAlreadySettled
case ContractCanceled:
return &invoice, ErrInvoiceAlreadyCanceled
}
// Now that we know the invoice hasn't already been settled, we'll
@ -930,3 +973,32 @@ func settleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
return &invoice, nil
}
func cancelInvoice(invoices *bbolt.Bucket, invoiceNum []byte) (
*Invoice, error) {
invoice, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return nil, err
}
switch invoice.Terms.State {
case ContractSettled:
return &invoice, ErrInvoiceAlreadySettled
case ContractCanceled:
return &invoice, ErrInvoiceAlreadyCanceled
}
invoice.Terms.State = ContractCanceled
var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err
}
if err := invoices.Put(invoiceNum[:], buf.Bytes()); err != nil {
return nil, err
}
return &invoice, nil
}

View File

@ -0,0 +1,82 @@
// +build invoicesrpc
package main
import (
"context"
"encoding/hex"
"fmt"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/urfave/cli"
)
// invoicesCommands will return nil for non-invoicesrpc builds.
func invoicesCommands() []cli.Command {
return []cli.Command{
cancelInvoiceCommand,
}
}
func getInvoicesClient(ctx *cli.Context) (invoicesrpc.InvoicesClient, func()) {
conn := getClientConn(ctx, false)
cleanUp := func() {
conn.Close()
}
return invoicesrpc.NewInvoicesClient(conn), cleanUp
}
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 := getInvoicesClient(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 := &invoicesrpc.CancelInvoiceMsg{
PaymentHash: paymentHash,
}
resp, err := client.CancelInvoice(context.Background(), invoice)
if err != nil {
return err
}
printJSON(resp)
return nil
}

View File

@ -0,0 +1,10 @@
// +build !invoicesrpc
package main
import "github.com/urfave/cli"
// invoicesCommands will return nil for non-invoicesrpc builds.
func invoicesCommands() []cli.Command {
return nil
}

View File

@ -298,6 +298,7 @@ func main() {
// Add any extra autopilot commands determined by build flags.
app.Commands = append(app.Commands, autopilotCommands()...)
app.Commands = append(app.Commands, invoicesCommands()...)
if err := app.Run(os.Args); err != nil {
fatal(err)

View File

@ -20,6 +20,10 @@ type InvoiceDatabase interface {
// SettleInvoice attempts to mark an invoice corresponding to the
// passed payment hash as fully settled.
SettleInvoice(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi) error
// CancelInvoice attempts to cancel the invoice corresponding to the
// passed payment hash.
CancelInvoice(payHash lntypes.Hash) error
}
// ChannelLink is an interface which represents the subsystem for managing the

View File

@ -2337,6 +2337,23 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
continue
}
// Reject htlcs for canceled invoices.
if invoice.Terms.State == channeldb.ContractCanceled {
l.errorf("Rejecting htlc due to canceled " +
"invoice")
failure := lnwire.NewFailUnknownPaymentHash(
pd.Amount,
)
l.sendHTLCError(
pd.HtlcIndex, failure, obfuscator,
pd.SourceRef,
)
needUpdate = true
continue
}
// If the invoice is already settled, we choose to
// accept the payment to simplify failure recovery.
//

View File

@ -218,7 +218,7 @@ func TestChannelLinkSingleHopPayment(t *testing.T) {
// * user notification to be sent.
receiver := n.bobServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -314,7 +314,7 @@ func TestChannelLinkBidirectionalOneHopPayments(t *testing.T) {
}
firstHop := n.firstBobChannelLink.ShortChanID()
_, r.err = n.makePayment(
_, r.err = makePayment(
n.aliceServer, n.bobServer, firstHop,
hopsForwards, amt, htlcAmt, totalTimelock,
).Wait(5 * time.Minute)
@ -331,7 +331,7 @@ func TestChannelLinkBidirectionalOneHopPayments(t *testing.T) {
}
firstHop := n.aliceChannelLink.ShortChanID()
_, r.err = n.makePayment(
_, r.err = makePayment(
n.bobServer, n.aliceServer, firstHop,
hopsBackwards, amt, htlcAmt, totalTimelock,
).Wait(5 * time.Minute)
@ -451,7 +451,7 @@ func TestChannelLinkMultiHopPayment(t *testing.T) {
// * user notification to be sent.
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -529,7 +529,7 @@ func TestExitNodeTimelockPayloadMismatch(t *testing.T) {
// the receiving node, instead we set it to be a random value.
hops[0].OutgoingCTLV = 500
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
htlcExpiry,
).Wait(30 * time.Second)
@ -582,7 +582,7 @@ func TestExitNodeAmountPayloadMismatch(t *testing.T) {
// receiving node expects to receive.
hops[0].AmountToForward = 1
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
htlcExpiry,
).Wait(30 * time.Second)
@ -630,7 +630,7 @@ func TestLinkForwardTimelockPolicyMismatch(t *testing.T) {
// Next, we'll make the payment which'll send an HTLC with our
// specified parameters to the first hop in the route.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
htlcExpiry,
).Wait(30 * time.Second)
@ -688,7 +688,7 @@ func TestLinkForwardFeePolicyMismatch(t *testing.T) {
// Next, we'll make the payment which'll send an HTLC with our
// specified parameters to the first hop in the route.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amountNoFee,
amountNoFee, htlcExpiry,
).Wait(30 * time.Second)
@ -746,7 +746,7 @@ func TestLinkForwardMinHTLCPolicyMismatch(t *testing.T) {
// Next, we'll make the payment which'll send an HTLC with our
// specified parameters to the first hop in the route.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
@ -805,7 +805,7 @@ func TestUpdateForwardingPolicy(t *testing.T) {
// First, send this 10 mSAT payment over the three hops, the payment
// should succeed, and all balances should be updated accordingly.
firstHop := n.firstBobChannelLink.ShortChanID()
payResp, err := n.makePayment(
payResp, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
@ -856,7 +856,7 @@ func TestUpdateForwardingPolicy(t *testing.T) {
// Next, we'll send the payment again, using the exact same per-hop
// payload for each node. This payment should fail as it won't factor
// in Bob's new fee policy.
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
@ -917,7 +917,7 @@ func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) {
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -1094,7 +1094,7 @@ func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
firstHop := n.firstBobChannelLink.ShortChanID()
receiver := n.carolServer
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
totalTimelock).Wait(30 * time.Second)
if err == nil {
@ -1201,7 +1201,7 @@ func TestChannelLinkMultiHopDecodeError(t *testing.T) {
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -1287,7 +1287,7 @@ func TestChannelLinkExpiryTooSoonExitNode(t *testing.T) {
// Now we'll send out the payment from Alice to Bob.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -1347,7 +1347,7 @@ func TestChannelLinkExpiryTooSoonMidNode(t *testing.T) {
// Now we'll send out the payment from Alice to Bob.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -1446,7 +1446,7 @@ func TestChannelLinkSingleHopMessageOrdering(t *testing.T) {
// * alice<->bob commitment state to be updated.
// * user notification to be sent.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -3283,7 +3283,7 @@ func TestChannelRetransmission(t *testing.T) {
// TODO(roasbeef); increase timeout?
receiver := n.bobServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, receiver, firstHop, hops, amount,
htlcAmt, totalTimelock,
).Wait(time.Second * 5)
@ -3578,7 +3578,7 @@ func TestChannelLinkShutdownDuringForward(t *testing.T) {
)
firstHop := n.firstBobChannelLink.ShortChanID()
n.makePayment(
makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
)
@ -3814,7 +3814,7 @@ func TestChannelLinkAcceptOverpay(t *testing.T) {
// invoice at Carol for only half of this amount.
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := n.makePayment(
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount/2, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -5119,7 +5119,7 @@ func TestForwardingAsymmetricTimeLockPolicies(t *testing.T) {
)
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -5201,3 +5201,60 @@ func TestHtlcSatisfyPolicy(t *testing.T) {
}
})
}
// TestChannelLinkCanceledInvoice in this test checks the interaction
// between Alice and Bob for a canceled invoice.
func TestChannelLinkCanceledInvoice(t *testing.T) {
t.Parallel()
// Setup a alice-bob network.
aliceChannel, bobChannel, cleanUp, err := createTwoClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newTwoHopNetwork(t, aliceChannel, bobChannel, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// Prepare an alice -> bob payment.
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.bobChannelLink)
firstHop := n.bobChannelLink.ShortChanID()
invoice, payFunc, err := preparePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
)
if err != nil {
t.Fatalf("unable to prepare the payment: %v", err)
}
// Cancel the invoice at bob's end.
hash := invoice.Terms.PaymentPreimage.Hash()
err = n.bobServer.registry.CancelInvoice(hash)
if err != nil {
t.Fatal(err)
}
// Have Alice fire the payment.
err = waitForPayFuncResult(payFunc, 30*time.Second)
// Because the invoice is canceled, we expect an unknown payment hash
// result.
fErr, ok := err.(*ForwardingError)
if !ok {
t.Fatalf("expected ForwardingError, but got %v", err)
}
_, ok = fErr.FailureMessage.(*lnwire.FailUnknownPaymentHash)
if !ok {
t.Fatalf("expected unknown payment hash, but got %v", err)
}
}

View File

@ -18,7 +18,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/go-errors/errors"
"github.com/lightningnetwork/lightning-onion"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt"
@ -735,6 +735,25 @@ func (i *mockInvoiceRegistry) SettleInvoice(rhash lntypes.Hash,
return nil
}
func (i *mockInvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
i.Lock()
defer i.Unlock()
invoice, ok := i.invoices[payHash]
if !ok {
return channeldb.ErrInvoiceNotFound
}
if invoice.Terms.State == channeldb.ContractCanceled {
return nil
}
invoice.Terms.State = channeldb.ContractCanceled
i.invoices[payHash] = invoice
return nil
}
func (i *mockInvoiceRegistry) AddInvoice(invoice channeldb.Invoice) error {
i.Lock()
defer i.Unlock()

View File

@ -1847,7 +1847,7 @@ func TestLocalPaymentNoForwardingEvents(t *testing.T) {
// proceeding.
receiver := n.bobServer
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = n.makePayment(
_, err = makePayment(
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
@ -1908,7 +1908,7 @@ func TestMultiHopPaymentForwardingEvents(t *testing.T) {
)
firstHop := n.firstBobChannelLink.ShortChanID()
for i := 0; i < numPayments/2; i++ {
_, err := n.makePayment(
_, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, finalAmt,
htlcAmt, totalTimelock,
).Wait(30 * time.Second)
@ -1961,7 +1961,7 @@ func TestMultiHopPaymentForwardingEvents(t *testing.T) {
// Send the remaining payments.
for i := numPayments / 2; i < numPayments; i++ {
_, err := n.makePayment(
_, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, finalAmt,
htlcAmt, totalTimelock,
).Wait(30 * time.Second)

View File

@ -585,9 +585,7 @@ type threeHopNetwork struct {
carolServer *mockServer
carolChannelLink *channelLink
feeEstimator *mockFeeEstimator
globalPolicy ForwardingPolicy
hopNetwork
}
// generateHops creates the per hop payload, the total amount to be sent, and
@ -657,15 +655,32 @@ type paymentResponse struct {
}
func (r *paymentResponse) Wait(d time.Duration) (lntypes.Hash, error) {
return r.rhash, waitForPaymentResult(r.err, d)
}
// waitForPaymentResult waits for either an error to be received on c or a
// timeout.
func waitForPaymentResult(c chan error, d time.Duration) error {
select {
case err := <-r.err:
close(r.err)
return r.rhash, err
case err := <-c:
close(c)
return err
case <-time.After(d):
return r.rhash, errors.New("htlc was no settled in time")
return errors.New("htlc was not settled in time")
}
}
// waitForPayFuncResult executes the given function and waits for a result with
// a timeout.
func waitForPayFuncResult(payFunc func() error, d time.Duration) error {
errChan := make(chan error)
go func() {
errChan <- payFunc()
}()
return waitForPaymentResult(errChan, d)
}
// makePayment takes the destination node and amount as input, sends the
// payment and returns the error channel to wait for error to be received and
// invoice in order to check its status after the payment finished.
@ -674,15 +689,45 @@ func (r *paymentResponse) Wait(d time.Duration) (lntypes.Hash, error) {
// * from Alice to Bob
// * from Alice to Carol through the Bob
// * from Alice to some another peer through the Bob
func (n *threeHopNetwork) makePayment(sendingPeer, receivingPeer lnpeer.Peer,
func makePayment(sendingPeer, receivingPeer lnpeer.Peer,
firstHop lnwire.ShortChannelID, hops []ForwardingInfo,
invoiceAmt, htlcAmt lnwire.MilliSatoshi,
timelock uint32) *paymentResponse {
paymentErr := make(chan error, 1)
var rhash lntypes.Hash
invoice, payFunc, err := preparePayment(sendingPeer, receivingPeer,
firstHop, hops, invoiceAmt, htlcAmt, timelock,
)
if err != nil {
paymentErr <- err
return &paymentResponse{
rhash: rhash,
err: paymentErr,
}
}
rhash = invoice.Terms.PaymentPreimage.Hash()
// Send payment and expose err channel.
go func() {
paymentErr <- payFunc()
}()
return &paymentResponse{
rhash: rhash,
err: paymentErr,
}
}
// preparePayment creates an invoice at the receivingPeer and returns a function
// that, when called, launches the payment from the sendingPeer.
func preparePayment(sendingPeer, receivingPeer lnpeer.Peer,
firstHop lnwire.ShortChannelID, hops []ForwardingInfo,
invoiceAmt, htlcAmt lnwire.MilliSatoshi,
timelock uint32) (*channeldb.Invoice, func() error, error) {
sender := sendingPeer.(*mockServer)
receiver := receivingPeer.(*mockServer)
@ -690,45 +735,27 @@ func (n *threeHopNetwork) makePayment(sendingPeer, receivingPeer lnpeer.Peer,
// htlc add request.
blob, err := generateRoute(hops...)
if err != nil {
paymentErr <- err
return &paymentResponse{
rhash: rhash,
err: paymentErr,
}
return nil, nil, err
}
// Generate payment: invoice and htlc.
invoice, htlc, err := generatePayment(invoiceAmt, htlcAmt, timelock, blob)
if err != nil {
paymentErr <- err
return &paymentResponse{
rhash: rhash,
err: paymentErr,
}
return nil, nil, err
}
rhash = fastsha256.Sum256(invoice.Terms.PaymentPreimage[:])
// Check who is last in the route and add invoice to server registry.
if err := receiver.registry.AddInvoice(*invoice); err != nil {
paymentErr <- err
return &paymentResponse{
rhash: rhash,
err: paymentErr,
}
return nil, nil, err
}
// Send payment and expose err channel.
go func() {
return invoice, func() error {
_, err := sender.htlcSwitch.SendHTLC(
firstHop, htlc, newMockDeobfuscator(),
)
paymentErr <- err
}()
return &paymentResponse{
rhash: rhash,
err: paymentErr,
}
return err
}, nil
}
// start starts the three hop network alice,bob,carol servers.
@ -854,23 +881,23 @@ func newThreeHopNetwork(t testing.TB, aliceChannel, firstBobChannel,
bobDb := firstBobChannel.State().Db
carolDb := carolChannel.State().Db
defaultDelta := uint32(6)
hopNetwork := newHopNetwork()
// Create three peers/servers.
aliceServer, err := newMockServer(
t, "alice", startingHeight, aliceDb, defaultDelta,
t, "alice", startingHeight, aliceDb, hopNetwork.defaultDelta,
)
if err != nil {
t.Fatalf("unable to create alice server: %v", err)
}
bobServer, err := newMockServer(
t, "bob", startingHeight, bobDb, defaultDelta,
t, "bob", startingHeight, bobDb, hopNetwork.defaultDelta,
)
if err != nil {
t.Fatalf("unable to create bob server: %v", err)
}
carolServer, err := newMockServer(
t, "carol", startingHeight, carolDb, defaultDelta,
t, "carol", startingHeight, carolDb, hopNetwork.defaultDelta,
)
if err != nil {
t.Fatalf("unable to create carol server: %v", err)
@ -882,17 +909,78 @@ func newThreeHopNetwork(t testing.TB, aliceChannel, firstBobChannel,
bobDecoder := newMockIteratorDecoder()
carolDecoder := newMockIteratorDecoder()
feeEstimator := &mockFeeEstimator{
byteFeeIn: make(chan lnwallet.SatPerKWeight),
quit: make(chan struct{}),
aliceChannelLink, err := hopNetwork.createChannelLink(aliceServer,
bobServer, aliceChannel, aliceDecoder,
)
if err != nil {
t.Fatal(err)
}
const (
batchTimeout = 50 * time.Millisecond
fwdPkgTimeout = 15 * time.Second
minFeeUpdateTimeout = 30 * time.Minute
maxFeeUpdateTimeout = 40 * time.Minute
)
firstBobChannelLink, err := hopNetwork.createChannelLink(bobServer,
aliceServer, firstBobChannel, bobDecoder)
if err != nil {
t.Fatal(err)
}
secondBobChannelLink, err := hopNetwork.createChannelLink(bobServer,
carolServer, secondBobChannel, bobDecoder)
if err != nil {
t.Fatal(err)
}
carolChannelLink, err := hopNetwork.createChannelLink(carolServer,
bobServer, carolChannel, carolDecoder)
if err != nil {
t.Fatal(err)
}
return &threeHopNetwork{
aliceServer: aliceServer,
aliceChannelLink: aliceChannelLink.(*channelLink),
bobServer: bobServer,
firstBobChannelLink: firstBobChannelLink.(*channelLink),
secondBobChannelLink: secondBobChannelLink.(*channelLink),
carolServer: carolServer,
carolChannelLink: carolChannelLink.(*channelLink),
hopNetwork: *hopNetwork,
}
}
// createTwoClusterChannels creates lightning channels which are needed for
// a 2 hop network cluster to be initialized.
func createTwoClusterChannels(aliceToBob, bobToCarol btcutil.Amount) (
*lnwallet.LightningChannel, *lnwallet.LightningChannel,
func(), error) {
_, _, firstChanID, _ := genIDs()
// Create lightning channels between Alice<->Bob and Bob<->Carol
aliceChannel, firstBobChannel, cleanAliceBob, _, err :=
createTestChannel(alicePrivKey, bobPrivKey, aliceToBob,
aliceToBob, 0, 0, firstChanID)
if err != nil {
return nil, nil, nil, errors.Errorf("unable to create "+
"alice<->bob channel: %v", err)
}
return aliceChannel, firstBobChannel, cleanAliceBob, nil
}
// hopNetwork is the base struct for two and three hop networks
type hopNetwork struct {
feeEstimator *mockFeeEstimator
globalPolicy ForwardingPolicy
obfuscator ErrorEncrypter
pCache *mockPreimageCache
defaultDelta uint32
}
func newHopNetwork() *hopNetwork {
defaultDelta := uint32(6)
pCache := &mockPreimageCache{
// hash -> preimage
@ -906,22 +994,47 @@ func newThreeHopNetwork(t testing.TB, aliceChannel, firstBobChannel,
}
obfuscator := NewMockObfuscator()
aliceChannelLink := NewChannelLink(
feeEstimator := &mockFeeEstimator{
byteFeeIn: make(chan lnwallet.SatPerKWeight),
quit: make(chan struct{}),
}
return &hopNetwork{
feeEstimator: feeEstimator,
globalPolicy: globalPolicy,
obfuscator: obfuscator,
pCache: pCache,
defaultDelta: defaultDelta,
}
}
func (h *hopNetwork) createChannelLink(server, peer *mockServer,
channel *lnwallet.LightningChannel,
decoder *mockIteratorDecoder) (ChannelLink, error) {
const (
batchTimeout = 50 * time.Millisecond
fwdPkgTimeout = 15 * time.Second
minFeeUpdateTimeout = 30 * time.Minute
maxFeeUpdateTimeout = 40 * time.Minute
)
link := NewChannelLink(
ChannelLinkConfig{
Switch: aliceServer.htlcSwitch,
FwrdingPolicy: globalPolicy,
Peer: bobServer,
Circuits: aliceServer.htlcSwitch.CircuitModifier(),
ForwardPackets: aliceServer.htlcSwitch.ForwardPackets,
DecodeHopIterators: aliceDecoder.DecodeHopIterators,
Switch: server.htlcSwitch,
FwrdingPolicy: h.globalPolicy,
Peer: peer,
Circuits: server.htlcSwitch.CircuitModifier(),
ForwardPackets: server.htlcSwitch.ForwardPackets,
DecodeHopIterators: decoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) (
ErrorEncrypter, lnwire.FailCode) {
return obfuscator, lnwire.CodeNone
return h.obfuscator, lnwire.CodeNone
},
FetchLastChannelUpdate: mockGetChanUpdateMessage,
Registry: aliceServer.registry,
FeeEstimator: feeEstimator,
PreimageCache: pCache,
Registry: server.registry,
FeeEstimator: h.feeEstimator,
PreimageCache: h.pCache,
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
@ -934,162 +1047,128 @@ func newThreeHopNetwork(t testing.TB, aliceChannel, firstBobChannel,
MaxFeeUpdateTimeout: maxFeeUpdateTimeout,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
},
aliceChannel,
channel,
)
if err := aliceServer.htlcSwitch.AddLink(aliceChannelLink); err != nil {
t.Fatalf("unable to add alice channel link: %v", err)
if err := server.htlcSwitch.AddLink(link); err != nil {
return nil, fmt.Errorf("unable to add channel link: %v", err)
}
go func() {
for {
select {
case <-aliceChannelLink.(*channelLink).htlcUpdates:
case <-aliceChannelLink.(*channelLink).quit:
case <-link.(*channelLink).htlcUpdates:
case <-link.(*channelLink).quit:
return
}
}
}()
firstBobChannelLink := NewChannelLink(
ChannelLinkConfig{
Switch: bobServer.htlcSwitch,
FwrdingPolicy: globalPolicy,
Peer: aliceServer,
Circuits: bobServer.htlcSwitch.CircuitModifier(),
ForwardPackets: bobServer.htlcSwitch.ForwardPackets,
DecodeHopIterators: bobDecoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) (
ErrorEncrypter, lnwire.FailCode) {
return obfuscator, lnwire.CodeNone
},
FetchLastChannelUpdate: mockGetChanUpdateMessage,
Registry: bobServer.registry,
FeeEstimator: feeEstimator,
PreimageCache: pCache,
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
ChainEvents: &contractcourt.ChainEventSubscription{},
SyncStates: true,
BatchSize: 10,
BatchTicker: ticker.MockNew(batchTimeout),
FwdPkgGCTicker: ticker.MockNew(fwdPkgTimeout),
MinFeeUpdateTimeout: minFeeUpdateTimeout,
MaxFeeUpdateTimeout: maxFeeUpdateTimeout,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
},
firstBobChannel,
)
if err := bobServer.htlcSwitch.AddLink(firstBobChannelLink); err != nil {
t.Fatalf("unable to add first bob channel link: %v", err)
}
go func() {
for {
select {
case <-firstBobChannelLink.(*channelLink).htlcUpdates:
case <-firstBobChannelLink.(*channelLink).quit:
return
}
}
}()
return link, nil
}
secondBobChannelLink := NewChannelLink(
ChannelLinkConfig{
Switch: bobServer.htlcSwitch,
FwrdingPolicy: globalPolicy,
Peer: carolServer,
Circuits: bobServer.htlcSwitch.CircuitModifier(),
ForwardPackets: bobServer.htlcSwitch.ForwardPackets,
DecodeHopIterators: bobDecoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) (
ErrorEncrypter, lnwire.FailCode) {
return obfuscator, lnwire.CodeNone
},
FetchLastChannelUpdate: mockGetChanUpdateMessage,
Registry: bobServer.registry,
FeeEstimator: feeEstimator,
PreimageCache: pCache,
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
ChainEvents: &contractcourt.ChainEventSubscription{},
SyncStates: true,
BatchSize: 10,
BatchTicker: ticker.MockNew(batchTimeout),
FwdPkgGCTicker: ticker.MockNew(fwdPkgTimeout),
MinFeeUpdateTimeout: minFeeUpdateTimeout,
MaxFeeUpdateTimeout: maxFeeUpdateTimeout,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
},
secondBobChannel,
)
if err := bobServer.htlcSwitch.AddLink(secondBobChannelLink); err != nil {
t.Fatalf("unable to add second bob channel link: %v", err)
}
go func() {
for {
select {
case <-secondBobChannelLink.(*channelLink).htlcUpdates:
case <-secondBobChannelLink.(*channelLink).quit:
return
}
}
}()
// twoHopNetwork is used for managing the created cluster of 2 hops.
type twoHopNetwork struct {
hopNetwork
carolChannelLink := NewChannelLink(
ChannelLinkConfig{
Switch: carolServer.htlcSwitch,
FwrdingPolicy: globalPolicy,
Peer: bobServer,
Circuits: carolServer.htlcSwitch.CircuitModifier(),
ForwardPackets: carolServer.htlcSwitch.ForwardPackets,
DecodeHopIterators: carolDecoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) (
ErrorEncrypter, lnwire.FailCode) {
return obfuscator, lnwire.CodeNone
},
FetchLastChannelUpdate: mockGetChanUpdateMessage,
Registry: carolServer.registry,
FeeEstimator: feeEstimator,
PreimageCache: pCache,
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
ChainEvents: &contractcourt.ChainEventSubscription{},
SyncStates: true,
BatchSize: 10,
BatchTicker: ticker.MockNew(batchTimeout),
FwdPkgGCTicker: ticker.MockNew(fwdPkgTimeout),
MinFeeUpdateTimeout: minFeeUpdateTimeout,
MaxFeeUpdateTimeout: maxFeeUpdateTimeout,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
},
carolChannel,
)
if err := carolServer.htlcSwitch.AddLink(carolChannelLink); err != nil {
t.Fatalf("unable to add carol channel link: %v", err)
}
go func() {
for {
select {
case <-carolChannelLink.(*channelLink).htlcUpdates:
case <-carolChannelLink.(*channelLink).quit:
return
}
}
}()
aliceServer *mockServer
aliceChannelLink *channelLink
return &threeHopNetwork{
bobServer *mockServer
bobChannelLink *channelLink
}
// newTwoHopNetwork function creates the following topology and returns the
// control object to manage this cluster:
//
// alice bob
// server - <-connection-> - server
// | |
// alice htlc bob htlc
// switch switch
// | |
// | |
// alice bob
// channel link channel link
//
func newTwoHopNetwork(t testing.TB,
aliceChannel, bobChannel *lnwallet.LightningChannel,
startingHeight uint32) *twoHopNetwork {
aliceDb := aliceChannel.State().Db
bobDb := bobChannel.State().Db
hopNetwork := newHopNetwork()
// Create two peers/servers.
aliceServer, err := newMockServer(
t, "alice", startingHeight, aliceDb, hopNetwork.defaultDelta,
)
if err != nil {
t.Fatalf("unable to create alice server: %v", err)
}
bobServer, err := newMockServer(
t, "bob", startingHeight, bobDb, hopNetwork.defaultDelta,
)
if err != nil {
t.Fatalf("unable to create bob server: %v", err)
}
// Create mock decoder instead of sphinx one in order to mock the route
// which htlc should follow.
aliceDecoder := newMockIteratorDecoder()
bobDecoder := newMockIteratorDecoder()
aliceChannelLink, err := hopNetwork.createChannelLink(
aliceServer, bobServer, aliceChannel, aliceDecoder,
)
if err != nil {
t.Fatal(err)
}
bobChannelLink, err := hopNetwork.createChannelLink(
bobServer, aliceServer, bobChannel, bobDecoder,
)
if err != nil {
t.Fatal(err)
}
return &twoHopNetwork{
aliceServer: aliceServer,
aliceChannelLink: aliceChannelLink.(*channelLink),
bobServer: bobServer,
firstBobChannelLink: firstBobChannelLink.(*channelLink),
secondBobChannelLink: secondBobChannelLink.(*channelLink),
bobServer: bobServer,
bobChannelLink: bobChannelLink.(*channelLink),
carolServer: carolServer,
carolChannelLink: carolChannelLink.(*channelLink),
feeEstimator: feeEstimator,
globalPolicy: globalPolicy,
hopNetwork: *hopNetwork,
}
}
// start starts the three hop network alice,bob,carol servers.
func (n *twoHopNetwork) start() error {
if err := n.aliceServer.Start(); err != nil {
return err
}
if err := n.bobServer.Start(); err != nil {
n.aliceServer.Stop()
return err
}
return nil
}
// stop stops nodes and cleanup its databases.
func (n *twoHopNetwork) stop() {
done := make(chan struct{})
go func() {
n.aliceServer.Stop()
done <- struct{}{}
}()
go func() {
n.bobServer.Stop()
done <- struct{}{}
}()
for i := 0; i < 2; i++ {
<-done
}
}

View File

@ -162,7 +162,11 @@ func (i *InvoiceRegistry) invoiceEventNotifier() {
// A sub-systems has just modified the invoice state, so we'll
// dispatch notifications to all registered clients.
case event := <-i.invoiceEvents:
i.dispatchToClients(event)
// For backwards compatibility, do not notify all
// invoice subscribers of cancel events
if event.state != channeldb.ContractCanceled {
i.dispatchToClients(event)
}
i.dispatchToSingleClients(event)
case <-i.quit:
@ -256,7 +260,7 @@ func (i *InvoiceRegistry) dispatchToClients(event *invoiceEvent) {
case channeldb.ContractOpen:
client.addIndex = invoice.AddIndex
default:
log.Errorf("unknown invoice state: %v", event.state)
log.Errorf("unexpected invoice state: %v", event.state)
}
}
}
@ -476,6 +480,34 @@ func (i *InvoiceRegistry) SettleInvoice(rHash lntypes.Hash,
return nil
}
// CancelInvoice attempts to cancel the invoice corresponding to the passed
// payment hash.
func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
i.Lock()
defer i.Unlock()
log.Debugf("Canceling invoice %v", payHash)
invoice, err := i.cdb.CancelInvoice(payHash)
// Implement idempotency by returning success if the invoice was already
// canceled.
if err == channeldb.ErrInvoiceAlreadyCanceled {
log.Debugf("Invoice %v already canceled", payHash)
return nil
}
if err != nil {
return err
}
log.Infof("Invoice %v canceled", payHash)
i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
return nil
}
// notifyClients notifies all currently registered invoice notification clients
// of a newly added/settled invoice.
func (i *InvoiceRegistry) notifyClients(hash lntypes.Hash,

View File

@ -150,6 +150,122 @@ func TestSettleInvoice(t *testing.T) {
if inv.AmtPaid != amtPaid {
t.Fatal("expected amount to be unchanged")
}
// Try to cancel.
err = registry.CancelInvoice(hash)
if err != channeldb.ErrInvoiceAlreadySettled {
t.Fatal("expected cancelation of a settled invoice to fail")
}
}
// TestCancelInvoice tests cancelation of an invoice and related notifications.
func TestCancelInvoice(t *testing.T) {
cdb, cleanup, err := newDB()
if err != nil {
t.Fatal(err)
}
defer cleanup()
// Instantiate and start the invoice registry.
registry := NewRegistry(cdb, &chaincfg.MainNetParams)
err = registry.Start()
if err != nil {
t.Fatal(err)
}
defer registry.Stop()
allSubscriptions := registry.SubscribeNotifications(0, 0)
defer allSubscriptions.Cancel()
// Try to cancel the not yet existing invoice. This should fail.
err = registry.CancelInvoice(hash)
if err != channeldb.ErrInvoiceNotFound {
t.Fatalf("expected ErrInvoiceNotFound, but got %v", err)
}
// Subscribe to the not yet existing invoice.
subscription := registry.SubscribeSingleInvoice(hash)
defer subscription.Cancel()
if subscription.hash != hash {
t.Fatalf("expected subscription for provided hash")
}
// Add the invoice.
amt := lnwire.MilliSatoshi(100000)
invoice := &channeldb.Invoice{
Terms: channeldb.ContractTerm{
PaymentPreimage: preimage,
Value: amt,
},
}
_, err = registry.AddInvoice(invoice, hash)
if err != nil {
t.Fatal(err)
}
// We expect the open state to be sent to the single invoice subscriber.
select {
case update := <-subscription.Updates:
if update.Terms.State != channeldb.ContractOpen {
t.Fatalf(
"expected state ContractOpen, but got %v",
update.Terms.State,
)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// We expect a new invoice notification to be sent out.
select {
case newInvoice := <-allSubscriptions.NewInvoices:
if newInvoice.Terms.State != channeldb.ContractOpen {
t.Fatalf(
"expected state ContractOpen, but got %v",
newInvoice.Terms.State,
)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// Cancel invoice.
err = registry.CancelInvoice(hash)
if err != nil {
t.Fatal(err)
}
// We expect the canceled state to be sent to the single invoice
// subscriber.
select {
case update := <-subscription.Updates:
if update.Terms.State != channeldb.ContractCanceled {
t.Fatalf(
"expected state ContractCanceled, but got %v",
update.Terms.State,
)
}
case <-time.After(testTimeout):
t.Fatal("no update received")
}
// We expect no cancel notification to be sent to all invoice
// subscribers (backwards compatibility).
// Try to cancel again.
err = registry.CancelInvoice(hash)
if err != nil {
t.Fatal("expected cancelation of a canceled invoice to succeed")
}
// Try to settle. This should not be possible.
err = registry.SettleInvoice(hash, amt)
if err != channeldb.ErrInvoiceAlreadyCanceled {
t.Fatal("expected settlement of a canceled invoice to fail")
}
}
func newDB() (*channeldb.DB, func(), error) {

View File

@ -25,6 +25,80 @@ var _ = math.Inf
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type CancelInvoiceMsg struct {
// / Hash corresponding to the invoice to cancel.
PaymentHash []byte `protobuf:"bytes,1,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *CancelInvoiceMsg) Reset() { *m = CancelInvoiceMsg{} }
func (m *CancelInvoiceMsg) String() string { return proto.CompactTextString(m) }
func (*CancelInvoiceMsg) ProtoMessage() {}
func (*CancelInvoiceMsg) Descriptor() ([]byte, []int) {
return fileDescriptor_invoices_1b708c9c030aea0e, []int{0}
}
func (m *CancelInvoiceMsg) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_CancelInvoiceMsg.Unmarshal(m, b)
}
func (m *CancelInvoiceMsg) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_CancelInvoiceMsg.Marshal(b, m, deterministic)
}
func (dst *CancelInvoiceMsg) XXX_Merge(src proto.Message) {
xxx_messageInfo_CancelInvoiceMsg.Merge(dst, src)
}
func (m *CancelInvoiceMsg) XXX_Size() int {
return xxx_messageInfo_CancelInvoiceMsg.Size(m)
}
func (m *CancelInvoiceMsg) XXX_DiscardUnknown() {
xxx_messageInfo_CancelInvoiceMsg.DiscardUnknown(m)
}
var xxx_messageInfo_CancelInvoiceMsg proto.InternalMessageInfo
func (m *CancelInvoiceMsg) GetPaymentHash() []byte {
if m != nil {
return m.PaymentHash
}
return nil
}
type CancelInvoiceResp struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *CancelInvoiceResp) Reset() { *m = CancelInvoiceResp{} }
func (m *CancelInvoiceResp) String() string { return proto.CompactTextString(m) }
func (*CancelInvoiceResp) ProtoMessage() {}
func (*CancelInvoiceResp) Descriptor() ([]byte, []int) {
return fileDescriptor_invoices_1b708c9c030aea0e, []int{1}
}
func (m *CancelInvoiceResp) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_CancelInvoiceResp.Unmarshal(m, b)
}
func (m *CancelInvoiceResp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_CancelInvoiceResp.Marshal(b, m, deterministic)
}
func (dst *CancelInvoiceResp) XXX_Merge(src proto.Message) {
xxx_messageInfo_CancelInvoiceResp.Merge(dst, src)
}
func (m *CancelInvoiceResp) XXX_Size() int {
return xxx_messageInfo_CancelInvoiceResp.Size(m)
}
func (m *CancelInvoiceResp) XXX_DiscardUnknown() {
xxx_messageInfo_CancelInvoiceResp.DiscardUnknown(m)
}
var xxx_messageInfo_CancelInvoiceResp proto.InternalMessageInfo
func init() {
proto.RegisterType((*CancelInvoiceMsg)(nil), "invoicesrpc.CancelInvoiceMsg")
proto.RegisterType((*CancelInvoiceResp)(nil), "invoicesrpc.CancelInvoiceResp")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
@ -42,6 +116,11 @@ type InvoicesClient interface {
// to notify the client of state transitions of the specified invoice.
// Initially the current invoice state is always sent out.
SubscribeSingleInvoice(ctx context.Context, in *lnrpc.PaymentHash, opts ...grpc.CallOption) (Invoices_SubscribeSingleInvoiceClient, error)
// *
// CancelInvoice cancels a currently open invoice. If the invoice is already
// canceled, this call will succeed. If the invoice is already settled, it will
// fail.
CancelInvoice(ctx context.Context, in *CancelInvoiceMsg, opts ...grpc.CallOption) (*CancelInvoiceResp, error)
}
type invoicesClient struct {
@ -84,6 +163,15 @@ func (x *invoicesSubscribeSingleInvoiceClient) Recv() (*lnrpc.Invoice, error) {
return m, nil
}
func (c *invoicesClient) CancelInvoice(ctx context.Context, in *CancelInvoiceMsg, opts ...grpc.CallOption) (*CancelInvoiceResp, error) {
out := new(CancelInvoiceResp)
err := c.cc.Invoke(ctx, "/invoicesrpc.Invoices/CancelInvoice", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// InvoicesServer is the server API for Invoices service.
type InvoicesServer interface {
// *
@ -91,6 +179,11 @@ type InvoicesServer interface {
// to notify the client of state transitions of the specified invoice.
// Initially the current invoice state is always sent out.
SubscribeSingleInvoice(*lnrpc.PaymentHash, Invoices_SubscribeSingleInvoiceServer) error
// *
// CancelInvoice cancels a currently open invoice. If the invoice is already
// canceled, this call will succeed. If the invoice is already settled, it will
// fail.
CancelInvoice(context.Context, *CancelInvoiceMsg) (*CancelInvoiceResp, error)
}
func RegisterInvoicesServer(s *grpc.Server, srv InvoicesServer) {
@ -118,10 +211,33 @@ func (x *invoicesSubscribeSingleInvoiceServer) Send(m *lnrpc.Invoice) error {
return x.ServerStream.SendMsg(m)
}
func _Invoices_CancelInvoice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CancelInvoiceMsg)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(InvoicesServer).CancelInvoice(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/invoicesrpc.Invoices/CancelInvoice",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(InvoicesServer).CancelInvoice(ctx, req.(*CancelInvoiceMsg))
}
return interceptor(ctx, in, info, handler)
}
var _Invoices_serviceDesc = grpc.ServiceDesc{
ServiceName: "invoicesrpc.Invoices",
HandlerType: (*InvoicesServer)(nil),
Methods: []grpc.MethodDesc{},
Methods: []grpc.MethodDesc{
{
MethodName: "CancelInvoice",
Handler: _Invoices_CancelInvoice_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "SubscribeSingleInvoice",
@ -133,21 +249,25 @@ var _Invoices_serviceDesc = grpc.ServiceDesc{
}
func init() {
proto.RegisterFile("invoicesrpc/invoices.proto", fileDescriptor_invoices_c6414974947f2940)
proto.RegisterFile("invoicesrpc/invoices.proto", fileDescriptor_invoices_1b708c9c030aea0e)
}
var fileDescriptor_invoices_c6414974947f2940 = []byte{
// 177 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8e, 0xb1, 0x8e, 0xc2, 0x30,
0x10, 0x44, 0x75, 0xcd, 0xe9, 0x2e, 0x27, 0x5d, 0xe1, 0x82, 0xc2, 0xe2, 0x1b, 0xb2, 0x40, 0x7a,
0x0a, 0x2a, 0xa0, 0x42, 0x4a, 0x47, 0x67, 0x1b, 0xcb, 0x59, 0xe1, 0xec, 0x5a, 0xce, 0x06, 0xc4,
0xdf, 0x23, 0x82, 0x91, 0xd2, 0x8d, 0x66, 0xe6, 0x49, 0xaf, 0xd2, 0x48, 0x37, 0x46, 0xe7, 0x87,
0x9c, 0x1c, 0x7c, 0x72, 0x9d, 0x32, 0x0b, 0xab, 0xbf, 0xd9, 0xa6, 0x97, 0x81, 0x39, 0x44, 0x0f,
0x26, 0x21, 0x18, 0x22, 0x16, 0x23, 0xc8, 0x54, 0xae, 0xfa, 0x37, 0x27, 0xf7, 0x8e, 0x9b, 0x63,
0xf5, 0x73, 0x28, 0x9c, 0xda, 0x56, 0x8b, 0x76, 0xb4, 0x83, 0xcb, 0x68, 0x7d, 0x8b, 0x14, 0xa2,
0x2f, 0x93, 0x52, 0x75, 0xa4, 0x17, 0x73, 0x32, 0x8f, 0xde, 0x93, 0xec, 0xcd, 0xd0, 0xe9, 0xff,
0xd2, 0x95, 0xcf, 0xea, 0x6b, 0xd7, 0x9c, 0xd7, 0x01, 0xa5, 0x1b, 0x6d, 0xed, 0xb8, 0x87, 0x88,
0xa1, 0x13, 0x42, 0x0a, 0xe4, 0xe5, 0xce, 0xf9, 0x0a, 0x91, 0x2e, 0x30, 0x21, 0x30, 0x33, 0xb5,
0xdf, 0x93, 0x47, 0xf3, 0x0c, 0x00, 0x00, 0xff, 0xff, 0xcf, 0x4e, 0x97, 0xf2, 0xdb, 0x00, 0x00,
0x00,
var fileDescriptor_invoices_1b708c9c030aea0e = []byte{
// 246 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x90, 0xbf, 0x4b, 0x43, 0x31,
0x10, 0xc7, 0x79, 0x8b, 0x68, 0x5a, 0x45, 0x23, 0x88, 0x04, 0x15, 0xed, 0xe4, 0x94, 0xa8, 0xc5,
0xd5, 0x41, 0x17, 0x1d, 0x14, 0x69, 0x37, 0x17, 0xc9, 0x8b, 0x21, 0x39, 0x4c, 0xef, 0x42, 0x92,
0x2a, 0xfe, 0x2b, 0xfe, 0xb5, 0xd2, 0x36, 0xe2, 0x7b, 0x42, 0xb7, 0xcb, 0x7d, 0x7f, 0xe4, 0x93,
0x30, 0x01, 0xf8, 0x41, 0x60, 0x6c, 0x4e, 0xd1, 0xa8, 0xdf, 0x59, 0xc6, 0x44, 0x85, 0xf8, 0xa0,
0xa3, 0x89, 0x23, 0x47, 0xe4, 0x82, 0x55, 0x3a, 0x82, 0xd2, 0x88, 0x54, 0x74, 0x01, 0xc2, 0x6a,
0x15, 0x5b, 0x29, 0x9a, 0xd5, 0x38, 0xba, 0x66, 0xbb, 0x77, 0x1a, 0x8d, 0x0d, 0x0f, 0xab, 0xf4,
0x63, 0x76, 0xfc, 0x8c, 0x0d, 0xa3, 0xfe, 0x9a, 0x59, 0x2c, 0xaf, 0x5e, 0x67, 0x7f, 0xd8, 0x9c,
0x36, 0xe7, 0xc3, 0xc9, 0xa0, 0xee, 0xee, 0x75, 0xf6, 0xa3, 0x7d, 0xb6, 0xd7, 0x8b, 0x4d, 0x6c,
0x8e, 0x57, 0xdf, 0x0d, 0xdb, 0xac, 0xe7, 0xcc, 0x6f, 0xd8, 0xc1, 0x74, 0xde, 0x66, 0x93, 0xa0,
0xb5, 0x53, 0x40, 0x17, 0x6c, 0x95, 0x38, 0x97, 0x01, 0x17, 0x00, 0xcf, 0x7f, 0x7d, 0x62, 0xa7,
0xee, 0xaa, 0xe7, 0xa2, 0xe1, 0x4f, 0x6c, 0xbb, 0x77, 0x03, 0x3f, 0x96, 0x9d, 0x07, 0xca, 0xff,
0xd0, 0xe2, 0x64, 0xbd, 0xbc, 0x80, 0xbb, 0x1d, 0xbf, 0x5c, 0x3a, 0x28, 0x7e, 0xde, 0x4a, 0x43,
0x33, 0x15, 0xc0, 0xf9, 0x82, 0x80, 0x0e, 0x6d, 0xf9, 0xa4, 0xf4, 0xae, 0x02, 0xbe, 0xa9, 0x25,
0x82, 0xea, 0xd4, 0xb4, 0x1b, 0xcb, 0x4f, 0x1a, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0xd2, 0xc3,
0x7e, 0x3a, 0x78, 0x01, 0x00, 0x00,
}

View File

@ -16,5 +16,17 @@ service Invoices {
Initially the current invoice state is always sent out.
*/
rpc SubscribeSingleInvoice (lnrpc.PaymentHash) returns (stream lnrpc.Invoice);
/**
CancelInvoice cancels a currently open invoice. If the invoice is already
canceled, this call will succeed. If the invoice is already settled, it will
fail.
*/
rpc CancelInvoice(CancelInvoiceMsg) returns (CancelInvoiceResp);
}
message CancelInvoiceMsg {
/// Hash corresponding to the invoice to cancel.
bytes payment_hash = 1;
}
message CancelInvoiceResp {}

View File

@ -8,10 +8,11 @@ import (
"os"
"path/filepath"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
)
const (
@ -26,6 +27,10 @@ var (
// macaroonOps are the set of capabilities that our minted macaroon (if
// it doesn't already exist) will have.
macaroonOps = []bakery.Op{
{
Entity: "invoices",
Action: "write",
},
{
Entity: "invoices",
Action: "read",
@ -38,6 +43,10 @@ var (
Entity: "invoices",
Action: "read",
}},
"/invoicesrpc.Invoices/CancelInvoice": {{
Entity: "invoices",
Action: "write",
}},
}
// DefaultInvoicesMacFilename is the default name of the invoices
@ -181,3 +190,24 @@ func (s *Server) SubscribeSingleInvoice(req *lnrpc.PaymentHash,
}
}
}
// CancelInvoice cancels a currently open invoice. If the invoice is already
// canceled, this call will succeed. If the invoice is already settled, it will
// fail.
func (s *Server) CancelInvoice(ctx context.Context,
in *CancelInvoiceMsg) (*CancelInvoiceResp, error) {
paymentHash, err := lntypes.NewHash(in.PaymentHash)
if err != nil {
return nil, err
}
err = s.cfg.InvoiceRegistry.CancelInvoice(*paymentHash)
if err != nil {
return nil, err
}
log.Infof("Canceled invoice %v", paymentHash)
return &CancelInvoiceResp{}, nil
}

View File

@ -59,8 +59,11 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
state = lnrpc.Invoice_OPEN
case channeldb.ContractSettled:
state = lnrpc.Invoice_SETTLED
case channeldb.ContractCanceled:
state = lnrpc.Invoice_CANCELED
default:
return nil, fmt.Errorf("unknown invoice state")
return nil, fmt.Errorf("unknown invoice state %v",
invoice.Terms.State)
}
return &lnrpc.Invoice{

File diff suppressed because it is too large Load Diff

View File

@ -1850,6 +1850,7 @@ message Invoice {
enum InvoiceState {
OPEN = 0;
SETTLED = 1;
CANCELED = 2;
}
/**

View File

@ -1141,7 +1141,8 @@
"type": "string",
"enum": [
"OPEN",
"SETTLED"
"SETTLED",
"CANCELED"
],
"default": "OPEN"
},