Merge pull request #3770 from joostjager/mpp-prep-registry

channeldb+invoices: prepare invoice db for mpp
This commit is contained in:
Joost Jager 2019-12-05 11:38:56 +01:00 committed by GitHub
commit 91d716be1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 575 additions and 251 deletions

View File

@ -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,

View File

@ -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,
},

View File

@ -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

View File

@ -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
}

View File

@ -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
}

164
invoices/update.go Normal file
View File

@ -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
}

View File

@ -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)
}