diff --git a/channeldb/db.go b/channeldb/db.go index cf811aa6..c3f63234 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -137,7 +137,8 @@ type DB struct { *bbolt.DB dbPath string graph *ChannelGraph - now func() time.Time + + Now func() time.Time } // Open opens an existing channeldb. Any necessary schemas migrations due to @@ -171,7 +172,7 @@ func Open(dbPath string, modifiers ...OptionModifier) (*DB, error) { chanDB := &DB{ DB: bdb, dbPath: dbPath, - now: time.Now, + Now: time.Now, } chanDB.graph = newChannelGraph( chanDB, opts.RejectCacheSize, opts.ChannelCacheSize, diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index 46687e25..d244f31b 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" ) @@ -184,6 +185,69 @@ func TestInvoiceWorkflow(t *testing.T) { } } +// TestInvoiceCancelSingleHtlc tests that a single htlc can be canceled on the +// invoice. +func TestInvoiceCancelSingleHtlc(t *testing.T) { + t.Parallel() + + db, cleanUp, err := makeTestDB() + defer cleanUp() + if err != nil { + t.Fatalf("unable to make test db: %v", err) + } + + testInvoice := &Invoice{ + Htlcs: map[CircuitKey]*InvoiceHTLC{}, + } + testInvoice.Terms.Value = lnwire.NewMSatFromSatoshis(10000) + testInvoice.Terms.Features = emptyFeatures + + var paymentHash lntypes.Hash + if _, err := db.AddInvoice(testInvoice, paymentHash); err != nil { + t.Fatalf("unable to find invoice: %v", err) + } + + // Accept an htlc on this invoice. + key := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4} + invoice, err := db.UpdateInvoice(paymentHash, + func(invoice *Invoice) (*InvoiceUpdateDesc, error) { + return &InvoiceUpdateDesc{ + AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{ + key: { + Amt: 500, + }, + }, + }, nil + }) + if err != nil { + t.Fatalf("unable to add invoice htlc: %v", err) + } + if len(invoice.Htlcs) != 1 { + t.Fatalf("expected the htlc to be added") + } + if invoice.Htlcs[key].State != HtlcStateAccepted { + t.Fatalf("expected htlc in state accepted") + } + + // Cancel the htlc again. + invoice, err = db.UpdateInvoice(paymentHash, func(invoice *Invoice) (*InvoiceUpdateDesc, error) { + return &InvoiceUpdateDesc{ + CancelHtlcs: map[CircuitKey]struct{}{ + key: {}, + }, + }, nil + }) + if err != nil { + t.Fatalf("unable to cancel htlc: %v", err) + } + if len(invoice.Htlcs) != 1 { + t.Fatalf("expected the htlc to be present") + } + if invoice.Htlcs[key].State != HtlcStateCanceled { + t.Fatalf("expected htlc in state canceled") + } +} + // TestInvoiceTimeSeries tests that newly added invoices invoices, as well as // settled invoices are added to the database are properly placed in the add // add or settle index which serves as an event time series. @@ -337,7 +401,7 @@ func TestDuplicateSettleInvoice(t *testing.T) { if err != nil { t.Fatalf("unable to make test db: %v", err) } - db.now = func() time.Time { return time.Unix(1, 0) } + db.Now = func() time.Time { return time.Unix(1, 0) } // We'll start out by creating an invoice and writing it to the DB. amt := lnwire.NewMSatFromSatoshis(1000) @@ -684,9 +748,11 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) InvoiceUpdateCallback { } update := &InvoiceUpdateDesc{ - Preimage: invoice.Terms.PaymentPreimage, - State: ContractSettled, - Htlcs: map[CircuitKey]*HtlcAcceptDesc{ + State: &InvoiceStateUpdateDesc{ + Preimage: invoice.Terms.PaymentPreimage, + NewState: ContractSettled, + }, + AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{ {}: { Amt: amt, }, diff --git a/channeldb/invoices.go b/channeldb/invoices.go index d06ef46a..73668cdc 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -76,6 +76,18 @@ var ( // ErrInvoiceStillOpen is returned when the invoice is still open. ErrInvoiceStillOpen = errors.New("invoice still open") + + // ErrInvoiceCannotOpen is returned when an attempt is made to move an + // invoice to the open state. + ErrInvoiceCannotOpen = errors.New("cannot move invoice to open") + + // ErrInvoiceCannotAccept is returned when an attempt is made to accept + // an invoice while the invoice is not in the open state. + ErrInvoiceCannotAccept = errors.New("cannot accept invoice") + + // ErrInvoicePreimageMismatch is returned when the preimage doesn't + // match the invoice hash. + ErrInvoicePreimageMismatch = errors.New("preimage does not match") ) const ( @@ -313,15 +325,24 @@ type HtlcAcceptDesc struct { // InvoiceUpdateDesc describes the changes that should be applied to the // invoice. type InvoiceUpdateDesc struct { - // State is the new state that this invoice should progress to. - State ContractState + // State is the new state that this invoice should progress to. If nil, + // the state is left unchanged. + State *InvoiceStateUpdateDesc - // Htlcs describes the changes that need to be made to the invoice htlcs - // in the database. Htlc map entries with their value set should be - // added. If the map value is nil, the htlc should be canceled. - Htlcs map[CircuitKey]*HtlcAcceptDesc + // CancelHtlcs describes the htlcs that need to be canceled. + CancelHtlcs map[CircuitKey]struct{} - // Preimage must be set to the preimage when state is settled. + // AddHtlcs describes the newly accepted htlcs that need to be added to + // the invoice. + AddHtlcs map[CircuitKey]*HtlcAcceptDesc +} + +// InvoiceStateUpdateDesc describes an invoice-level state transition. +type InvoiceStateUpdateDesc struct { + // NewState is the new state that this invoice should progress to. + NewState ContractState + + // Preimage must be set to the preimage when NewState is settled. Preimage lntypes.Preimage } @@ -1229,8 +1250,6 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke return nil, err } - preUpdateState := invoice.State - // Create deep copy to prevent any accidental modification in the // callback. invoiceCopy := copyInvoice(&invoice) @@ -1241,79 +1260,102 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke return &invoice, err } - // Update invoice state. - invoice.State = update.State + // If there is nothing to update, return early. + if update == nil { + return &invoice, nil + } - now := d.now() + now := d.Now() - // Update htlc set. - for key, htlcUpdate := range update.Htlcs { - htlc, ok := invoice.Htlcs[key] - - // No update means the htlc needs to be canceled. - if htlcUpdate == nil { - if !ok { - return nil, fmt.Errorf("unknown htlc %v", key) - } - if htlc.State != HtlcStateAccepted { - return nil, fmt.Errorf("can only cancel " + - "accepted htlcs") - } - - htlc.State = HtlcStateCanceled - htlc.ResolveTime = now - invoice.AmtPaid -= htlc.Amt - - continue + // Update invoice state if the update descriptor indicates an invoice + // state change. + if update.State != nil { + err := updateInvoiceState(&invoice, hash, *update.State) + if err != nil { + return nil, err } - // Add new htlc paying to the invoice. - if ok { - return nil, fmt.Errorf("htlc %v already exists", key) + if update.State.NewState == ContractSettled { + err := setSettleMetaFields( + settleIndex, invoiceNum, &invoice, now, + ) + if err != nil { + return nil, err + } } - htlc = &InvoiceHTLC{ + } + + // Process add actions from update descriptor. + for key, htlcUpdate := range update.AddHtlcs { + if _, exists := invoice.Htlcs[key]; exists { + return nil, fmt.Errorf("duplicate add of htlc %v", key) + } + htlc := &InvoiceHTLC{ Amt: htlcUpdate.Amt, Expiry: htlcUpdate.Expiry, AcceptHeight: uint32(htlcUpdate.AcceptHeight), AcceptTime: now, - } - if preUpdateState == ContractSettled { - htlc.State = HtlcStateSettled - htlc.ResolveTime = now - } else { - htlc.State = HtlcStateAccepted + State: HtlcStateAccepted, } invoice.Htlcs[key] = htlc - invoice.AmtPaid += htlc.Amt } - // If invoice moved to the settled state, update settle index and settle - // time. - if preUpdateState != invoice.State && - invoice.State == ContractSettled { - - if update.Preimage.Hash() != hash { - return nil, fmt.Errorf("preimage does not match") - } - invoice.Terms.PaymentPreimage = update.Preimage - - // Settle all accepted htlcs. - for _, htlc := range invoice.Htlcs { - if htlc.State != HtlcStateAccepted { - continue + // Align htlc states with invoice state and recalculate amount paid. + var ( + amtPaid lnwire.MilliSatoshi + cancelHtlcs = update.CancelHtlcs + ) + for key, htlc := range invoice.Htlcs { + // Check whether this htlc needs to be canceled. If it does, + // update the htlc state to Canceled. + _, cancel := cancelHtlcs[key] + if cancel { + // Consistency check to verify that there is no overlap + // between the add and cancel sets. + if _, added := update.AddHtlcs[key]; added { + return nil, fmt.Errorf("added htlc %v canceled", + key) } - htlc.State = HtlcStateSettled - htlc.ResolveTime = now + err := cancelSingleHtlc(now, htlc, invoice.State) + if err != nil { + return nil, err + } + + // Delete processed cancel action, so that we can check + // later that there are no actions left. + delete(cancelHtlcs, key) + + continue } - err := setSettleFields(settleIndex, invoiceNum, &invoice, now) + // The invoice state may have changed and this could have + // implications for the states of the individual htlcs. Align + // the htlc state with the current invoice state. + err := updateHtlc(now, htlc, invoice.State) if err != nil { return nil, err } + + // Update the running amount paid to this invoice. We don't + // include accepted htlcs when the invoice is still open. + if invoice.State != ContractOpen && + (htlc.State == HtlcStateAccepted || + htlc.State == HtlcStateSettled) { + + amtPaid += htlc.Amt + } + } + invoice.AmtPaid = amtPaid + + // Verify that we didn't get an action for htlcs that are not present on + // the invoice. + if len(cancelHtlcs) > 0 { + return nil, errors.New("cancel action on non-existent htlc(s)") } + // Reserialize and update invoice. var buf bytes.Buffer if err := serializeInvoice(&buf, &invoice); err != nil { return nil, err @@ -1326,7 +1368,119 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke return &invoice, nil } -func setSettleFields(settleIndex *bbolt.Bucket, invoiceNum []byte, +// updateInvoiceState validates and processes an invoice state update. +func updateInvoiceState(invoice *Invoice, hash lntypes.Hash, + update InvoiceStateUpdateDesc) error { + + // Returning to open is never allowed from any state. + if update.NewState == ContractOpen { + return ErrInvoiceCannotOpen + } + + switch invoice.State { + + // Once a contract is accepted, we can only transition to settled or + // canceled. Forbid transitioning back into this state. Otherwise this + // state is identical to ContractOpen, so we fallthrough to apply the + // same checks that we apply to open invoices. + case ContractAccepted: + if update.NewState == ContractAccepted { + return ErrInvoiceCannotAccept + } + + fallthrough + + // If a contract is open, permit a state transition to accepted, settled + // or canceled. The only restriction is on transitioning to settled + // where we ensure the preimage is valid. + case ContractOpen: + if update.NewState == ContractSettled { + // Validate preimage. + if update.Preimage.Hash() != hash { + return ErrInvoicePreimageMismatch + } + invoice.Terms.PaymentPreimage = update.Preimage + } + + // Once settled, we are in a terminal state. + case ContractSettled: + return ErrInvoiceAlreadySettled + + // Once canceled, we are in a terminal state. + case ContractCanceled: + return ErrInvoiceAlreadyCanceled + + default: + return errors.New("unknown state transition") + } + + invoice.State = update.NewState + + return nil +} + +// cancelSingleHtlc validates cancelation of a single htlc and update its state. +func cancelSingleHtlc(resolveTime time.Time, htlc *InvoiceHTLC, + invState ContractState) error { + + // It is only possible to cancel individual htlcs on an open invoice. + if invState != ContractOpen { + return fmt.Errorf("htlc canceled on invoice in "+ + "state %v", invState) + } + + // It is only possible if the htlc is still pending. + if htlc.State != HtlcStateAccepted { + return fmt.Errorf("htlc canceled in state %v", + htlc.State) + } + + htlc.State = HtlcStateCanceled + htlc.ResolveTime = resolveTime + + return nil +} + +// updateHtlc aligns the state of an htlc with the given invoice state. +func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC, + invState ContractState) error { + + switch invState { + + case ContractSettled: + if htlc.State == HtlcStateAccepted { + htlc.State = HtlcStateSettled + htlc.ResolveTime = resolveTime + } + + case ContractCanceled: + switch htlc.State { + + case HtlcStateAccepted: + htlc.State = HtlcStateCanceled + htlc.ResolveTime = resolveTime + + case HtlcStateSettled: + return fmt.Errorf("cannot have a settled htlc with " + + "invoice in state canceled") + } + + case ContractOpen, ContractAccepted: + if htlc.State == HtlcStateSettled { + return fmt.Errorf("cannot have a settled htlc with "+ + "invoice in state %v", invState) + } + + default: + return errors.New("unknown state transition") + } + + return nil +} + +// setSettleMetaFields updates the metadata associated with settlement of an +// invoice. +func setSettleMetaFields(settleIndex *bbolt.Bucket, invoiceNum []byte, invoice *Invoice, now time.Time) error { // Now that we know the invoice hasn't already been settled, we'll @@ -1343,7 +1497,6 @@ func setSettleFields(settleIndex *bbolt.Bucket, invoiceNum []byte, return err } - invoice.State = ContractSettled invoice.SettleDate = now invoice.SettleIndex = nextSettleSeqNo diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 51b57c09..f388a666 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -24,9 +24,6 @@ var ( // ErrShuttingDown is returned when an operation failed because the // invoice registry is shutting down. ErrShuttingDown = errors.New("invoice registry shutting down") - - // errNoUpdate is returned when no invoice updated is required. - errNoUpdate = errors.New("no update needed") ) // HodlEvent describes how an htlc should be resolved. If HodlEvent.Preimage is @@ -415,9 +412,8 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice, return i.cdb.LookupInvoice(rHash) } -// NotifyExitHopHtlc attempts to mark an invoice as settled. If the invoice is a -// debug invoice, then this method is a noop as debug invoices are never fully -// settled. The return value describes how the htlc should be resolved. +// NotifyExitHopHtlc attempts to mark an invoice as settled. The return value +// describes how the htlc should be resolved. // // When the preimage of the invoice is not yet known (hodl invoice), this // function moves the invoice to the accepted state. When SettleHoldInvoice is @@ -439,111 +435,48 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, rHash[:], s, amtPaid, expiry, circuitKey) } - // Default is to not update subscribers after the invoice update. - updateSubscribers := false - - updateInvoice := func(inv *channeldb.Invoice) ( - *channeldb.InvoiceUpdateDesc, error) { - - // Don't update the invoice when this is a replayed htlc. - htlc, ok := inv.Htlcs[circuitKey] - if ok { - switch htlc.State { - case channeldb.HtlcStateCanceled: - debugLog("replayed htlc to canceled invoice") - - case channeldb.HtlcStateAccepted: - debugLog("replayed htlc to accepted invoice") - - case channeldb.HtlcStateSettled: - debugLog("replayed htlc to settled invoice") - - default: - return nil, errors.New("unexpected htlc state") - } - - return nil, errNoUpdate - } - - // If the invoice is already canceled, there is no further - // checking to do. - if inv.State == channeldb.ContractCanceled { - debugLog("invoice already canceled") - return nil, errNoUpdate - } - - // If an invoice amount is specified, check that enough - // is paid. Also check this for duplicate payments if - // the invoice is already settled or accepted. - if inv.Terms.Value > 0 && amtPaid < inv.Terms.Value { - debugLog("amount too low") - return nil, errNoUpdate - } - - // The invoice is still open. Check the expiry. - if expiry < uint32(currentHeight+i.finalCltvRejectDelta) { - debugLog("expiry too soon") - return nil, errNoUpdate - } - - if expiry < uint32(currentHeight+inv.Terms.FinalCltvDelta) { - debugLog("expiry too soon") - return nil, errNoUpdate - } - - // Record HTLC in the invoice database. - newHtlcs := map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc{ - circuitKey: { - Amt: amtPaid, - Expiry: expiry, - AcceptHeight: currentHeight, - }, - } - - update := channeldb.InvoiceUpdateDesc{ - Htlcs: newHtlcs, - } - - // Don't update invoice state if we are accepting a duplicate - // payment. We do accept or settle the HTLC. - switch inv.State { - case channeldb.ContractAccepted: - debugLog("accepting duplicate payment to accepted invoice") - update.State = channeldb.ContractAccepted - return &update, nil - - case channeldb.ContractSettled: - debugLog("accepting duplicate payment to settled invoice") - update.State = channeldb.ContractSettled - return &update, nil - } - - // Check to see if we can settle or this is an hold invoice and - // we need to wait for the preimage. - holdInvoice := inv.Terms.PaymentPreimage == channeldb.UnknownPreimage - if holdInvoice { - debugLog("accepted") - update.State = channeldb.ContractAccepted - } else { - debugLog("settled") - update.Preimage = inv.Terms.PaymentPreimage - update.State = channeldb.ContractSettled - } - - updateSubscribers = true - - return &update, nil + // Create the update context containing the relevant details of the + // incoming htlc. + updateCtx := invoiceUpdateCtx{ + circuitKey: circuitKey, + amtPaid: amtPaid, + expiry: expiry, + currentHeight: currentHeight, + finalCltvRejectDelta: i.finalCltvRejectDelta, } // We'll attempt to settle an invoice matching this rHash on disk (if - // one exists). The callback will set the resolution action that is - // returned to the link or contract resolver. - invoice, err := i.cdb.UpdateInvoice(rHash, updateInvoice) - if err != nil && err != errNoUpdate { + // one exists). The callback will update the invoice state and/or htlcs. + var ( + result updateResult + updateSubscribers bool + ) + invoice, err := i.cdb.UpdateInvoice( + rHash, + func(inv *channeldb.Invoice) ( + *channeldb.InvoiceUpdateDesc, error) { + + updateDesc, res, err := updateInvoice(&updateCtx, inv) + if err != nil { + return nil, err + } + + // Only send an update if the invoice state was changed. + updateSubscribers = updateDesc != nil && + updateDesc.State != nil + + // Assign result to outer scope variable. + result = res + + return updateDesc, nil + }, + ) + if err != nil { debugLog(err.Error()) return nil, err } + debugLog(result.String()) if updateSubscribers { i.notifyClients(rHash, invoice, invoice.State) @@ -607,8 +540,10 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error { } return &channeldb.InvoiceUpdateDesc{ - State: channeldb.ContractSettled, - Preimage: preimage, + State: &channeldb.InvoiceStateUpdateDesc{ + NewState: channeldb.ContractSettled, + Preimage: preimage, + }, }, nil } @@ -655,39 +590,13 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error { updateInvoice := func(invoice *channeldb.Invoice) ( *channeldb.InvoiceUpdateDesc, error) { - switch invoice.State { - case channeldb.ContractSettled: - return nil, channeldb.ErrInvoiceAlreadySettled - case channeldb.ContractCanceled: - return nil, channeldb.ErrInvoiceAlreadyCanceled - } - - // Mark individual held htlcs as canceled. - canceledHtlcs := make( - map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc, - ) - for key, htlc := range invoice.Htlcs { - switch htlc.State { - - // If we get here, there shouldn't be any settled htlcs. - case channeldb.HtlcStateSettled: - return nil, errors.New("cannot cancel " + - "invoice with settled htlc(s)") - - // Don't cancel htlcs that were already canceled, - // because it would incorrectly modify the invoice paid - // amt. - case channeldb.HtlcStateCanceled: - continue - } - - canceledHtlcs[key] = nil - } - - // Move invoice to the canceled state. + // Move invoice to the canceled state. Rely on validation in + // channeldb to return an error if the invoice is already + // settled or canceled. return &channeldb.InvoiceUpdateDesc{ - Htlcs: canceledHtlcs, - State: channeldb.ContractCanceled, + State: &channeldb.InvoiceStateUpdateDesc{ + NewState: channeldb.ContractCanceled, + }, }, nil } diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go index 9e6fc62a..bb14ba12 100644 --- a/invoices/invoiceregistry_test.go +++ b/invoices/invoiceregistry_test.go @@ -9,6 +9,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" ) var ( @@ -32,10 +33,13 @@ var ( testFeatures = lnwire.NewFeatureVector( nil, lnwire.Features, ) + + testPayload = &mockPayload{} ) var ( - testInvoice = &channeldb.Invoice{ + testInvoiceAmt = lnwire.MilliSatoshi(100000) + testInvoice = &channeldb.Invoice{ Terms: channeldb.ContractTerm{ PaymentPreimage: preimage, Value: lnwire.MilliSatoshi(100000), @@ -46,19 +50,26 @@ var ( testHodlInvoice = &channeldb.Invoice{ Terms: channeldb.ContractTerm{ PaymentPreimage: channeldb.UnknownPreimage, - Value: lnwire.MilliSatoshi(100000), + Value: testInvoiceAmt, Features: testFeatures, }, } ) -func newTestContext(t *testing.T) (*InvoiceRegistry, func()) { +type testContext struct { + registry *InvoiceRegistry + + cleanup func() + t *testing.T +} + +func newTestContext(t *testing.T) *testContext { cdb, cleanup, err := newDB() if err != nil { t.Fatal(err) } - // Instantiate and start the invoice registry. + // Instantiate and start the invoice ctx.registry. registry := NewRegistry(cdb, testFinalCltvRejectDelta) err = registry.Start() @@ -67,10 +78,16 @@ func newTestContext(t *testing.T) (*InvoiceRegistry, func()) { t.Fatal(err) } - return registry, func() { - registry.Stop() - cleanup() + ctx := testContext{ + registry: registry, + t: t, + cleanup: func() { + registry.Stop() + cleanup() + }, } + + return &ctx } func getCircuitKey(htlcID uint64) channeldb.CircuitKey { @@ -84,14 +101,14 @@ func getCircuitKey(htlcID uint64) channeldb.CircuitKey { // TestSettleInvoice tests settling of an invoice and related notifications. func TestSettleInvoice(t *testing.T) { - registry, cleanup := newTestContext(t) - defer cleanup() + ctx := newTestContext(t) + defer ctx.cleanup() - allSubscriptions := registry.SubscribeNotifications(0, 0) + allSubscriptions := ctx.registry.SubscribeNotifications(0, 0) defer allSubscriptions.Cancel() // Subscribe to the not yet existing invoice. - subscription, err := registry.SubscribeSingleInvoice(hash) + subscription, err := ctx.registry.SubscribeSingleInvoice(hash) if err != nil { t.Fatal(err) } @@ -102,7 +119,7 @@ func TestSettleInvoice(t *testing.T) { } // Add the invoice. - addIdx, err := registry.AddInvoice(testInvoice, hash) + addIdx, err := ctx.registry.AddInvoice(testInvoice, hash) if err != nil { t.Fatal(err) } @@ -137,10 +154,10 @@ func TestSettleInvoice(t *testing.T) { hodlChan := make(chan interface{}, 1) // Try to settle invoice with an htlc that expires too soon. - event, err := registry.NotifyExitHopHtlc( + event, err := ctx.registry.NotifyExitHopHtlc( hash, testInvoice.Terms.Value, uint32(testCurrentHeight)+testInvoiceCltvDelta-1, - testCurrentHeight, getCircuitKey(10), hodlChan, nil, + testCurrentHeight, getCircuitKey(10), hodlChan, testPayload, ) if err != nil { t.Fatal(err) @@ -155,9 +172,9 @@ func TestSettleInvoice(t *testing.T) { // Settle invoice with a slightly higher amount. amtPaid := lnwire.MilliSatoshi(100500) - _, err = registry.NotifyExitHopHtlc( + _, err = ctx.registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatal(err) @@ -191,9 +208,9 @@ func TestSettleInvoice(t *testing.T) { // Try to settle again with the same htlc id. We need this idempotent // behaviour after a restart. - event, err = registry.NotifyExitHopHtlc( + event, err = ctx.registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err) @@ -205,9 +222,9 @@ func TestSettleInvoice(t *testing.T) { // Try to settle again with a new higher-valued htlc. This payment // should also be accepted, to prevent any change in behaviour for a // paid invoice that may open up a probe vector. - event, err = registry.NotifyExitHopHtlc( + event, err = ctx.registry.NotifyExitHopHtlc( hash, amtPaid+600, testHtlcExpiry, testCurrentHeight, - getCircuitKey(1), hodlChan, nil, + getCircuitKey(1), hodlChan, testPayload, ) if err != nil { t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err) @@ -218,9 +235,9 @@ func TestSettleInvoice(t *testing.T) { // Try to settle again with a lower amount. This should fail just as it // would have failed if it were the first payment. - event, err = registry.NotifyExitHopHtlc( + event, err = ctx.registry.NotifyExitHopHtlc( hash, amtPaid-600, testHtlcExpiry, testCurrentHeight, - getCircuitKey(2), hodlChan, nil, + getCircuitKey(2), hodlChan, testPayload, ) if err != nil { t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err) @@ -231,7 +248,7 @@ func TestSettleInvoice(t *testing.T) { // Check that settled amount is equal to the sum of values of the htlcs // 0 and 1. - inv, err := registry.LookupInvoice(hash) + inv, err := ctx.registry.LookupInvoice(hash) if err != nil { t.Fatal(err) } @@ -240,7 +257,7 @@ func TestSettleInvoice(t *testing.T) { } // Try to cancel. - err = registry.CancelInvoice(hash) + err = ctx.registry.CancelInvoice(hash) if err != channeldb.ErrInvoiceAlreadySettled { t.Fatal("expected cancelation of a settled invoice to fail") } @@ -255,20 +272,20 @@ func TestSettleInvoice(t *testing.T) { // TestCancelInvoice tests cancelation of an invoice and related notifications. func TestCancelInvoice(t *testing.T) { - registry, cleanup := newTestContext(t) - defer cleanup() + ctx := newTestContext(t) + defer ctx.cleanup() - allSubscriptions := registry.SubscribeNotifications(0, 0) + allSubscriptions := ctx.registry.SubscribeNotifications(0, 0) defer allSubscriptions.Cancel() // Try to cancel the not yet existing invoice. This should fail. - err := registry.CancelInvoice(hash) + err := ctx.registry.CancelInvoice(hash) if err != channeldb.ErrInvoiceNotFound { t.Fatalf("expected ErrInvoiceNotFound, but got %v", err) } // Subscribe to the not yet existing invoice. - subscription, err := registry.SubscribeSingleInvoice(hash) + subscription, err := ctx.registry.SubscribeSingleInvoice(hash) if err != nil { t.Fatal(err) } @@ -280,7 +297,7 @@ func TestCancelInvoice(t *testing.T) { // Add the invoice. amt := lnwire.MilliSatoshi(100000) - _, err = registry.AddInvoice(testInvoice, hash) + _, err = ctx.registry.AddInvoice(testInvoice, hash) if err != nil { t.Fatal(err) } @@ -312,7 +329,7 @@ func TestCancelInvoice(t *testing.T) { } // Cancel invoice. - err = registry.CancelInvoice(hash) + err = ctx.registry.CancelInvoice(hash) if err != nil { t.Fatal(err) } @@ -335,7 +352,7 @@ func TestCancelInvoice(t *testing.T) { // subscribers (backwards compatibility). // Try to cancel again. - err = registry.CancelInvoice(hash) + err = ctx.registry.CancelInvoice(hash) if err != nil { t.Fatal("expected cancelation of a canceled invoice to succeed") } @@ -343,9 +360,9 @@ func TestCancelInvoice(t *testing.T) { // Notify arrival of a new htlc paying to this invoice. This should // result in a cancel event. hodlChan := make(chan interface{}) - event, err := registry.NotifyExitHopHtlc( + event, err := ctx.registry.NotifyExitHopHtlc( hash, amt, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatal("expected settlement of a canceled invoice to succeed") @@ -371,7 +388,7 @@ func TestSettleHoldInvoice(t *testing.T) { } defer cleanup() - // Instantiate and start the invoice registry. + // Instantiate and start the invoice ctx.registry. registry := NewRegistry(cdb, testFinalCltvRejectDelta) err = registry.Start() @@ -423,7 +440,7 @@ func TestSettleHoldInvoice(t *testing.T) { // should be possible. event, err := registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatalf("expected settle to succeed but got %v", err) @@ -435,7 +452,7 @@ func TestSettleHoldInvoice(t *testing.T) { // Test idempotency. event, err = registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatalf("expected settle to succeed but got %v", err) @@ -448,7 +465,7 @@ func TestSettleHoldInvoice(t *testing.T) { // is a replay. event, err = registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight+10, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatalf("expected settle to succeed but got %v", err) @@ -461,7 +478,7 @@ func TestSettleHoldInvoice(t *testing.T) { // requirement. It should be rejected. event, err = registry.NotifyExitHopHtlc( hash, amtPaid, 1, testCurrentHeight, - getCircuitKey(1), hodlChan, nil, + getCircuitKey(1), hodlChan, testPayload, ) if err != nil { t.Fatalf("expected settle to succeed but got %v", err) @@ -539,7 +556,7 @@ func TestCancelHoldInvoice(t *testing.T) { } defer cleanup() - // Instantiate and start the invoice registry. + // Instantiate and start the invoice ctx.registry. registry := NewRegistry(cdb, testFinalCltvRejectDelta) err = registry.Start() @@ -561,7 +578,7 @@ func TestCancelHoldInvoice(t *testing.T) { // should be possible. event, err := registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatalf("expected settle to succeed but got %v", err) @@ -586,7 +603,7 @@ func TestCancelHoldInvoice(t *testing.T) { // accept height. event, err = registry.NotifyExitHopHtlc( hash, amtPaid, testHtlcExpiry, testCurrentHeight+1, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != nil { t.Fatalf("expected settle to succeed but got %v", err) @@ -629,18 +646,26 @@ func newDB() (*channeldb.DB, func(), error) { // the exit hop, but in htlcIncomingContestResolver it is called with forwarded // htlc hashes as well. func TestUnknownInvoice(t *testing.T) { - registry, cleanup := newTestContext(t) - defer cleanup() + ctx := newTestContext(t) + defer ctx.cleanup() // Notify arrival of a new htlc paying to this invoice. This should // succeed. hodlChan := make(chan interface{}) amt := lnwire.MilliSatoshi(100000) - _, err := registry.NotifyExitHopHtlc( + _, err := ctx.registry.NotifyExitHopHtlc( hash, amt, testHtlcExpiry, testCurrentHeight, - getCircuitKey(0), hodlChan, nil, + getCircuitKey(0), hodlChan, testPayload, ) if err != channeldb.ErrInvoiceNotFound { t.Fatal("expected invoice not found error") } } + +type mockPayload struct { + mpp *record.MPP +} + +func (p *mockPayload) MultiPath() *record.MPP { + return p.mpp +} diff --git a/invoices/update.go b/invoices/update.go new file mode 100644 index 00000000..7a1b0324 --- /dev/null +++ b/invoices/update.go @@ -0,0 +1,164 @@ +package invoices + +import ( + "errors" + + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwire" +) + +// updateResult is the result of the invoice update call. +type updateResult uint8 + +const ( + resultInvalid updateResult = iota + resultReplayToCanceled + resultReplayToAccepted + resultReplayToSettled + resultInvoiceAlreadyCanceled + resultAmountTooLow + resultExpiryTooSoon + resultDuplicateToAccepted + resultDuplicateToSettled + resultAccepted + resultSettled +) + +// String returns a human-readable representation of the invoice update result. +func (u updateResult) String() string { + switch u { + + case resultInvalid: + return "invalid" + + case resultReplayToCanceled: + return "replayed htlc to canceled invoice" + + case resultReplayToAccepted: + return "replayed htlc to accepted invoice" + + case resultReplayToSettled: + return "replayed htlc to settled invoice" + + case resultInvoiceAlreadyCanceled: + return "invoice already canceled" + + case resultAmountTooLow: + return "amount too low" + + case resultExpiryTooSoon: + return "expiry too soon" + + case resultDuplicateToAccepted: + return "accepting duplicate payment to accepted invoice" + + case resultDuplicateToSettled: + return "accepting duplicate payment to settled invoice" + + case resultAccepted: + return "accepted" + + case resultSettled: + return "settled" + + default: + return "unknown" + } +} + +// invoiceUpdateCtx is an object that describes the context for the invoice +// update to be carried out. +type invoiceUpdateCtx struct { + circuitKey channeldb.CircuitKey + amtPaid lnwire.MilliSatoshi + expiry uint32 + currentHeight int32 + finalCltvRejectDelta int32 +} + +// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice +// settlement logic. +func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) ( + *channeldb.InvoiceUpdateDesc, updateResult, error) { + + // Don't update the invoice when this is a replayed htlc. + htlc, ok := inv.Htlcs[ctx.circuitKey] + if ok { + switch htlc.State { + case channeldb.HtlcStateCanceled: + return nil, resultReplayToCanceled, nil + + case channeldb.HtlcStateAccepted: + return nil, resultReplayToAccepted, nil + + case channeldb.HtlcStateSettled: + return nil, resultReplayToSettled, nil + + default: + return nil, 0, errors.New("unknown htlc state") + } + } + + // If the invoice is already canceled, there is no further checking to + // do. + if inv.State == channeldb.ContractCanceled { + return nil, resultInvoiceAlreadyCanceled, nil + } + + // If an invoice amount is specified, check that enough is paid. Also + // check this for duplicate payments if the invoice is already settled + // or accepted. In case this is a zero-valued invoice, it will always be + // enough. + if ctx.amtPaid < inv.Terms.Value { + return nil, resultAmountTooLow, nil + } + + // The invoice is still open. Check the expiry. + if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) { + return nil, resultExpiryTooSoon, nil + } + + if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) { + return nil, resultExpiryTooSoon, nil + } + + // Record HTLC in the invoice database. + newHtlcs := map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc{ + ctx.circuitKey: { + Amt: ctx.amtPaid, + Expiry: ctx.expiry, + AcceptHeight: ctx.currentHeight, + }, + } + + update := channeldb.InvoiceUpdateDesc{ + AddHtlcs: newHtlcs, + } + + // Don't update invoice state if we are accepting a duplicate payment. + // We do accept or settle the HTLC. + switch inv.State { + case channeldb.ContractAccepted: + return &update, resultDuplicateToAccepted, nil + + case channeldb.ContractSettled: + return &update, resultDuplicateToSettled, nil + } + + // Check to see if we can settle or this is an hold invoice and we need + // to wait for the preimage. + holdInvoice := inv.Terms.PaymentPreimage == channeldb.UnknownPreimage + if holdInvoice { + update.State = &channeldb.InvoiceStateUpdateDesc{ + NewState: channeldb.ContractAccepted, + } + return &update, resultAccepted, nil + } + + update.State = &channeldb.InvoiceStateUpdateDesc{ + NewState: channeldb.ContractSettled, + Preimage: inv.Terms.PaymentPreimage, + } + + return &update, resultSettled, nil +} diff --git a/record/mpp.go b/record/mpp.go index b28d1085..38877caf 100644 --- a/record/mpp.go +++ b/record/mpp.go @@ -1,6 +1,7 @@ package record import ( + "fmt" "io" "github.com/lightningnetwork/lnd/lnwire" @@ -96,3 +97,8 @@ func (r *MPP) Record() tlv.Record { MPPOnionType, r, size, MPPEncoder, MPPDecoder, ) } + +// String returns a human-readable representation of the mpp payload field. +func (r *MPP) String() string { + return fmt.Sprintf("total=%v, addr=%x", r.totalMsat, r.paymentAddr) +}