channeldb: stricter validation of invoice updates

This commit is contained in:
Joost Jager 2019-11-27 13:20:14 +01:00
parent a4a3c41924
commit 00d93ed87b
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
4 changed files with 221 additions and 94 deletions

View File

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

View File

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

View File

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

View File

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