diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index 510e0802..61806d39 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -1668,6 +1668,671 @@ func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { require.Equal(t, test.expError, err) } +type updateHTLCTest struct { + name string + input InvoiceHTLC + invState ContractState + setID *[32]byte + output InvoiceHTLC + expErr error +} + +// TestUpdateHTLC asserts the behavior of the updateHTLC method in various +// scenarios for MPP and AMP. +func TestUpdateHTLC(t *testing.T) { + t.Parallel() + + setID := [32]byte{0x01} + ampRecord := record.NewAMP([32]byte{0x02}, setID, 3) + preimage := lntypes.Preimage{0x04} + hash := preimage.Hash() + + diffSetID := [32]byte{0x05} + fakePreimage := lntypes.Preimage{0x06} + testAlreadyNow := time.Now() + + tests := []updateHTLCTest{ + { + name: "MPP accept", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + invState: ContractAccepted, + setID: nil, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + expErr: nil, + }, + { + name: "MPP settle", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + invState: ContractSettled, + setID: nil, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + expErr: nil, + }, + { + name: "MPP cancel", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + invState: ContractCanceled, + setID: nil, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + expErr: nil, + }, + { + name: "AMP accept missing preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + expErr: ErrHTLCPreimageMissing, + }, + { + name: "AMP accept invalid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + expErr: ErrHTLCPreimageMismatch, + }, + { + name: "AMP accept valid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "AMP accept valid preimage different htlc set", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &diffSetID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "AMP settle missing preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + expErr: ErrHTLCPreimageMissing, + }, + { + name: "AMP settle invalid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + expErr: ErrHTLCPreimageMismatch, + }, + { + name: "AMP settle valid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "AMP settle valid preimage different htlc set", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &diffSetID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "accept invoice htlc already settled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: ErrHTLCAlreadySettled, + }, + { + name: "cancel invoice htlc already settled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractCanceled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: ErrHTLCAlreadySettled, + }, + { + name: "settle invoice htlc already settled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "cancel invoice", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractCanceled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "accept invoice htlc already canceled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "cancel invoice htlc already canceled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractCanceled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "settle invoice htlc already canceled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + testUpdateHTLC(t, test) + }) + } +} + +func testUpdateHTLC(t *testing.T, test updateHTLCTest) { + htlc := test.input.Copy() + err := updateHtlc(testNow, htlc, test.invState, test.setID) + require.Equal(t, test.expErr, err) + require.Equal(t, test.output, *htlc) +} + // TestDeleteInvoices tests that deleting a list of invoices will succeed // if all delete references are valid, or will fail otherwise. func TestDeleteInvoices(t *testing.T) { diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 5daee5ff..2dcd1be8 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -117,6 +117,19 @@ var ( // match the invoice hash. ErrInvoicePreimageMismatch = errors.New("preimage does not match") + // ErrHTLCPreimageMissing is returned when trying to accept/settle an + // AMP HTLC but the HTLC-level preimage has not been set. + ErrHTLCPreimageMissing = errors.New("AMP htlc missing preimage") + + // ErrHTLCPreimageMismatch is returned when trying to accept/settle an + // AMP HTLC but the HTLC-level preimage does not satisfying the + // HTLC-level payment hash. + ErrHTLCPreimageMismatch = errors.New("htlc preimage mismatch") + + // ErrHTLCAlreadySettled is returned when trying to settle an invoice + // but HTLC already exists in the settled state. + ErrHTLCAlreadySettled = errors.New("htlc already settled") + // ErrInvoiceHasHtlcs is returned when attempting to insert an invoice // that already has HTLCs. ErrInvoiceHasHtlcs = errors.New("cannot add invoice with htlcs") @@ -2108,10 +2121,25 @@ func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC, // already know the preimage is valid due to checks at // the invoice level. For AMP HTLCs, verify that the // per-HTLC preimage-hash pair is valid. - if setID != nil && !htlc.AMP.Preimage.Matches(htlc.AMP.Hash) { - return fmt.Errorf("AMP preimage mismatch, "+ - "preimage=%v hash=%v", *htlc.AMP.Preimage, - htlc.AMP.Hash) + switch { + + // Non-AMP HTLCs can be settle immediately since we + // already know the preimage is valid due to checks at + // the invoice level. + case setID == nil: + + // At this popint, the setID is non-nil, meaning this is + // an AMP HTLC. We know that htlc.AMP cannot be nil, + // otherwise IsInHTLCSet would have returned false. + // + // Fail if an accepted AMP HTLC has no preimage. + case htlc.AMP.Preimage == nil: + return ErrHTLCPreimageMissing + + // Fail if the accepted AMP HTLC has an invalid + // preimage. + case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash): + return ErrHTLCPreimageMismatch } htlcState = HtlcStateSettled @@ -2140,8 +2168,7 @@ func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC, // We should never find a settled HTLC on an invoice that isn't in // ContractSettled. if htlc.State == HtlcStateSettled { - return fmt.Errorf("cannot have a settled htlc with "+ - "invoice in state %v", invState) + return ErrHTLCAlreadySettled } switch invState {