mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-26 03:30:01 +02:00
channeldb: stricter validation of invoice updates
This commit is contained in:
parent
a4a3c41924
commit
00d93ed87b
@ -684,8 +684,10 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) InvoiceUpdateCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update := &InvoiceUpdateDesc{
|
update := &InvoiceUpdateDesc{
|
||||||
|
State: &InvoiceStateUpdateDesc{
|
||||||
Preimage: invoice.Terms.PaymentPreimage,
|
Preimage: invoice.Terms.PaymentPreimage,
|
||||||
State: ContractSettled,
|
NewState: ContractSettled,
|
||||||
|
},
|
||||||
AddHtlcs: 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,8 +325,9 @@ 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
|
||||||
|
|
||||||
// CancelHtlcs describes the htlcs that need to be canceled.
|
// CancelHtlcs describes the htlcs that need to be canceled.
|
||||||
CancelHtlcs map[CircuitKey]struct{}
|
CancelHtlcs map[CircuitKey]struct{}
|
||||||
@ -322,8 +335,14 @@ type InvoiceUpdateDesc struct {
|
|||||||
// AddHtlcs describes the newly accepted htlcs that need to be added to
|
// AddHtlcs describes the newly accepted htlcs that need to be added to
|
||||||
// the invoice.
|
// the invoice.
|
||||||
AddHtlcs map[CircuitKey]*HtlcAcceptDesc
|
AddHtlcs map[CircuitKey]*HtlcAcceptDesc
|
||||||
|
}
|
||||||
|
|
||||||
// Preimage must be set to the preimage when state is settled.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1231,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)
|
||||||
@ -1248,78 +1265,97 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
|
|||||||
return &invoice, nil
|
return &invoice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update invoice state.
|
|
||||||
invoice.State = update.State
|
|
||||||
|
|
||||||
now := d.Now()
|
now := d.Now()
|
||||||
|
|
||||||
// Process cancel actions from update descriptor.
|
// Update invoice state if the update descriptor indicates an invoice
|
||||||
for key := range update.CancelHtlcs {
|
// state change.
|
||||||
htlc, ok := invoice.Htlcs[key]
|
if update.State != nil {
|
||||||
if !ok {
|
err := updateInvoiceState(&invoice, hash, *update.State)
|
||||||
return nil, fmt.Errorf("unknown htlc %v", key)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
if htlc.State != HtlcStateAccepted {
|
|
||||||
return nil, fmt.Errorf("can only cancel " +
|
|
||||||
"accepted htlcs")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
htlc.State = HtlcStateCanceled
|
if update.State.NewState == ContractSettled {
|
||||||
htlc.ResolveTime = now
|
err := setSettleMetaFields(
|
||||||
invoice.AmtPaid -= htlc.Amt
|
settleIndex, invoiceNum, &invoice, now,
|
||||||
}
|
)
|
||||||
|
|
||||||
// Process add actions from update descriptor.
|
|
||||||
for key, htlcUpdate := range update.AddHtlcs {
|
|
||||||
htlc, ok := invoice.Htlcs[key]
|
|
||||||
|
|
||||||
// Add new htlc paying to the invoice.
|
|
||||||
if ok {
|
|
||||||
return nil, fmt.Errorf("htlc %v already exists", 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@ -1332,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
|
||||||
@ -1349,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
|
||||||
|
|
||||||
|
@ -464,7 +464,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
|
|
||||||
// Only send an update if the invoice state was changed.
|
// Only send an update if the invoice state was changed.
|
||||||
updateSubscribers = updateDesc != nil &&
|
updateSubscribers = updateDesc != nil &&
|
||||||
inv.State != updateDesc.State
|
updateDesc.State != nil
|
||||||
|
|
||||||
// Assign result to outer scope variable.
|
// Assign result to outer scope variable.
|
||||||
result = res
|
result = res
|
||||||
@ -541,8 +541,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,39 +591,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]struct{},
|
|
||||||
)
|
|
||||||
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] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move invoice to the canceled state.
|
|
||||||
return &channeldb.InvoiceUpdateDesc{
|
return &channeldb.InvoiceUpdateDesc{
|
||||||
CancelHtlcs: canceledHtlcs,
|
State: &channeldb.InvoiceStateUpdateDesc{
|
||||||
State: channeldb.ContractCanceled,
|
NewState: channeldb.ContractCanceled,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,11 +138,9 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// We do accept or settle the HTLC.
|
// We do accept or settle the HTLC.
|
||||||
switch inv.State {
|
switch inv.State {
|
||||||
case channeldb.ContractAccepted:
|
case channeldb.ContractAccepted:
|
||||||
update.State = channeldb.ContractAccepted
|
|
||||||
return &update, resultDuplicateToAccepted, nil
|
return &update, resultDuplicateToAccepted, nil
|
||||||
|
|
||||||
case channeldb.ContractSettled:
|
case channeldb.ContractSettled:
|
||||||
update.State = channeldb.ContractSettled
|
|
||||||
return &update, resultDuplicateToSettled, nil
|
return &update, resultDuplicateToSettled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,12 +148,16 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// to wait for the preimage.
|
// to wait for the preimage.
|
||||||
holdInvoice := inv.Terms.PaymentPreimage == channeldb.UnknownPreimage
|
holdInvoice := inv.Terms.PaymentPreimage == channeldb.UnknownPreimage
|
||||||
if holdInvoice {
|
if holdInvoice {
|
||||||
update.State = channeldb.ContractAccepted
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
|
NewState: channeldb.ContractAccepted,
|
||||||
|
}
|
||||||
return &update, resultAccepted, nil
|
return &update, resultAccepted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
update.Preimage = inv.Terms.PaymentPreimage
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
update.State = channeldb.ContractSettled
|
NewState: channeldb.ContractSettled,
|
||||||
|
Preimage: inv.Terms.PaymentPreimage,
|
||||||
|
}
|
||||||
|
|
||||||
return &update, resultSettled, nil
|
return &update, resultSettled, nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user