Merge pull request #3770 from joostjager/mpp-prep-registry
channeldb+invoices: prepare invoice db for mpp
This commit is contained in:
commit
91d716be1f
@ -137,7 +137,8 @@ type DB struct {
|
|||||||
*bbolt.DB
|
*bbolt.DB
|
||||||
dbPath string
|
dbPath string
|
||||||
graph *ChannelGraph
|
graph *ChannelGraph
|
||||||
now func() time.Time
|
|
||||||
|
Now func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open opens an existing channeldb. Any necessary schemas migrations due to
|
// 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{
|
chanDB := &DB{
|
||||||
DB: bdb,
|
DB: bdb,
|
||||||
dbPath: dbPath,
|
dbPath: dbPath,
|
||||||
now: time.Now,
|
Now: time.Now,
|
||||||
}
|
}
|
||||||
chanDB.graph = newChannelGraph(
|
chanDB.graph = newChannelGraph(
|
||||||
chanDB, opts.RejectCacheSize, opts.ChannelCacheSize,
|
chanDB, opts.RejectCacheSize, opts.ChannelCacheSize,
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"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
|
// TestInvoiceTimeSeries tests that newly added invoices invoices, as well as
|
||||||
// settled invoices are added to the database are properly placed in the add
|
// settled invoices are added to the database are properly placed in the add
|
||||||
// add or settle index which serves as an event time series.
|
// add or settle index which serves as an event time series.
|
||||||
@ -337,7 +401,7 @@ func TestDuplicateSettleInvoice(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to make test db: %v", err)
|
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.
|
// We'll start out by creating an invoice and writing it to the DB.
|
||||||
amt := lnwire.NewMSatFromSatoshis(1000)
|
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
@ -684,9 +748,11 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) InvoiceUpdateCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update := &InvoiceUpdateDesc{
|
update := &InvoiceUpdateDesc{
|
||||||
|
State: &InvoiceStateUpdateDesc{
|
||||||
Preimage: invoice.Terms.PaymentPreimage,
|
Preimage: invoice.Terms.PaymentPreimage,
|
||||||
State: ContractSettled,
|
NewState: ContractSettled,
|
||||||
Htlcs: map[CircuitKey]*HtlcAcceptDesc{
|
},
|
||||||
|
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
|
||||||
{}: {
|
{}: {
|
||||||
Amt: amt,
|
Amt: amt,
|
||||||
},
|
},
|
||||||
|
@ -76,6 +76,18 @@ var (
|
|||||||
|
|
||||||
// ErrInvoiceStillOpen is returned when the invoice is still open.
|
// ErrInvoiceStillOpen is returned when the invoice is still open.
|
||||||
ErrInvoiceStillOpen = errors.New("invoice 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 (
|
const (
|
||||||
@ -313,15 +325,24 @@ type HtlcAcceptDesc struct {
|
|||||||
// InvoiceUpdateDesc describes the changes that should be applied to the
|
// InvoiceUpdateDesc describes the changes that should be applied to the
|
||||||
// invoice.
|
// invoice.
|
||||||
type InvoiceUpdateDesc struct {
|
type InvoiceUpdateDesc struct {
|
||||||
// State is the new state that this invoice should progress to.
|
// State is the new state that this invoice should progress to. If nil,
|
||||||
State ContractState
|
// the state is left unchanged.
|
||||||
|
State *InvoiceStateUpdateDesc
|
||||||
|
|
||||||
// Htlcs describes the changes that need to be made to the invoice htlcs
|
// CancelHtlcs describes the htlcs that need to be canceled.
|
||||||
// in the database. Htlc map entries with their value set should be
|
CancelHtlcs map[CircuitKey]struct{}
|
||||||
// added. If the map value is nil, the htlc should be canceled.
|
|
||||||
Htlcs map[CircuitKey]*HtlcAcceptDesc
|
|
||||||
|
|
||||||
// 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
|
Preimage lntypes.Preimage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1229,8 +1250,6 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
preUpdateState := invoice.State
|
|
||||||
|
|
||||||
// Create deep copy to prevent any accidental modification in the
|
// Create deep copy to prevent any accidental modification in the
|
||||||
// callback.
|
// callback.
|
||||||
invoiceCopy := copyInvoice(&invoice)
|
invoiceCopy := copyInvoice(&invoice)
|
||||||
@ -1241,79 +1260,102 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
|
|||||||
return &invoice, err
|
return &invoice, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update invoice state.
|
// If there is nothing to update, return early.
|
||||||
invoice.State = update.State
|
if update == nil {
|
||||||
|
return &invoice, nil
|
||||||
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
|
now := d.Now()
|
||||||
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 update.State.NewState == ContractSettled {
|
||||||
if ok {
|
err := setSettleMetaFields(
|
||||||
return nil, fmt.Errorf("htlc %v already exists", key)
|
settleIndex, invoiceNum, &invoice, now,
|
||||||
}
|
)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
htlc.State = HtlcStateSettled
|
|
||||||
htlc.ResolveTime = now
|
|
||||||
}
|
|
||||||
|
|
||||||
err := setSettleFields(settleIndex, invoiceNum, &invoice, now)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
State: HtlcStateAccepted,
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.Htlcs[key] = htlc
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
var buf bytes.Buffer
|
||||||
if err := serializeInvoice(&buf, &invoice); err != nil {
|
if err := serializeInvoice(&buf, &invoice); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1326,7 +1368,119 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
|
|||||||
return &invoice, nil
|
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 {
|
invoice *Invoice, now time.Time) error {
|
||||||
|
|
||||||
// Now that we know the invoice hasn't already been settled, we'll
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.State = ContractSettled
|
|
||||||
invoice.SettleDate = now
|
invoice.SettleDate = now
|
||||||
invoice.SettleIndex = nextSettleSeqNo
|
invoice.SettleIndex = nextSettleSeqNo
|
||||||
|
|
||||||
|
@ -24,9 +24,6 @@ var (
|
|||||||
// ErrShuttingDown is returned when an operation failed because the
|
// ErrShuttingDown is returned when an operation failed because the
|
||||||
// invoice registry is shutting down.
|
// invoice registry is shutting down.
|
||||||
ErrShuttingDown = errors.New("invoice registry 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
|
// 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)
|
return i.cdb.LookupInvoice(rHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the invoice is a
|
// NotifyExitHopHtlc attempts to mark an invoice as settled. The return value
|
||||||
// debug invoice, then this method is a noop as debug invoices are never fully
|
// describes how the htlc should be resolved.
|
||||||
// settled. The return value describes how the htlc should be resolved.
|
|
||||||
//
|
//
|
||||||
// When the preimage of the invoice is not yet known (hodl invoice), this
|
// When the preimage of the invoice is not yet known (hodl invoice), this
|
||||||
// function moves the invoice to the accepted state. When SettleHoldInvoice is
|
// 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)
|
rHash[:], s, amtPaid, expiry, circuitKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default is to not update subscribers after the invoice update.
|
// Create the update context containing the relevant details of the
|
||||||
updateSubscribers := false
|
// incoming htlc.
|
||||||
|
updateCtx := invoiceUpdateCtx{
|
||||||
updateInvoice := func(inv *channeldb.Invoice) (
|
circuitKey: circuitKey,
|
||||||
*channeldb.InvoiceUpdateDesc, error) {
|
amtPaid: amtPaid,
|
||||||
|
expiry: expiry,
|
||||||
// Don't update the invoice when this is a replayed htlc.
|
currentHeight: currentHeight,
|
||||||
htlc, ok := inv.Htlcs[circuitKey]
|
finalCltvRejectDelta: i.finalCltvRejectDelta,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll attempt to settle an invoice matching this rHash on disk (if
|
// We'll attempt to settle an invoice matching this rHash on disk (if
|
||||||
// one exists). The callback will set the resolution action that is
|
// one exists). The callback will update the invoice state and/or htlcs.
|
||||||
// returned to the link or contract resolver.
|
var (
|
||||||
invoice, err := i.cdb.UpdateInvoice(rHash, updateInvoice)
|
result updateResult
|
||||||
if err != nil && err != errNoUpdate {
|
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())
|
debugLog(err.Error())
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
debugLog(result.String())
|
||||||
|
|
||||||
if updateSubscribers {
|
if updateSubscribers {
|
||||||
i.notifyClients(rHash, invoice, invoice.State)
|
i.notifyClients(rHash, invoice, invoice.State)
|
||||||
@ -607,8 +540,10 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &channeldb.InvoiceUpdateDesc{
|
return &channeldb.InvoiceUpdateDesc{
|
||||||
State: channeldb.ContractSettled,
|
State: &channeldb.InvoiceStateUpdateDesc{
|
||||||
|
NewState: channeldb.ContractSettled,
|
||||||
Preimage: preimage,
|
Preimage: preimage,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,39 +590,13 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
|
|||||||
updateInvoice := func(invoice *channeldb.Invoice) (
|
updateInvoice := func(invoice *channeldb.Invoice) (
|
||||||
*channeldb.InvoiceUpdateDesc, error) {
|
*channeldb.InvoiceUpdateDesc, error) {
|
||||||
|
|
||||||
switch invoice.State {
|
// Move invoice to the canceled state. Rely on validation in
|
||||||
case channeldb.ContractSettled:
|
// channeldb to return an error if the invoice is already
|
||||||
return nil, channeldb.ErrInvoiceAlreadySettled
|
// settled or canceled.
|
||||||
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.
|
|
||||||
return &channeldb.InvoiceUpdateDesc{
|
return &channeldb.InvoiceUpdateDesc{
|
||||||
Htlcs: canceledHtlcs,
|
State: &channeldb.InvoiceStateUpdateDesc{
|
||||||
State: channeldb.ContractCanceled,
|
NewState: channeldb.ContractCanceled,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -32,9 +33,12 @@ var (
|
|||||||
testFeatures = lnwire.NewFeatureVector(
|
testFeatures = lnwire.NewFeatureVector(
|
||||||
nil, lnwire.Features,
|
nil, lnwire.Features,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testPayload = &mockPayload{}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
testInvoiceAmt = lnwire.MilliSatoshi(100000)
|
||||||
testInvoice = &channeldb.Invoice{
|
testInvoice = &channeldb.Invoice{
|
||||||
Terms: channeldb.ContractTerm{
|
Terms: channeldb.ContractTerm{
|
||||||
PaymentPreimage: preimage,
|
PaymentPreimage: preimage,
|
||||||
@ -46,19 +50,26 @@ var (
|
|||||||
testHodlInvoice = &channeldb.Invoice{
|
testHodlInvoice = &channeldb.Invoice{
|
||||||
Terms: channeldb.ContractTerm{
|
Terms: channeldb.ContractTerm{
|
||||||
PaymentPreimage: channeldb.UnknownPreimage,
|
PaymentPreimage: channeldb.UnknownPreimage,
|
||||||
Value: lnwire.MilliSatoshi(100000),
|
Value: testInvoiceAmt,
|
||||||
Features: testFeatures,
|
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()
|
cdb, cleanup, err := newDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate and start the invoice registry.
|
// Instantiate and start the invoice ctx.registry.
|
||||||
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
||||||
|
|
||||||
err = registry.Start()
|
err = registry.Start()
|
||||||
@ -67,10 +78,16 @@ func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return registry, func() {
|
ctx := testContext{
|
||||||
|
registry: registry,
|
||||||
|
t: t,
|
||||||
|
cleanup: func() {
|
||||||
registry.Stop()
|
registry.Stop()
|
||||||
cleanup()
|
cleanup()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCircuitKey(htlcID uint64) channeldb.CircuitKey {
|
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.
|
// TestSettleInvoice tests settling of an invoice and related notifications.
|
||||||
func TestSettleInvoice(t *testing.T) {
|
func TestSettleInvoice(t *testing.T) {
|
||||||
registry, cleanup := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
defer cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
allSubscriptions := registry.SubscribeNotifications(0, 0)
|
allSubscriptions := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Subscribe to the not yet existing invoice.
|
// Subscribe to the not yet existing invoice.
|
||||||
subscription, err := registry.SubscribeSingleInvoice(hash)
|
subscription, err := ctx.registry.SubscribeSingleInvoice(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -102,7 +119,7 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the invoice.
|
// Add the invoice.
|
||||||
addIdx, err := registry.AddInvoice(testInvoice, hash)
|
addIdx, err := ctx.registry.AddInvoice(testInvoice, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -137,10 +154,10 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
hodlChan := make(chan interface{}, 1)
|
hodlChan := make(chan interface{}, 1)
|
||||||
|
|
||||||
// Try to settle invoice with an htlc that expires too soon.
|
// Try to settle invoice with an htlc that expires too soon.
|
||||||
event, err := registry.NotifyExitHopHtlc(
|
event, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, testInvoice.Terms.Value,
|
hash, testInvoice.Terms.Value,
|
||||||
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
|
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
|
||||||
testCurrentHeight, getCircuitKey(10), hodlChan, nil,
|
testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -155,9 +172,9 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
|
|
||||||
// Settle invoice with a slightly higher amount.
|
// Settle invoice with a slightly higher amount.
|
||||||
amtPaid := lnwire.MilliSatoshi(100500)
|
amtPaid := lnwire.MilliSatoshi(100500)
|
||||||
_, err = registry.NotifyExitHopHtlc(
|
_, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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
|
// Try to settle again with the same htlc id. We need this idempotent
|
||||||
// behaviour after a restart.
|
// behaviour after a restart.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
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
|
// Try to settle again with a new higher-valued htlc. This payment
|
||||||
// should also be accepted, to prevent any change in behaviour for a
|
// should also be accepted, to prevent any change in behaviour for a
|
||||||
// paid invoice that may open up a probe vector.
|
// paid invoice that may open up a probe vector.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(1), hodlChan, nil,
|
getCircuitKey(1), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
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
|
// Try to settle again with a lower amount. This should fail just as it
|
||||||
// would have failed if it were the first payment.
|
// would have failed if it were the first payment.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(2), hodlChan, nil,
|
getCircuitKey(2), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
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
|
// Check that settled amount is equal to the sum of values of the htlcs
|
||||||
// 0 and 1.
|
// 0 and 1.
|
||||||
inv, err := registry.LookupInvoice(hash)
|
inv, err := ctx.registry.LookupInvoice(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -240,7 +257,7 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to cancel.
|
// Try to cancel.
|
||||||
err = registry.CancelInvoice(hash)
|
err = ctx.registry.CancelInvoice(hash)
|
||||||
if err != channeldb.ErrInvoiceAlreadySettled {
|
if err != channeldb.ErrInvoiceAlreadySettled {
|
||||||
t.Fatal("expected cancelation of a settled invoice to fail")
|
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.
|
// TestCancelInvoice tests cancelation of an invoice and related notifications.
|
||||||
func TestCancelInvoice(t *testing.T) {
|
func TestCancelInvoice(t *testing.T) {
|
||||||
registry, cleanup := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
defer cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
allSubscriptions := registry.SubscribeNotifications(0, 0)
|
allSubscriptions := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Try to cancel the not yet existing invoice. This should fail.
|
// Try to cancel the not yet existing invoice. This should fail.
|
||||||
err := registry.CancelInvoice(hash)
|
err := ctx.registry.CancelInvoice(hash)
|
||||||
if err != channeldb.ErrInvoiceNotFound {
|
if err != channeldb.ErrInvoiceNotFound {
|
||||||
t.Fatalf("expected ErrInvoiceNotFound, but got %v", err)
|
t.Fatalf("expected ErrInvoiceNotFound, but got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to the not yet existing invoice.
|
// Subscribe to the not yet existing invoice.
|
||||||
subscription, err := registry.SubscribeSingleInvoice(hash)
|
subscription, err := ctx.registry.SubscribeSingleInvoice(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -280,7 +297,7 @@ func TestCancelInvoice(t *testing.T) {
|
|||||||
|
|
||||||
// Add the invoice.
|
// Add the invoice.
|
||||||
amt := lnwire.MilliSatoshi(100000)
|
amt := lnwire.MilliSatoshi(100000)
|
||||||
_, err = registry.AddInvoice(testInvoice, hash)
|
_, err = ctx.registry.AddInvoice(testInvoice, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -312,7 +329,7 @@ func TestCancelInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cancel invoice.
|
// Cancel invoice.
|
||||||
err = registry.CancelInvoice(hash)
|
err = ctx.registry.CancelInvoice(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -335,7 +352,7 @@ func TestCancelInvoice(t *testing.T) {
|
|||||||
// subscribers (backwards compatibility).
|
// subscribers (backwards compatibility).
|
||||||
|
|
||||||
// Try to cancel again.
|
// Try to cancel again.
|
||||||
err = registry.CancelInvoice(hash)
|
err = ctx.registry.CancelInvoice(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("expected cancelation of a canceled invoice to succeed")
|
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
|
// Notify arrival of a new htlc paying to this invoice. This should
|
||||||
// result in a cancel event.
|
// result in a cancel event.
|
||||||
hodlChan := make(chan interface{})
|
hodlChan := make(chan interface{})
|
||||||
event, err := registry.NotifyExitHopHtlc(
|
event, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, amt, testHtlcExpiry, testCurrentHeight,
|
hash, amt, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("expected settlement of a canceled invoice to succeed")
|
t.Fatal("expected settlement of a canceled invoice to succeed")
|
||||||
@ -371,7 +388,7 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Instantiate and start the invoice registry.
|
// Instantiate and start the invoice ctx.registry.
|
||||||
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
||||||
|
|
||||||
err = registry.Start()
|
err = registry.Start()
|
||||||
@ -423,7 +440,7 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
// should be possible.
|
// should be possible.
|
||||||
event, err := registry.NotifyExitHopHtlc(
|
event, err := registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
@ -435,7 +452,7 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
// Test idempotency.
|
// Test idempotency.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
@ -448,7 +465,7 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
// is a replay.
|
// is a replay.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
@ -461,7 +478,7 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
// requirement. It should be rejected.
|
// requirement. It should be rejected.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, 1, testCurrentHeight,
|
hash, amtPaid, 1, testCurrentHeight,
|
||||||
getCircuitKey(1), hodlChan, nil,
|
getCircuitKey(1), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
@ -539,7 +556,7 @@ func TestCancelHoldInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Instantiate and start the invoice registry.
|
// Instantiate and start the invoice ctx.registry.
|
||||||
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
|
||||||
|
|
||||||
err = registry.Start()
|
err = registry.Start()
|
||||||
@ -561,7 +578,7 @@ func TestCancelHoldInvoice(t *testing.T) {
|
|||||||
// should be possible.
|
// should be possible.
|
||||||
event, err := registry.NotifyExitHopHtlc(
|
event, err := registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
@ -586,7 +603,7 @@ func TestCancelHoldInvoice(t *testing.T) {
|
|||||||
// accept height.
|
// accept height.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
event, err = registry.NotifyExitHopHtlc(
|
||||||
hash, amtPaid, testHtlcExpiry, testCurrentHeight+1,
|
hash, amtPaid, testHtlcExpiry, testCurrentHeight+1,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
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
|
// the exit hop, but in htlcIncomingContestResolver it is called with forwarded
|
||||||
// htlc hashes as well.
|
// htlc hashes as well.
|
||||||
func TestUnknownInvoice(t *testing.T) {
|
func TestUnknownInvoice(t *testing.T) {
|
||||||
registry, cleanup := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
defer cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
// Notify arrival of a new htlc paying to this invoice. This should
|
// Notify arrival of a new htlc paying to this invoice. This should
|
||||||
// succeed.
|
// succeed.
|
||||||
hodlChan := make(chan interface{})
|
hodlChan := make(chan interface{})
|
||||||
amt := lnwire.MilliSatoshi(100000)
|
amt := lnwire.MilliSatoshi(100000)
|
||||||
_, err := registry.NotifyExitHopHtlc(
|
_, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
hash, amt, testHtlcExpiry, testCurrentHeight,
|
hash, amt, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, nil,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != channeldb.ErrInvoiceNotFound {
|
if err != channeldb.ErrInvoiceNotFound {
|
||||||
t.Fatal("expected invoice not found error")
|
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
164
invoices/update.go
Normal 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
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package record
|
package record
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -96,3 +97,8 @@ func (r *MPP) Record() tlv.Record {
|
|||||||
MPPOnionType, r, size, MPPEncoder, MPPDecoder,
|
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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user