Merge pull request #3390 from joostjager/invoice-circuits

channeldb+cnct+invoices: track invoice htlcs
This commit is contained in:
Olaoluwa Osuntokun 2019-09-04 19:51:37 -07:00 committed by GitHub
commit 7eca7b02a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1993 additions and 1010 deletions

View file

@ -108,7 +108,7 @@ func WriteElement(w io.Writer, element interface{}) error {
return err
}
case uint64:
case int64, uint64:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
}
@ -280,7 +280,7 @@ func ReadElement(r io.Reader, element interface{}) error {
return err
}
case *uint64:
case *int64, *uint64:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
}

View file

@ -110,6 +110,11 @@ var (
number: 10,
migration: migrateRouteSerialization,
},
{
// Add invoice htlc and cltv delta fields.
number: 11,
migration: migrateInvoices,
},
}
// Big endian is the preferred byte order, due to cursor scans over
@ -124,6 +129,7 @@ type DB struct {
*bbolt.DB
dbPath string
graph *ChannelGraph
now func() time.Time
}
// Open opens an existing channeldb. Any necessary schemas migrations due to
@ -157,6 +163,7 @@ func Open(dbPath string, modifiers ...OptionModifier) (*DB, error) {
chanDB := &DB{
DB: bdb,
dbPath: dbPath,
now: time.Now,
}
chanDB.graph = newChannelGraph(
chanDB, opts.RejectCacheSize, opts.ChannelCacheSize,

View file

@ -24,6 +24,8 @@ func randInvoice(value lnwire.MilliSatoshi) (*Invoice, error) {
PaymentPreimage: pre,
Value: value,
},
Htlcs: map[CircuitKey]*InvoiceHTLC{},
Expiry: 4000,
}
i.Memo = []byte("memo")
i.Receipt = []byte("receipt")
@ -59,6 +61,7 @@ func TestInvoiceWorkflow(t *testing.T) {
// Use single second precision to avoid false positive test
// failures due to the monotonic time component.
CreationDate: time.Unix(time.Now().Unix(), 0),
Htlcs: map[CircuitKey]*InvoiceHTLC{},
}
fakeInvoice.Memo = []byte("memo")
fakeInvoice.Receipt = []byte("receipt")
@ -99,9 +102,7 @@ func TestInvoiceWorkflow(t *testing.T) {
// now have the settled bit toggle to true and a non-default
// SettledDate
payAmt := fakeInvoice.Terms.Value * 2
_, err = db.AcceptOrSettleInvoice(
paymentHash, payAmt, checkHtlcParameters,
)
_, err = db.UpdateInvoice(paymentHash, getUpdateInvoice(payAmt))
if err != nil {
t.Fatalf("unable to settle invoice: %v", err)
}
@ -264,8 +265,8 @@ func TestInvoiceAddTimeSeries(t *testing.T) {
paymentHash := invoice.Terms.PaymentPreimage.Hash()
_, err := db.AcceptOrSettleInvoice(
paymentHash, 0, checkHtlcParameters,
_, err := db.UpdateInvoice(
paymentHash, getUpdateInvoice(0),
)
if err != nil {
t.Fatalf("unable to settle invoice: %v", err)
@ -332,6 +333,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) }
// We'll start out by creating an invoice and writing it to the DB.
amt := lnwire.NewMSatFromSatoshis(1000)
@ -347,8 +349,8 @@ func TestDuplicateSettleInvoice(t *testing.T) {
}
// With the invoice in the DB, we'll now attempt to settle the invoice.
dbInvoice, err := db.AcceptOrSettleInvoice(
payHash, amt, checkHtlcParameters,
dbInvoice, err := db.UpdateInvoice(
payHash, getUpdateInvoice(amt),
)
if err != nil {
t.Fatalf("unable to settle invoice: %v", err)
@ -360,6 +362,14 @@ func TestDuplicateSettleInvoice(t *testing.T) {
invoice.Terms.State = ContractSettled
invoice.AmtPaid = amt
invoice.SettleDate = dbInvoice.SettleDate
invoice.Htlcs = map[CircuitKey]*InvoiceHTLC{
{}: {
Amt: amt,
AcceptTime: time.Unix(1, 0),
ResolveTime: time.Unix(1, 0),
State: HtlcStateSettled,
},
}
// We should get back the exact same invoice that we just inserted.
if !reflect.DeepEqual(dbInvoice, invoice) {
@ -369,8 +379,8 @@ func TestDuplicateSettleInvoice(t *testing.T) {
// If we try to settle the invoice again, then we should get the very
// same invoice back, but with an error this time.
dbInvoice, err = db.AcceptOrSettleInvoice(
payHash, amt, checkHtlcParameters,
dbInvoice, err = db.UpdateInvoice(
payHash, getUpdateInvoice(amt),
)
if err != ErrInvoiceAlreadySettled {
t.Fatalf("expected ErrInvoiceAlreadySettled")
@ -416,8 +426,8 @@ func TestQueryInvoices(t *testing.T) {
// We'll only settle half of all invoices created.
if i%2 == 0 {
_, err := db.AcceptOrSettleInvoice(
paymentHash, i, checkHtlcParameters,
_, err := db.UpdateInvoice(
paymentHash, getUpdateInvoice(i),
)
if err != nil {
t.Fatalf("unable to settle invoice: %v", err)
@ -661,10 +671,24 @@ func TestQueryInvoices(t *testing.T) {
}
}
func checkHtlcParameters(invoice *Invoice) error {
if invoice.Terms.State == ContractSettled {
return ErrInvoiceAlreadySettled
}
// getUpdateInvoice returns an invoice update callback that, when called,
// settles the invoice with the given amount.
func getUpdateInvoice(amt lnwire.MilliSatoshi) InvoiceUpdateCallback {
return func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
if invoice.Terms.State == ContractSettled {
return nil, ErrInvoiceAlreadySettled
}
return nil
update := &InvoiceUpdateDesc{
Preimage: invoice.Terms.PaymentPreimage,
State: ContractSettled,
Htlcs: map[CircuitKey]*HtlcAcceptDesc{
{}: {
Amt: amt,
},
},
}
return update, nil
}
}

View file

@ -12,6 +12,7 @@ import (
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
var (
@ -92,6 +93,17 @@ const (
// TODO(halseth): determine the max length payment request when field
// lengths are final.
MaxPaymentRequestSize = 4096
// A set of tlv type definitions used to serialize invoice htlcs to the
// database.
chanIDType tlv.Type = 1
htlcIDType tlv.Type = 3
amtType tlv.Type = 5
acceptHeightType tlv.Type = 7
acceptTimeType tlv.Type = 9
resolveTimeType tlv.Type = 11
expiryHeightType tlv.Type = 13
stateType tlv.Type = 15
)
// ContractState describes the state the invoice is in.
@ -172,6 +184,13 @@ type Invoice struct {
// for this invoice can be stored.
PaymentRequest []byte
// FinalCltvDelta is the minimum required number of blocks before htlc
// expiry when the invoice is accepted.
FinalCltvDelta int32
// Expiry defines how long after creation this invoice should expire.
Expiry time.Duration
// CreationDate is the exact time the invoice was created.
CreationDate time.Time
@ -209,8 +228,85 @@ type Invoice struct {
// that the invoice originally didn't specify an amount, or the sender
// overpaid.
AmtPaid lnwire.MilliSatoshi
// Htlcs records all htlcs that paid to this invoice. Some of these
// htlcs may have been marked as cancelled.
Htlcs map[CircuitKey]*InvoiceHTLC
}
// HtlcState defines the states an htlc paying to an invoice can be in.
type HtlcState uint8
const (
// HtlcStateAccepted indicates the htlc is locked-in, but not resolved.
HtlcStateAccepted HtlcState = iota
// HtlcStateCancelled indicates the htlc is cancelled back to the
// sender.
HtlcStateCancelled
// HtlcStateSettled indicates the htlc is settled.
HtlcStateSettled
)
// InvoiceHTLC contains details about an htlc paying to this invoice.
type InvoiceHTLC struct {
// Amt is the amount that is carried by this htlc.
Amt lnwire.MilliSatoshi
// AcceptHeight is the block height at which the invoice registry
// decided to accept this htlc as a payment to the invoice. At this
// height, the invoice cltv delay must have been met.
AcceptHeight uint32
// AcceptTime is the wall clock time at which the invoice registry
// decided to accept the htlc.
AcceptTime time.Time
// ResolveTime is the wall clock time at which the invoice registry
// decided to settle the htlc.
ResolveTime time.Time
// Expiry is the expiry height of this htlc.
Expiry uint32
// State indicates the state the invoice htlc is currently in. A
// cancelled htlc isn't just removed from the invoice htlcs map, because
// we need AcceptedHeight to properly cancel the htlc back.
State HtlcState
}
// HtlcAcceptDesc describes the details of a newly accepted htlc.
type HtlcAcceptDesc struct {
// AcceptHeight is the block height at which this htlc was accepted.
AcceptHeight int32
// Amt is the amount that is carried by this htlc.
Amt lnwire.MilliSatoshi
// Expiry is the expiry height of this htlc.
Expiry uint32
}
// 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
// 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 cancelled.
Htlcs map[CircuitKey]*HtlcAcceptDesc
// Preimage must be set to the preimage when state is settled.
Preimage lntypes.Preimage
}
// InvoiceUpdateCallback is a callback used in the db transaction to update the
// invoice.
type InvoiceUpdateCallback = func(invoice *Invoice) (*InvoiceUpdateDesc, error)
func validateInvoice(i *Invoice) error {
if len(i.Memo) > MaxMemoSize {
return fmt.Errorf("max length a memo is %v, and invoice "+
@ -624,21 +720,17 @@ func (d *DB) QueryInvoices(q InvoiceQuery) (InvoiceSlice, error) {
return resp, nil
}
// AcceptOrSettleInvoice attempts to mark an invoice corresponding to the passed
// payment hash as settled. If an invoice matching the passed payment hash
// doesn't existing within the database, then the action will fail with a "not
// found" error.
// UpdateInvoice attempts to update an invoice corresponding to the passed
// payment hash. If an invoice matching the passed payment hash doesn't exist
// within the database, then the action will fail with a "not found" error.
//
// When the preimage for the invoice is unknown (hold invoice), the invoice is
// marked as accepted.
//
// TODO: Store invoice cltv as separate field in database so that it doesn't
// need to be decoded from the payment request.
func (d *DB) AcceptOrSettleInvoice(paymentHash [32]byte,
amtPaid lnwire.MilliSatoshi,
checkHtlcParameters func(invoice *Invoice) error) (*Invoice, error) {
// The update is performed inside the same database transaction that fetches the
// invoice and is therefore atomic. The fields to update are controlled by the
// supplied callback.
func (d *DB) UpdateInvoice(paymentHash lntypes.Hash,
callback InvoiceUpdateCallback) (*Invoice, error) {
var settledInvoice *Invoice
var updatedInvoice *Invoice
err := d.Update(func(tx *bbolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
@ -664,49 +756,9 @@ func (d *DB) AcceptOrSettleInvoice(paymentHash [32]byte,
return ErrInvoiceNotFound
}
settledInvoice, err = acceptOrSettleInvoice(
invoices, settleIndex, invoiceNum, amtPaid,
checkHtlcParameters,
)
return err
})
return settledInvoice, err
}
// SettleHoldInvoice sets the preimage of a hodl invoice and marks the invoice
// as settled.
func (d *DB) SettleHoldInvoice(preimage lntypes.Preimage) (*Invoice, error) {
var updatedInvoice *Invoice
hash := preimage.Hash()
err := d.Update(func(tx *bbolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
return err
}
invoiceIndex, err := invoices.CreateBucketIfNotExists(
invoiceIndexBucket,
)
if err != nil {
return err
}
settleIndex, err := invoices.CreateBucketIfNotExists(
settleIndexBucket,
)
if err != nil {
return err
}
// Check the invoice index to see if an invoice paying to this
// hash exists within the DB.
invoiceNum := invoiceIndex.Get(hash[:])
if invoiceNum == nil {
return ErrInvoiceNotFound
}
updatedInvoice, err = settleHoldInvoice(
invoices, settleIndex, invoiceNum, preimage,
updatedInvoice, err = d.updateInvoice(
paymentHash, invoices, settleIndex, invoiceNum,
callback,
)
return err
@ -715,37 +767,6 @@ func (d *DB) SettleHoldInvoice(preimage lntypes.Preimage) (*Invoice, error) {
return updatedInvoice, err
}
// CancelInvoice attempts to cancel the invoice corresponding to the passed
// payment hash.
func (d *DB) CancelInvoice(paymentHash lntypes.Hash) (*Invoice, error) {
var canceledInvoice *Invoice
err := d.Update(func(tx *bbolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
return err
}
invoiceIndex, err := invoices.CreateBucketIfNotExists(
invoiceIndexBucket,
)
if err != nil {
return err
}
// Check the invoice index to see if an invoice paying to this
// hash exists within the DB.
invoiceNum := invoiceIndex.Get(paymentHash[:])
if invoiceNum == nil {
return ErrInvoiceNotFound
}
canceledInvoice, err = cancelInvoice(invoices, invoiceNum)
return err
})
return canceledInvoice, err
}
// InvoicesSettledSince can be used by callers to catch up any settled invoices
// they missed within the settled invoice time series. We'll return all known
// settled invoice that have a settle index higher than the passed
@ -855,7 +876,7 @@ func putInvoice(invoices, invoiceIndex, addIndex *bbolt.Bucket,
// Finally, serialize the invoice itself to be written to the disk.
var buf bytes.Buffer
if err := serializeInvoice(&buf, i); err != nil {
return 0, nil
return 0, err
}
if err := invoices.Put(invoiceKey[:], buf.Bytes()); err != nil {
@ -865,6 +886,11 @@ func putInvoice(invoices, invoiceIndex, addIndex *bbolt.Bucket,
return nextAddSeqNo, nil
}
// serializeInvoice serializes an invoice to a writer.
//
// Note: this function is in use for a migration. Before making changes that
// would modify the on disk format, make a copy of the original code and store
// it with the migration.
func serializeInvoice(w io.Writer, i *Invoice) error {
if err := wire.WriteVarBytes(w, 0, i.Memo[:]); err != nil {
return err
@ -876,6 +902,14 @@ func serializeInvoice(w io.Writer, i *Invoice) error {
return err
}
if err := binary.Write(w, byteOrder, i.FinalCltvDelta); err != nil {
return err
}
if err := binary.Write(w, byteOrder, int64(i.Expiry)); err != nil {
return err
}
birthBytes, err := i.CreationDate.MarshalBinary()
if err != nil {
return err
@ -918,6 +952,57 @@ func serializeInvoice(w io.Writer, i *Invoice) error {
return err
}
if err := serializeHtlcs(w, i.Htlcs); err != nil {
return err
}
return nil
}
// serializeHtlcs serializes a map containing circuit keys and invoice htlcs to
// a writer.
func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
for key, htlc := range htlcs {
// Encode the htlc in a tlv stream.
chanID := key.ChanID.ToUint64()
amt := uint64(htlc.Amt)
acceptTime := uint64(htlc.AcceptTime.UnixNano())
resolveTime := uint64(htlc.ResolveTime.UnixNano())
state := uint8(htlc.State)
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(chanIDType, &chanID),
tlv.MakePrimitiveRecord(htlcIDType, &key.HtlcID),
tlv.MakePrimitiveRecord(amtType, &amt),
tlv.MakePrimitiveRecord(
acceptHeightType, &htlc.AcceptHeight,
),
tlv.MakePrimitiveRecord(acceptTimeType, &acceptTime),
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
tlv.MakePrimitiveRecord(stateType, &state),
)
if err != nil {
return err
}
var b bytes.Buffer
if err := tlvStream.Encode(&b); err != nil {
return err
}
// Write the length of the tlv stream followed by the stream
// bytes.
err = binary.Write(w, byteOrder, uint64(b.Len()))
if err != nil {
return err
}
if _, err := w.Write(b.Bytes()); err != nil {
return err
}
}
return nil
}
@ -951,6 +1036,16 @@ func deserializeInvoice(r io.Reader) (Invoice, error) {
return invoice, err
}
if err := binary.Read(r, byteOrder, &invoice.FinalCltvDelta); err != nil {
return invoice, err
}
var expiry int64
if err := binary.Read(r, byteOrder, &expiry); err != nil {
return invoice, err
}
invoice.Expiry = time.Duration(expiry)
birthBytes, err := wire.ReadVarBytes(r, 0, 300, "birth")
if err != nil {
return invoice, err
@ -990,38 +1085,204 @@ func deserializeInvoice(r io.Reader) (Invoice, error) {
return invoice, err
}
invoice.Htlcs, err = deserializeHtlcs(r)
if err != nil {
return Invoice{}, err
}
return invoice, nil
}
func acceptOrSettleInvoice(invoices, settleIndex *bbolt.Bucket,
invoiceNum []byte, amtPaid lnwire.MilliSatoshi,
checkHtlcParameters func(invoice *Invoice) error) (
*Invoice, error) {
// deserializeHtlcs reads a list of invoice htlcs from a reader and returns it
// as a map.
func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
htlcs := make(map[CircuitKey]*InvoiceHTLC, 0)
for {
// Read the length of the tlv stream for this htlc.
var streamLen uint64
if err := binary.Read(r, byteOrder, &streamLen); err != nil {
if err == io.EOF {
break
}
return nil, err
}
streamBytes := make([]byte, streamLen)
if _, err := r.Read(streamBytes); err != nil {
return nil, err
}
streamReader := bytes.NewReader(streamBytes)
// Decode the contents into the htlc fields.
var (
htlc InvoiceHTLC
key CircuitKey
chanID uint64
state uint8
acceptTime, resolveTime uint64
amt uint64
)
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(chanIDType, &chanID),
tlv.MakePrimitiveRecord(htlcIDType, &key.HtlcID),
tlv.MakePrimitiveRecord(amtType, &amt),
tlv.MakePrimitiveRecord(
acceptHeightType, &htlc.AcceptHeight,
),
tlv.MakePrimitiveRecord(acceptTimeType, &acceptTime),
tlv.MakePrimitiveRecord(resolveTimeType, &resolveTime),
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
tlv.MakePrimitiveRecord(stateType, &state),
)
if err != nil {
return nil, err
}
if err := tlvStream.Decode(streamReader); err != nil {
return nil, err
}
key.ChanID = lnwire.NewShortChanIDFromInt(chanID)
htlc.AcceptTime = time.Unix(0, int64(acceptTime))
htlc.ResolveTime = time.Unix(0, int64(resolveTime))
htlc.State = HtlcState(state)
htlc.Amt = lnwire.MilliSatoshi(amt)
htlcs[key] = &htlc
}
return htlcs, nil
}
// copySlice allocates a new slice and copies the source into it.
func copySlice(src []byte) []byte {
dest := make([]byte, len(src))
copy(dest, src)
return dest
}
// copyInvoice makes a deep copy of the supplied invoice.
func copyInvoice(src *Invoice) *Invoice {
dest := Invoice{
Memo: copySlice(src.Memo),
Receipt: copySlice(src.Receipt),
PaymentRequest: copySlice(src.PaymentRequest),
FinalCltvDelta: src.FinalCltvDelta,
CreationDate: src.CreationDate,
SettleDate: src.SettleDate,
Terms: src.Terms,
AddIndex: src.AddIndex,
SettleIndex: src.SettleIndex,
AmtPaid: src.AmtPaid,
Htlcs: make(
map[CircuitKey]*InvoiceHTLC, len(src.Htlcs),
),
}
for k, v := range src.Htlcs {
dest.Htlcs[k] = v
}
return &dest
}
// updateInvoice fetches the invoice, obtains the update descriptor from the
// callback and applies the updates in a single db transaction.
func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucket,
invoiceNum []byte, callback InvoiceUpdateCallback) (*Invoice, error) {
invoice, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return nil, err
}
// If the invoice is still open, check the htlc parameters.
if err := checkHtlcParameters(&invoice); err != nil {
preUpdateState := invoice.Terms.State
// Create deep copy to prevent any accidental modification in the
// callback.
copy := copyInvoice(&invoice)
// Call the callback and obtain the update descriptor.
update, err := callback(copy)
if err != nil {
return &invoice, err
}
// Check to see if we can settle or this is an hold invoice and we need
// to wait for the preimage.
holdInvoice := invoice.Terms.PaymentPreimage == UnknownPreimage
if holdInvoice {
invoice.Terms.State = ContractAccepted
} else {
err := setSettleFields(settleIndex, invoiceNum, &invoice)
// Update invoice state.
invoice.Terms.State = update.State
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 cancelled.
if htlcUpdate == nil {
if !ok {
return nil, fmt.Errorf("unknown htlc %v", key)
}
if htlc.State == HtlcStateSettled {
return nil, fmt.Errorf("cannot cancel a " +
"settled htlc")
}
htlc.State = HtlcStateCancelled
htlc.ResolveTime = now
invoice.AmtPaid -= htlc.Amt
continue
}
// 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.Terms.State &&
invoice.Terms.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 {
return nil, err
}
}
invoice.AmtPaid = amtPaid
var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err
@ -1035,7 +1296,7 @@ func acceptOrSettleInvoice(invoices, settleIndex *bbolt.Bucket,
}
func setSettleFields(settleIndex *bbolt.Bucket, invoiceNum []byte,
invoice *Invoice) error {
invoice *Invoice, now time.Time) error {
// Now that we know the invoice hasn't already been settled, we'll
// update the settle index so we can place this settle event in the
@ -1052,77 +1313,8 @@ func setSettleFields(settleIndex *bbolt.Bucket, invoiceNum []byte,
}
invoice.Terms.State = ContractSettled
invoice.SettleDate = time.Now()
invoice.SettleDate = now
invoice.SettleIndex = nextSettleSeqNo
return nil
}
func settleHoldInvoice(invoices, settleIndex *bbolt.Bucket,
invoiceNum []byte, preimage lntypes.Preimage) (*Invoice,
error) {
invoice, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return nil, err
}
switch invoice.Terms.State {
case ContractOpen:
return &invoice, ErrInvoiceStillOpen
case ContractCanceled:
return &invoice, ErrInvoiceAlreadyCanceled
case ContractSettled:
return &invoice, ErrInvoiceAlreadySettled
}
invoice.Terms.PaymentPreimage = preimage
err = setSettleFields(settleIndex, invoiceNum, &invoice)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err
}
if err := invoices.Put(invoiceNum[:], buf.Bytes()); err != nil {
return nil, err
}
return &invoice, nil
}
func cancelInvoice(invoices *bbolt.Bucket, invoiceNum []byte) (
*Invoice, error) {
invoice, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return nil, err
}
switch invoice.Terms.State {
case ContractSettled:
return &invoice, ErrInvoiceAlreadySettled
case ContractCanceled:
return &invoice, ErrInvoiceAlreadyCanceled
}
invoice.Terms.State = ContractCanceled
// Set AmtPaid back to 0, in case the invoice was already accepted.
invoice.AmtPaid = 0
var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil {
return nil, err
}
if err := invoices.Put(invoiceNum[:], buf.Bytes()); err != nil {
return nil, err
}
return &invoice, nil
}

View file

@ -177,7 +177,7 @@ func fetchPaymentStatusTx(tx *bbolt.Tx, paymentHash [32]byte) (PaymentStatus, er
func serializeOutgoingPayment(w io.Writer, p *outgoingPayment) error {
var scratch [8]byte
if err := serializeInvoice(w, &p.Invoice); err != nil {
if err := serializeInvoiceLegacy(w, &p.Invoice); err != nil {
return err
}
@ -218,7 +218,7 @@ func deserializeOutgoingPayment(r io.Reader) (*outgoingPayment, error) {
p := &outgoingPayment{}
inv, err := deserializeInvoice(r)
inv, err := deserializeInvoiceLegacy(r)
if err != nil {
return nil, err
}

View file

@ -0,0 +1,225 @@
package channeldb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
bitcoinCfg "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
litecoinCfg "github.com/ltcsuite/ltcd/chaincfg"
)
// migrateInvoices adds invoice htlcs and a separate cltv delta field to the
// invoices.
func migrateInvoices(tx *bbolt.Tx) error {
log.Infof("Migrating invoices to new invoice format")
invoiceB := tx.Bucket(invoiceBucket)
if invoiceB == nil {
return nil
}
// Iterate through the entire key space of the top-level invoice bucket.
// If key with a non-nil value stores the next invoice ID which maps to
// the corresponding invoice. Store those keys first, because it isn't
// safe to modify the bucket inside a ForEach loop.
var invoiceKeys [][]byte
err := invoiceB.ForEach(func(k, v []byte) error {
if v == nil {
return nil
}
invoiceKeys = append(invoiceKeys, k)
return nil
})
if err != nil {
return err
}
nets := []*bitcoinCfg.Params{
&bitcoinCfg.MainNetParams, &bitcoinCfg.SimNetParams,
&bitcoinCfg.RegressionNetParams, &bitcoinCfg.TestNet3Params,
}
ltcNets := []*litecoinCfg.Params{
&litecoinCfg.MainNetParams, &litecoinCfg.SimNetParams,
&litecoinCfg.RegressionNetParams, &litecoinCfg.TestNet4Params,
}
for _, net := range ltcNets {
var convertedNet bitcoinCfg.Params
convertedNet.Bech32HRPSegwit = net.Bech32HRPSegwit
nets = append(nets, &convertedNet)
}
// Iterate over all stored keys and migrate the invoices.
for _, k := range invoiceKeys {
v := invoiceB.Get(k)
// Deserialize the invoice with the deserializing function that
// was in use for this version of the database.
invoiceReader := bytes.NewReader(v)
invoice, err := deserializeInvoiceLegacy(invoiceReader)
if err != nil {
return err
}
// Try to decode the payment request for every possible net to
// avoid passing a the active network to channeldb. This would
// be a layering violation, while this migration is only running
// once and will likely be removed in the future.
var payReq *zpay32.Invoice
for _, net := range nets {
payReq, err = zpay32.Decode(
string(invoice.PaymentRequest), net,
)
if err == nil {
break
}
}
if payReq == nil {
return fmt.Errorf("cannot decode payreq")
}
invoice.FinalCltvDelta = int32(payReq.MinFinalCLTVExpiry())
invoice.Expiry = payReq.Expiry()
// Serialize the invoice in the new format and use it to replace
// the old invoice in the database.
var buf bytes.Buffer
if err := serializeInvoice(&buf, &invoice); err != nil {
return err
}
err = invoiceB.Put(k, buf.Bytes())
if err != nil {
return err
}
}
log.Infof("Migration of invoices completed!")
return nil
}
func deserializeInvoiceLegacy(r io.Reader) (Invoice, error) {
var err error
invoice := Invoice{}
// TODO(roasbeef): use read full everywhere
invoice.Memo, err = wire.ReadVarBytes(r, 0, MaxMemoSize, "")
if err != nil {
return invoice, err
}
invoice.Receipt, err = wire.ReadVarBytes(r, 0, MaxReceiptSize, "")
if err != nil {
return invoice, err
}
invoice.PaymentRequest, err = wire.ReadVarBytes(r, 0, MaxPaymentRequestSize, "")
if err != nil {
return invoice, err
}
birthBytes, err := wire.ReadVarBytes(r, 0, 300, "birth")
if err != nil {
return invoice, err
}
if err := invoice.CreationDate.UnmarshalBinary(birthBytes); err != nil {
return invoice, err
}
settledBytes, err := wire.ReadVarBytes(r, 0, 300, "settled")
if err != nil {
return invoice, err
}
if err := invoice.SettleDate.UnmarshalBinary(settledBytes); err != nil {
return invoice, err
}
if _, err := io.ReadFull(r, invoice.Terms.PaymentPreimage[:]); err != nil {
return invoice, err
}
var scratch [8]byte
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return invoice, err
}
invoice.Terms.Value = lnwire.MilliSatoshi(byteOrder.Uint64(scratch[:]))
if err := binary.Read(r, byteOrder, &invoice.Terms.State); err != nil {
return invoice, err
}
if err := binary.Read(r, byteOrder, &invoice.AddIndex); err != nil {
return invoice, err
}
if err := binary.Read(r, byteOrder, &invoice.SettleIndex); err != nil {
return invoice, err
}
if err := binary.Read(r, byteOrder, &invoice.AmtPaid); err != nil {
return invoice, err
}
return invoice, nil
}
// serializeInvoiceLegacy serializes an invoice in the format of the previous db
// version.
func serializeInvoiceLegacy(w io.Writer, i *Invoice) error {
if err := wire.WriteVarBytes(w, 0, i.Memo[:]); err != nil {
return err
}
if err := wire.WriteVarBytes(w, 0, i.Receipt[:]); err != nil {
return err
}
if err := wire.WriteVarBytes(w, 0, i.PaymentRequest[:]); err != nil {
return err
}
birthBytes, err := i.CreationDate.MarshalBinary()
if err != nil {
return err
}
if err := wire.WriteVarBytes(w, 0, birthBytes); err != nil {
return err
}
settleBytes, err := i.SettleDate.MarshalBinary()
if err != nil {
return err
}
if err := wire.WriteVarBytes(w, 0, settleBytes); err != nil {
return err
}
if _, err := w.Write(i.Terms.PaymentPreimage[:]); err != nil {
return err
}
var scratch [8]byte
byteOrder.PutUint64(scratch[:], uint64(i.Terms.Value))
if _, err := w.Write(scratch[:]); err != nil {
return err
}
if err := binary.Write(w, byteOrder, i.Terms.State); err != nil {
return err
}
if err := binary.Write(w, byteOrder, i.AddIndex); err != nil {
return err
}
if err := binary.Write(w, byteOrder, i.SettleIndex); err != nil {
return err
}
if err := binary.Write(w, byteOrder, int64(i.AmtPaid)); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,166 @@
package channeldb
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
bitcoinCfg "github.com/btcsuite/btcd/chaincfg"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/zpay32"
litecoinCfg "github.com/ltcsuite/ltcd/chaincfg"
)
var (
testPrivKeyBytes = []byte{
0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf,
0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9,
0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f,
0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90,
}
testCltvDelta = int32(50)
)
// TestMigrateInvoices checks that invoices are migrated correctly.
func TestMigrateInvoices(t *testing.T) {
t.Parallel()
payReqBtc, err := getPayReq(&bitcoinCfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
var ltcNetParams bitcoinCfg.Params
ltcNetParams.Bech32HRPSegwit = litecoinCfg.MainNetParams.Bech32HRPSegwit
payReqLtc, err := getPayReq(&ltcNetParams)
if err != nil {
t.Fatal(err)
}
invoices := []Invoice{
{
PaymentRequest: []byte(payReqBtc),
},
{
PaymentRequest: []byte(payReqLtc),
},
}
beforeMigrationFunc := func(d *DB) {
err := d.Update(func(tx *bbolt.Tx) error {
invoicesBucket, err := tx.CreateBucketIfNotExists(
invoiceBucket,
)
if err != nil {
return err
}
invoiceNum := uint32(1)
for _, invoice := range invoices {
var invoiceKey [4]byte
byteOrder.PutUint32(invoiceKey[:], invoiceNum)
invoiceNum++
var buf bytes.Buffer
err := serializeInvoiceLegacy(&buf, &invoice)
if err != nil {
return err
}
err = invoicesBucket.Put(
invoiceKey[:], buf.Bytes(),
)
if err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
// Verify that all invoices were migrated.
afterMigrationFunc := func(d *DB) {
meta, err := d.FetchMeta(nil)
if err != nil {
t.Fatal(err)
}
if meta.DbVersionNumber != 1 {
t.Fatal("migration 'invoices' wasn't applied")
}
dbInvoices, err := d.FetchAllInvoices(false)
if err != nil {
t.Fatalf("unable to fetch invoices: %v", err)
}
if len(invoices) != len(dbInvoices) {
t.Fatalf("expected %d invoices, got %d", len(invoices),
len(dbInvoices))
}
for _, dbInvoice := range dbInvoices {
if dbInvoice.FinalCltvDelta != testCltvDelta {
t.Fatal("incorrect final cltv delta")
}
if dbInvoice.Expiry != 3600*time.Second {
t.Fatal("incorrect expiry")
}
if len(dbInvoice.Htlcs) != 0 {
t.Fatal("expected no htlcs after migration")
}
}
}
applyMigration(t,
beforeMigrationFunc,
afterMigrationFunc,
migrateInvoices,
false)
}
// signDigestCompact generates a test signature to be used in the generation of
// test payment requests.
func signDigestCompact(hash []byte) ([]byte, error) {
// Should the signature reference a compressed public key or not.
isCompressedKey := true
privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), testPrivKeyBytes)
// btcec.SignCompact returns a pubkey-recoverable signature
sig, err := btcec.SignCompact(
btcec.S256(), privKey, hash, isCompressedKey,
)
if err != nil {
return nil, fmt.Errorf("can't sign the hash: %v", err)
}
return sig, nil
}
// getPayReq creates a payment request for the given net.
func getPayReq(net *bitcoinCfg.Params) (string, error) {
options := []func(*zpay32.Invoice){
zpay32.CLTVExpiry(uint64(testCltvDelta)),
zpay32.Description("test"),
}
payReq, err := zpay32.NewInvoice(
net, [32]byte{}, time.Unix(1, 0), options...,
)
if err != nil {
return "", err
}
return payReq.Encode(
zpay32.MessageSigner{
SignCompact: signDigestCompact,
},
)
}

View file

@ -168,7 +168,7 @@ func migrateInvoiceTimeSeries(tx *bbolt.Tx) error {
invoiceBytesCopy = append(invoiceBytesCopy, padding...)
invoiceReader := bytes.NewReader(invoiceBytesCopy)
invoice, err := deserializeInvoice(invoiceReader)
invoice, err := deserializeInvoiceLegacy(invoiceReader)
if err != nil {
return fmt.Errorf("unable to decode invoice: %v", err)
}
@ -227,7 +227,7 @@ func migrateInvoiceTimeSeries(tx *bbolt.Tx) error {
// We've fully migrated an invoice, so we'll now update the
// invoice in-place.
var b bytes.Buffer
if err := serializeInvoice(&b, &invoice); err != nil {
if err := serializeInvoiceLegacy(&b, &invoice); err != nil {
return err
}

View file

@ -479,7 +479,7 @@ func (c *ChannelArbitrator) relaunchResolvers() error {
"resolvers", c.cfg.ChanPoint, len(unresolvedContracts))
for _, resolver := range unresolvedContracts {
supplementResolver(resolver, htlcMap)
c.supplementResolver(resolver, htlcMap)
}
c.launchResolvers(unresolvedContracts)
@ -489,24 +489,22 @@ func (c *ChannelArbitrator) relaunchResolvers() error {
// supplementResolver takes a resolver as it is restored from the log and fills
// in missing data from the htlcMap.
func supplementResolver(resolver ContractResolver,
func (c *ChannelArbitrator) supplementResolver(resolver ContractResolver,
htlcMap map[wire.OutPoint]*channeldb.HTLC) error {
switch r := resolver.(type) {
case *htlcSuccessResolver:
return supplementSuccessResolver(r, htlcMap)
return c.supplementSuccessResolver(r, htlcMap)
case *htlcIncomingContestResolver:
return supplementSuccessResolver(
&r.htlcSuccessResolver, htlcMap,
)
return c.supplementIncomingContestResolver(r, htlcMap)
case *htlcTimeoutResolver:
return supplementTimeoutResolver(r, htlcMap)
return c.supplementTimeoutResolver(r, htlcMap)
case *htlcOutgoingContestResolver:
return supplementTimeoutResolver(
return c.supplementTimeoutResolver(
&r.htlcTimeoutResolver, htlcMap,
)
}
@ -514,9 +512,33 @@ func supplementResolver(resolver ContractResolver,
return nil
}
// supplementSuccessResolver takes a htlcIncomingContestResolver as it is
// restored from the log and fills in missing data from the htlcMap.
func (c *ChannelArbitrator) supplementIncomingContestResolver(
r *htlcIncomingContestResolver,
htlcMap map[wire.OutPoint]*channeldb.HTLC) error {
res := r.htlcResolution
htlcPoint := res.HtlcPoint()
htlc, ok := htlcMap[htlcPoint]
if !ok {
return errors.New(
"htlc for incoming contest resolver unavailable",
)
}
r.htlcAmt = htlc.Amt
r.circuitKey = channeldb.CircuitKey{
ChanID: c.cfg.ShortChanID,
HtlcID: htlc.HtlcIndex,
}
return nil
}
// supplementSuccessResolver takes a htlcSuccessResolver as it is restored from
// the log and fills in missing data from the htlcMap.
func supplementSuccessResolver(r *htlcSuccessResolver,
func (c *ChannelArbitrator) supplementSuccessResolver(r *htlcSuccessResolver,
htlcMap map[wire.OutPoint]*channeldb.HTLC) error {
res := r.htlcResolution
@ -533,7 +555,7 @@ func supplementSuccessResolver(r *htlcSuccessResolver,
// supplementTimeoutResolver takes a htlcSuccessResolver as it is restored from
// the log and fills in missing data from the htlcMap.
func supplementTimeoutResolver(r *htlcTimeoutResolver,
func (c *ChannelArbitrator) supplementTimeoutResolver(r *htlcTimeoutResolver,
htlcMap map[wire.OutPoint]*channeldb.HTLC) error {
res := r.htlcResolution
@ -1326,7 +1348,7 @@ func (c *ChannelArbitrator) isPreimageAvailable(hash lntypes.Hash) (bool,
// than the invoice cltv delta. We don't want to go to chain only to
// have the incoming contest resolver decide that we don't want to
// settle this invoice.
invoice, _, err := c.cfg.Registry.LookupInvoice(hash)
invoice, err := c.cfg.Registry.LookupInvoice(hash)
switch err {
case nil:
case channeldb.ErrInvoiceNotFound, channeldb.ErrNoInvoicesCreated:
@ -1723,9 +1745,15 @@ func (c *ChannelArbitrator) prepContractResolutions(
continue
}
circuitKey := channeldb.CircuitKey{
HtlcID: htlc.HtlcIndex,
ChanID: c.cfg.ShortChanID,
}
resKit.Quit = make(chan struct{})
resolver := &htlcIncomingContestResolver{
htlcExpiry: htlc.RefundTimeout,
circuitKey: circuitKey,
htlcSuccessResolver: htlcSuccessResolver{
htlcResolution: resolution,
broadcastHeight: height,

View file

@ -27,6 +27,9 @@ type htlcIncomingContestResolver struct {
// successfully.
htlcExpiry uint32
// circuitKey describes the incoming htlc that is being resolved.
circuitKey channeldb.CircuitKey
// htlcSuccessResolver is the inner resolver that may be utilized if we
// learn of the preimage.
htlcSuccessResolver
@ -166,7 +169,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
// identical to HTLC resolution in the link.
event, err := h.Registry.NotifyExitHopHtlc(
h.payHash, h.htlcAmt, h.htlcExpiry, currentHeight,
hodlChan, nil,
h.circuitKey, hodlChan, nil,
)
switch err {
case channeldb.ErrInvoiceNotFound:

View file

@ -10,10 +10,8 @@ import (
// Registry is an interface which represents the invoice registry.
type Registry interface {
// LookupInvoice attempts to look up an invoice according to its 32
// byte payment hash. This method should also reutrn the min final CLTV
// delta for this invoice. We'll use this to ensure that the HTLC
// extended to us gives us enough time to settle as we prescribe.
LookupInvoice(lntypes.Hash) (channeldb.Invoice, uint32, error)
// byte payment hash.
LookupInvoice(lntypes.Hash) (channeldb.Invoice, error)
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the
// invoice is a debug invoice, then this method is a noop as debug
@ -22,7 +20,7 @@ type Registry interface {
// the resolution is sent on the passed in hodlChan later.
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32,
hodlChan chan<- interface{},
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error)
// HodlUnsubscribeAll unsubscribes from all hodl events.

View file

@ -23,7 +23,8 @@ type mockRegistry struct {
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, eob []byte) (*invoices.HodlEvent, error) {
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) {
r.notifyChan <- notifyExitHopData{
hodlChan: hodlChan,
@ -38,8 +39,8 @@ func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
func (r *mockRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {}
func (r *mockRegistry) LookupInvoice(lntypes.Hash) (channeldb.Invoice, uint32,
func (r *mockRegistry) LookupInvoice(lntypes.Hash) (channeldb.Invoice,
error) {
return channeldb.Invoice{}, 0, channeldb.ErrInvoiceNotFound
return channeldb.Invoice{}, channeldb.ErrInvoiceNotFound
}

View file

@ -14,10 +14,8 @@ import (
// which may search, lookup and settle invoices.
type InvoiceDatabase interface {
// LookupInvoice attempts to look up an invoice according to its 32
// byte payment hash. This method should also reutrn the min final CLTV
// delta for this invoice. We'll use this to ensure that the HTLC
// extended to us gives us enough time to settle as we prescribe.
LookupInvoice(lntypes.Hash) (channeldb.Invoice, uint32, error)
// byte payment hash.
LookupInvoice(lntypes.Hash) (channeldb.Invoice, error)
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the
// invoice is a debug invoice, then this method is a noop as debug
@ -28,7 +26,7 @@ type InvoiceDatabase interface {
// for decoding purposes.
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32,
hodlChan chan<- interface{},
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error)
// CancelInvoice attempts to cancel the invoice corresponding to the

View file

@ -2878,9 +2878,14 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
// receive back a resolution event.
invoiceHash := lntypes.Hash(pd.RHash)
circuitKey := channeldb.CircuitKey{
ChanID: l.ShortChanID(),
HtlcID: pd.HtlcIndex,
}
event, err := l.cfg.Registry.NotifyExitHopHtlc(
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
l.hodlQueue.ChanIn(), eob,
circuitKey, l.hodlQueue.ChanIn(), eob,
)
switch err {

View file

@ -238,7 +238,7 @@ func TestChannelLinkSingleHopPayment(t *testing.T) {
// Check that alice invoice was settled and bandwidth of HTLC
// links was changed.
invoice, _, err := receiver.registry.LookupInvoice(rhash)
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
@ -498,7 +498,7 @@ func testChannelLinkMultiHopPayment(t *testing.T,
// Check that Carol invoice was settled and bandwidth of HTLC
// links were changed.
invoice, _, err := receiver.registry.LookupInvoice(rhash)
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
@ -912,7 +912,7 @@ func TestUpdateForwardingPolicy(t *testing.T) {
// Carol's invoice should now be shown as settled as the payment
// succeeded.
invoice, _, err := n.carolServer.registry.LookupInvoice(payResp)
invoice, err := n.carolServer.registry.LookupInvoice(payResp)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
@ -1030,7 +1030,7 @@ func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) {
// Check that alice invoice wasn't settled and bandwidth of htlc
// links hasn't been changed.
invoice, _, err := receiver.registry.LookupInvoice(rhash)
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
@ -1215,7 +1215,7 @@ func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
// Check that alice invoice wasn't settled and bandwidth of htlc
// links hasn't been changed.
invoice, _, err := receiver.registry.LookupInvoice(rhash)
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
@ -1330,7 +1330,7 @@ func TestChannelLinkMultiHopDecodeError(t *testing.T) {
// Check that alice invoice wasn't settled and bandwidth of htlc
// links hasn't been changed.
invoice, _, err := receiver.registry.LookupInvoice(rhash)
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
@ -3455,7 +3455,7 @@ func TestChannelRetransmission(t *testing.T) {
// Check that alice invoice wasn't settled and
// bandwidth of htlc links hasn't been changed.
invoice, _, err = receiver.registry.LookupInvoice(rhash)
invoice, err = receiver.registry.LookupInvoice(rhash)
if err != nil {
err = errors.Errorf("unable to get invoice: %v", err)
continue
@ -3974,7 +3974,7 @@ func TestChannelLinkAcceptOverpay(t *testing.T) {
// Even though we sent 2x what was asked for, Carol should still have
// accepted the payment and marked it as settled.
invoice, _, err := receiver.registry.LookupInvoice(rhash)
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}

View file

@ -768,13 +768,9 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
panic(err)
}
decodeExpiry := func(invoice string) (uint32, error) {
return testInvoiceCltvExpiry, nil
}
finalCltvRejectDelta := int32(5)
registry := invoices.NewRegistry(cdb, decodeExpiry, finalCltvRejectDelta)
registry := invoices.NewRegistry(cdb, finalCltvRejectDelta)
registry.Start()
return &mockInvoiceRegistry{
@ -783,7 +779,9 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
}
}
func (i *mockInvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice, uint32, error) {
func (i *mockInvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (
channeldb.Invoice, error) {
return i.registry.LookupInvoice(rHash)
}
@ -793,10 +791,11 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, eob []byte) (*invoices.HodlEvent, error) {
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) {
event, err := i.registry.NotifyExitHopHtlc(
rhash, amt, expiry, currentHeight, hodlChan, eob,
rhash, amt, expiry, currentHeight, circuitKey, hodlChan, eob,
)
if err != nil {
return nil, err

View file

@ -557,6 +557,7 @@ func generatePaymentWithPreimage(invoiceAmt, htlcAmt lnwire.MilliSatoshi,
Value: invoiceAmt,
PaymentPreimage: preimage,
},
FinalCltvDelta: testInvoiceCltvExpiry,
}
htlc := &lnwire.UpdateAddHTLC{

View file

@ -2,7 +2,6 @@ package invoices
import (
"errors"
"fmt"
"sync"
"sync/atomic"
@ -25,6 +24,13 @@ 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")
// errReplayedHtlc is returned if the htlc is already recorded on the
// invoice.
errReplayedHtlc = errors.New("replayed htlc")
)
// HodlEvent describes how an htlc should be resolved. If HodlEvent.Preimage is
@ -58,10 +64,6 @@ type InvoiceRegistry struct {
// new single invoice subscriptions are carried.
invoiceEvents chan interface{}
// decodeFinalCltvExpiry is a function used to decode the final expiry
// value from the payment request.
decodeFinalCltvExpiry func(invoice string) (uint32, error)
// subscriptions is a map from a payment hash to a list of subscribers.
// It is used for efficient notification of links.
hodlSubscriptions map[lntypes.Hash]map[chan<- interface{}]struct{}
@ -85,8 +87,7 @@ type InvoiceRegistry struct {
// wraps the persistent on-disk invoice storage with an additional in-memory
// layer. The in-memory layer is in place such that debug invoices can be added
// which are volatile yet available system wide within the daemon.
func NewRegistry(cdb *channeldb.DB, decodeFinalCltvExpiry func(invoice string) (
uint32, error), finalCltvRejectDelta int32) *InvoiceRegistry {
func NewRegistry(cdb *channeldb.DB, finalCltvRejectDelta int32) *InvoiceRegistry {
return &InvoiceRegistry{
cdb: cdb,
@ -97,7 +98,6 @@ func NewRegistry(cdb *channeldb.DB, decodeFinalCltvExpiry func(invoice string) (
invoiceEvents: make(chan interface{}, 100),
hodlSubscriptions: make(map[lntypes.Hash]map[chan<- interface{}]struct{}),
hodlReverseSubscriptions: make(map[chan<- interface{}]map[lntypes.Hash]struct{}),
decodeFinalCltvExpiry: decodeFinalCltvExpiry,
finalCltvRejectDelta: finalCltvRejectDelta,
quit: make(chan struct{}),
}
@ -404,76 +404,15 @@ func (i *InvoiceRegistry) AddInvoice(invoice *channeldb.Invoice,
}
// LookupInvoice looks up an invoice by its payment hash (R-Hash), if found
// then we're able to pull the funds pending within an HTLC. We'll also return
// what the expected min final CLTV delta is, pre-parsed from the payment
// request. This may be used by callers to determine if an HTLC is well formed
// according to the cltv delta.
// then we're able to pull the funds pending within an HTLC.
//
// TODO(roasbeef): ignore if settled?
func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice, uint32, error) {
func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
error) {
// We'll check the database to see if there's an existing matching
// invoice.
invoice, err := i.cdb.LookupInvoice(rHash)
if err != nil {
return channeldb.Invoice{}, 0, err
}
expiry, err := i.decodeFinalCltvExpiry(string(invoice.PaymentRequest))
if err != nil {
return channeldb.Invoice{}, 0, err
}
return invoice, expiry, nil
}
// checkHtlcParameters is a callback used inside invoice db transactions to
// atomically check-and-update an invoice.
func (i *InvoiceRegistry) checkHtlcParameters(invoice *channeldb.Invoice,
amtPaid lnwire.MilliSatoshi, htlcExpiry uint32, currentHeight int32) error {
// If the invoice is already canceled, there is no further checking to
// do.
if invoice.Terms.State == channeldb.ContractCanceled {
return channeldb.ErrInvoiceAlreadyCanceled
}
// If a payment has already been made, we only accept more payments if
// the amount is the exact same. This prevents probing with small
// amounts on settled invoices to find out the receiver node.
if invoice.AmtPaid != 0 && amtPaid != invoice.AmtPaid {
return ErrInvoiceAmountTooLow
}
// Return early in case the invoice was already accepted or settled. We
// don't want to check the expiry again, because it may be that we are
// just restarting.
switch invoice.Terms.State {
case channeldb.ContractAccepted:
return channeldb.ErrInvoiceAlreadyAccepted
case channeldb.ContractSettled:
return channeldb.ErrInvoiceAlreadySettled
}
// The invoice is still open. Check the expiry.
expiry, err := i.decodeFinalCltvExpiry(string(invoice.PaymentRequest))
if err != nil {
return err
}
if htlcExpiry < uint32(currentHeight+i.finalCltvRejectDelta) {
return ErrInvoiceExpiryTooSoon
}
if htlcExpiry < uint32(currentHeight)+expiry {
return ErrInvoiceExpiryTooSoon
}
// If an invoice amount is specified, check that enough is paid.
if invoice.Terms.Value > 0 && amtPaid < invoice.Terms.Value {
return ErrInvoiceAmountTooLow
}
return nil
return i.cdb.LookupInvoice(rHash)
}
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the invoice is a
@ -489,108 +428,155 @@ func (i *InvoiceRegistry) checkHtlcParameters(invoice *channeldb.Invoice,
// prevent deadlock.
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
hodlChan chan<- interface{}, eob []byte) (*HodlEvent, error) {
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*HodlEvent, error) {
i.Lock()
defer i.Unlock()
debugLog := func(s string) {
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v",
rHash[:], s, amtPaid, expiry)
log.Debugf("Invoice(%x): %v, amt=%v, expiry=%v, circuit=%v",
rHash[:], s, amtPaid, expiry, circuitKey)
}
// If this isn't a debug invoice, then we'll attempt to settle an
// invoice matching this rHash on disk (if one exists).
invoice, err := i.cdb.AcceptOrSettleInvoice(
rHash, amtPaid,
func(inv *channeldb.Invoice) error {
return i.checkHtlcParameters(
inv, amtPaid, expiry, currentHeight,
)
},
)
switch err {
// Default is to not update subscribers after the invoice update.
updateSubscribers := false
// If invoice is already settled, settle htlc. This means we accept more
// payments to the same invoice hash.
//
// NOTE: Though our recovery and forwarding logic is predominately
// batched, settling invoices happens iteratively. We may reject one of
// two payments for the same rhash at first, but then restart and reject
// both after seeing that the invoice has been settled. Without any
// record of which one settles first, it is ambiguous as to which one
// actually settled the invoice. Thus, by accepting all payments, we
// eliminate the race condition that can lead to this inconsistency.
//
// TODO(conner): track ownership of settlements to properly recover from
// failures? or add batch invoice settlement
case channeldb.ErrInvoiceAlreadySettled:
debugLog("accepting duplicate payment to settled invoice")
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.HtlcStateCancelled:
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.Terms.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.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.Terms.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
// 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 {
debugLog(err.Error())
return nil, err
}
if updateSubscribers {
i.notifyClients(rHash, invoice, invoice.Terms.State)
}
// Inspect latest htlc state on the invoice.
invoiceHtlc, ok := invoice.Htlcs[circuitKey]
// If it isn't recorded, cancel htlc.
if !ok {
return &HodlEvent{
Hash: rHash,
}, nil
}
switch invoiceHtlc.State {
case channeldb.HtlcStateCancelled:
return &HodlEvent{
Hash: rHash,
}, nil
case channeldb.HtlcStateSettled:
return &HodlEvent{
Hash: rHash,
Preimage: &invoice.Terms.PaymentPreimage,
}, nil
// If invoice is already canceled, cancel htlc.
case channeldb.ErrInvoiceAlreadyCanceled:
debugLog("invoice already canceled")
return &HodlEvent{
Hash: rHash,
}, nil
// If invoice is already accepted, add this htlc to the list of
// subscribers.
case channeldb.ErrInvoiceAlreadyAccepted:
debugLog("accepting duplicate payment to accepted invoice")
case channeldb.HtlcStateAccepted:
i.hodlSubscribe(hodlChan, rHash)
return nil, nil
// If there are not enough blocks left, cancel the htlc.
case ErrInvoiceExpiryTooSoon:
debugLog("expiry too soon")
return &HodlEvent{
Hash: rHash,
}, nil
// If there are not enough blocks left, cancel the htlc.
case ErrInvoiceAmountTooLow:
debugLog("amount too low")
return &HodlEvent{
Hash: rHash,
}, nil
// If this call settled the invoice, settle the htlc. Otherwise
// subscribe for a future hodl event.
case nil:
i.notifyClients(rHash, invoice, invoice.Terms.State)
switch invoice.Terms.State {
case channeldb.ContractSettled:
debugLog("settled")
return &HodlEvent{
Hash: rHash,
Preimage: &invoice.Terms.PaymentPreimage,
}, nil
case channeldb.ContractAccepted:
debugLog("accepted")
// Subscribe to updates to this invoice.
i.hodlSubscribe(hodlChan, rHash)
return nil, nil
default:
return nil, fmt.Errorf("unexpected invoice state %v",
invoice.Terms.State)
}
default:
debugLog(err.Error())
return nil, err
panic("unknown action")
}
}
@ -599,13 +585,31 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
i.Lock()
defer i.Unlock()
invoice, err := i.cdb.SettleHoldInvoice(preimage)
updateInvoice := func(invoice *channeldb.Invoice) (
*channeldb.InvoiceUpdateDesc, error) {
switch invoice.Terms.State {
case channeldb.ContractOpen:
return nil, channeldb.ErrInvoiceStillOpen
case channeldb.ContractCanceled:
return nil, channeldb.ErrInvoiceAlreadyCanceled
case channeldb.ContractSettled:
return nil, channeldb.ErrInvoiceAlreadySettled
}
return &channeldb.InvoiceUpdateDesc{
State: channeldb.ContractSettled,
Preimage: preimage,
}, nil
}
hash := preimage.Hash()
invoice, err := i.cdb.UpdateInvoice(hash, updateInvoice)
if err != nil {
log.Errorf("SettleHodlInvoice with preimage %v: %v", preimage, err)
return err
}
hash := preimage.Hash()
log.Debugf("Invoice(%v): settled with preimage %v", hash,
invoice.Terms.PaymentPreimage)
@ -626,7 +630,32 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
log.Debugf("Invoice(%v): canceling invoice", payHash)
invoice, err := i.cdb.CancelInvoice(payHash)
updateInvoice := func(invoice *channeldb.Invoice) (
*channeldb.InvoiceUpdateDesc, error) {
switch invoice.Terms.State {
case channeldb.ContractSettled:
return nil, channeldb.ErrInvoiceAlreadySettled
case channeldb.ContractCanceled:
return nil, channeldb.ErrInvoiceAlreadyCanceled
}
// Mark individual held htlcs as cancelled.
canceledHtlcs := make(
map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc,
)
for key := range invoice.Htlcs {
canceledHtlcs[key] = nil
}
// Move invoice to the canceled state.
return &channeldb.InvoiceUpdateDesc{
Htlcs: canceledHtlcs,
State: channeldb.ContractCanceled,
}, nil
}
invoice, err := i.cdb.UpdateInvoice(payHash, updateInvoice)
// Implement idempotency by returning success if the invoice was already
// canceled.

View file

@ -21,17 +21,15 @@ var (
hash = preimage.Hash()
testInvoiceExpiry = uint32(3)
testHtlcExpiry = uint32(5)
testCurrentHeight = int32(0)
testInvoiceCltvDelta = uint32(4)
testFinalCltvRejectDelta = int32(3)
testFinalCltvRejectDelta = int32(4)
testCurrentHeight = int32(1)
)
func decodeExpiry(payReq string) (uint32, error) {
return uint32(testInvoiceExpiry), nil
}
var (
testInvoice = &channeldb.Invoice{
Terms: channeldb.ContractTerm{
@ -48,7 +46,7 @@ func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
}
// Instantiate and start the invoice registry.
registry := NewRegistry(cdb, decodeExpiry, testFinalCltvRejectDelta)
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
err = registry.Start()
if err != nil {
@ -62,6 +60,15 @@ func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
}
}
func getCircuitKey(htlcID uint64) channeldb.CircuitKey {
return channeldb.CircuitKey{
ChanID: lnwire.ShortChannelID{
BlockHeight: 1, TxIndex: 2, TxPosition: 3,
},
HtlcID: htlcID,
}
}
// TestSettleInvoice tests settling of an invoice and related notifications.
func TestSettleInvoice(t *testing.T) {
registry, cleanup := newTestContext(t)
@ -119,7 +126,8 @@ func TestSettleInvoice(t *testing.T) {
// Settle invoice with a slightly higher amount.
amtPaid := lnwire.MilliSatoshi(100500)
_, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testInvoiceExpiry, 0, hodlChan, nil,
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
)
if err != nil {
t.Fatal(err)
@ -151,11 +159,11 @@ func TestSettleInvoice(t *testing.T) {
t.Fatal("no update received")
}
// Try to settle again. We need this idempotent behaviour after a
// restart.
// Try to settle again with the same htlc id. We need this idempotent
// behaviour after a restart.
event, err := registry.NotifyExitHopHtlc(
hash, amtPaid, testInvoiceExpiry, testCurrentHeight, hodlChan,
nil,
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
@ -164,12 +172,25 @@ func TestSettleInvoice(t *testing.T) {
t.Fatal("expected settle event")
}
// Try to settle again with a higher amount. This should result in a
// cancel event because after a restart the amount should still be the
// same. New HTLCs with a different amount should be rejected.
// 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(
hash, amtPaid+600, testInvoiceExpiry, testCurrentHeight,
hodlChan, nil,
hash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
getCircuitKey(1), hodlChan, nil,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if event.Preimage == nil {
t.Fatal("expected settle event")
}
// 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(
hash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
getCircuitKey(2), hodlChan, nil,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
@ -178,26 +199,14 @@ func TestSettleInvoice(t *testing.T) {
t.Fatal("expected cancel event")
}
// Try to settle again with a lower amount. This should show the same
// behaviour as settling with a higher amount.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid-600, testInvoiceExpiry, testCurrentHeight,
hodlChan, nil,
)
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if event.Preimage != nil {
t.Fatal("expected cancel event")
}
// Check that settled amount remains unchanged.
inv, _, err := registry.LookupInvoice(hash)
// Check that settled amount is equal to the sum of values of the htlcs
// 0 and 1.
inv, err := registry.LookupInvoice(hash)
if err != nil {
t.Fatal(err)
}
if inv.AmtPaid != amtPaid {
t.Fatal("expected amount to be unchanged")
if inv.AmtPaid != amtPaid+amtPaid+600 {
t.Fatal("amount incorrect")
}
// Try to cancel.
@ -305,7 +314,8 @@ func TestCancelInvoice(t *testing.T) {
// succeed.
hodlChan := make(chan interface{})
event, err := registry.NotifyExitHopHtlc(
hash, amt, testInvoiceExpiry, testCurrentHeight, hodlChan, nil,
hash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
)
if err != nil {
t.Fatal("expected settlement of a canceled invoice to succeed")
@ -324,7 +334,7 @@ func TestHoldInvoice(t *testing.T) {
defer cleanup()
// Instantiate and start the invoice registry.
registry := NewRegistry(cdb, decodeExpiry, testFinalCltvRejectDelta)
registry := NewRegistry(cdb, testFinalCltvRejectDelta)
err = registry.Start()
if err != nil {
@ -381,26 +391,52 @@ func TestHoldInvoice(t *testing.T) {
// NotifyExitHopHtlc without a preimage present in the invoice registry
// should be possible.
event, err := registry.NotifyExitHopHtlc(
hash, amtPaid, testInvoiceExpiry, testCurrentHeight, hodlChan,
nil,
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("unexpect direct settle")
t.Fatalf("expected htlc to be held")
}
// Test idempotency.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testInvoiceExpiry, testCurrentHeight, hodlChan,
nil,
hash, amtPaid, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("unexpect direct settle")
t.Fatalf("expected htlc to be held")
}
// Test replay at a higher height. We expect the same result because it
// is a replay.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
getCircuitKey(0), hodlChan, nil,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event != nil {
t.Fatalf("expected htlc to be held")
}
// Test a new htlc coming in that doesn't meet the final cltv delta
// requirement. It should be rejected.
event, err = registry.NotifyExitHopHtlc(
hash, amtPaid, 1, testCurrentHeight,
getCircuitKey(1), hodlChan, nil,
)
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if event == nil || event.Preimage != nil {
t.Fatalf("expected htlc to be cancelled")
}
// We expect the accepted state to be sent to the single invoice
@ -433,6 +469,10 @@ func TestHoldInvoice(t *testing.T) {
t.Fatalf("expected state ContractSettled, but got %v",
settledInvoice.Terms.State)
}
if settledInvoice.AmtPaid != amtPaid {
t.Fatalf("expected amount to be %v, but got %v",
amtPaid, settledInvoice.AmtPaid)
}
update = <-subscription.Updates
if update.Terms.State != channeldb.ContractSettled {
@ -490,7 +530,8 @@ func TestUnknownInvoice(t *testing.T) {
hodlChan := make(chan interface{})
amt := lnwire.MilliSatoshi(100000)
_, err := registry.NotifyExitHopHtlc(
hash, amt, testInvoiceExpiry, testCurrentHeight, hodlChan, nil,
hash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, nil,
)
if err != channeldb.ErrInvoiceNotFound {
t.Fatal("expected invoice not found error")

View file

@ -394,6 +394,8 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
Memo: []byte(invoice.Memo),
Receipt: invoice.Receipt,
PaymentRequest: []byte(payReqString),
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
Expiry: payReq.Expiry(),
Terms: channeldb.ContractTerm{
Value: amtMSat,
PaymentPreimage: paymentPreimage,

View file

@ -36,13 +36,6 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
settleDate = invoice.SettleDate.Unix()
}
// Expiry time will default to 3600 seconds if not specified
// explicitly.
expiry := int64(decoded.Expiry().Seconds())
// The expiry will default to 9 blocks if not specified explicitly.
cltvExpiry := decoded.MinFinalCLTVExpiry()
// Convert between the `lnrpc` and `routing` types.
routeHints := CreateRPCRouteHints(decoded.RouteHints)
@ -67,6 +60,38 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
invoice.Terms.State)
}
rpcHtlcs := make([]*lnrpc.InvoiceHTLC, 0, len(invoice.Htlcs))
for key, htlc := range invoice.Htlcs {
var state lnrpc.InvoiceHTLCState
switch htlc.State {
case channeldb.HtlcStateAccepted:
state = lnrpc.InvoiceHTLCState_ACCEPTED
case channeldb.HtlcStateSettled:
state = lnrpc.InvoiceHTLCState_SETTLED
case channeldb.HtlcStateCancelled:
state = lnrpc.InvoiceHTLCState_CANCELLED
default:
return nil, fmt.Errorf("unknown state %v", htlc.State)
}
rpcHtlc := lnrpc.InvoiceHTLC{
ChanId: key.ChanID.ToUint64(),
HtlcIndex: key.HtlcID,
AcceptHeight: int32(htlc.AcceptHeight),
AcceptTime: htlc.AcceptTime.Unix(),
ExpiryHeight: int32(htlc.Expiry),
AmtMsat: uint64(htlc.Amt),
State: state,
}
// Only report resolved times if htlc is resolved.
if htlc.State != channeldb.HtlcStateAccepted {
rpcHtlc.ResolveTime = htlc.ResolveTime.Unix()
}
rpcHtlcs = append(rpcHtlcs, &rpcHtlc)
}
rpcInvoice := &lnrpc.Invoice{
Memo: string(invoice.Memo[:]),
Receipt: invoice.Receipt[:],
@ -77,8 +102,8 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
Settled: isSettled,
PaymentRequest: paymentRequest,
DescriptionHash: descHash,
Expiry: expiry,
CltvExpiry: cltvExpiry,
Expiry: int64(invoice.Expiry.Seconds()),
CltvExpiry: uint64(invoice.FinalCltvDelta),
FallbackAddr: fallbackAddr,
RouteHints: routeHints,
AddIndex: invoice.AddIndex,
@ -88,6 +113,7 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
AmtPaidMsat: int64(invoice.AmtPaid),
AmtPaid: int64(invoice.AmtPaid),
State: state,
Htlcs: rpcHtlcs,
}
if preimage != channeldb.UnknownPreimage {

File diff suppressed because it is too large Load diff

View file

@ -2117,6 +2117,42 @@ message Invoice {
The state the invoice is in.
*/
InvoiceState state = 21 [json_name = "state"];
/// List of HTLCs paying to this invoice [EXPERIMENTAL].
repeated InvoiceHTLC htlcs = 22 [json_name = "htlcs"];
}
enum InvoiceHTLCState {
ACCEPTED = 0;
SETTLED = 1;
CANCELLED = 2;
}
/// Details of an HTLC that paid to an invoice
message InvoiceHTLC {
/// Short channel id over which the htlc was received.
uint64 chan_id = 1 [json_name = "chan_id"];
/// Index identifying the htlc on the channel.
uint64 htlc_index = 2 [json_name = "htlc_index"];
/// The amount of the htlc in msat.
uint64 amt_msat = 3 [json_name = "amt_msat"];
/// Block height at which this htlc was accepted.
int32 accept_height = 4 [json_name = "accept_height"];
/// Time at which this htlc was accepted.
int64 accept_time = 5 [json_name = "accept_time"];
/// Time at which this htlc was settled or cancelled.
int64 resolve_time = 6 [json_name = "resolve_time"];
/// Block height at which this htlc expires.
int32 expiry_height = 7 [json_name = "expiry_height"];
/// Current state the htlc is in.
InvoiceHTLCState state = 8 [json_name = "state"];
}
message AddInvoiceResponse {

View file

@ -2489,9 +2489,70 @@
"state": {
"$ref": "#/definitions/InvoiceInvoiceState",
"description": "*\nThe state the invoice is in."
},
"htlcs": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcInvoiceHTLC"
},
"description": "/ List of HTLCs paying to this invoice [EXPERIMENTAL]."
}
}
},
"lnrpcInvoiceHTLC": {
"type": "object",
"properties": {
"chan_id": {
"type": "string",
"format": "uint64",
"description": "/ Short channel id over which the htlc was received."
},
"htlc_index": {
"type": "string",
"format": "uint64",
"description": "/ Index identifying the htlc on the channel."
},
"amt_msat": {
"type": "string",
"format": "uint64",
"description": "/ The amount of the htlc in msat."
},
"accept_height": {
"type": "integer",
"format": "int32",
"description": "/ Block height at which this htlc was accepted."
},
"accept_time": {
"type": "string",
"format": "int64",
"description": "/ Time at which this htlc was accepted."
},
"resolve_time": {
"type": "string",
"format": "int64",
"description": "/ Time at which this htlc was settled or cancelled."
},
"expiry_height": {
"type": "integer",
"format": "int32",
"description": "/ Block height at which this htlc expires."
},
"state": {
"$ref": "#/definitions/lnrpcInvoiceHTLCState",
"description": "/ Current state the htlc is in."
}
},
"title": "/ Details of an HTLC that paid to an invoice"
},
"lnrpcInvoiceHTLCState": {
"type": "string",
"enum": [
"ACCEPTED",
"SETTLED",
"CANCELLED"
],
"default": "ACCEPTED"
},
"lnrpcLightningAddress": {
"type": "object",
"properties": {

View file

@ -3496,7 +3496,7 @@ func (r *rpcServer) LookupInvoice(ctx context.Context,
rpcsLog.Tracef("[lookupinvoice] searching for invoice %x", payHash[:])
invoice, _, err := r.server.invoices.LookupInvoice(payHash)
invoice, err := r.server.invoices.LookupInvoice(payHash)
if err != nil {
return nil, err
}

View file

@ -55,7 +55,6 @@ import (
"github.com/lightningnetwork/lnd/watchtower/wtclient"
"github.com/lightningnetwork/lnd/watchtower/wtdb"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
"github.com/lightningnetwork/lnd/zpay32"
)
const (
@ -347,14 +346,6 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
readBufferPool, cfg.Workers.Read, pool.DefaultWorkerTimeout,
)
decodeFinalCltvExpiry := func(payReq string) (uint32, error) {
invoice, err := zpay32.Decode(payReq, activeNetParams.Params)
if err != nil {
return 0, err
}
return uint32(invoice.MinFinalCLTVExpiry()), nil
}
s := &server{
chanDB: chanDB,
cc: cc,
@ -364,8 +355,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
chansToRestore: chansToRestore,
invoices: invoices.NewRegistry(
chanDB, decodeFinalCltvExpiry,
defaultFinalCltvRejectDelta,
chanDB, defaultFinalCltvRejectDelta,
),
channelNotifier: channelnotifier.New(chanDB),