mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-04 01:36:24 +01:00
invoices: move UpdateInvoice implementation to the invoices package
With the introducation of the `InvoiceUpdater` interface we are now able to move the non-kv parts of `UpdateInvoice` completely under the invoices package. This is a preprequisite for being able to use the same code-base for the sql InvoiceDB implementation of UpdateInvoice.
This commit is contained in:
parent
ecbfc46312
commit
6b0931af82
4 changed files with 1512 additions and 1491 deletions
|
@ -2333,682 +2333,6 @@ func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) {
|
|||
require.Equal(t, test.expError, err)
|
||||
}
|
||||
|
||||
type updateHTLCTest struct {
|
||||
name string
|
||||
input invpkg.InvoiceHTLC
|
||||
invState invpkg.ContractState
|
||||
setID *[32]byte
|
||||
output invpkg.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: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: nil,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "MPP settle",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: nil,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "MPP cancel",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
},
|
||||
invState: invpkg.ContractCanceled,
|
||||
setID: nil,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "AMP accept missing preimage",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: nil,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: nil,
|
||||
},
|
||||
},
|
||||
expErr: invpkg.ErrHTLCPreimageMissing,
|
||||
},
|
||||
{
|
||||
name: "AMP accept invalid preimage",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &fakePreimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &fakePreimage,
|
||||
},
|
||||
},
|
||||
expErr: invpkg.ErrHTLCPreimageMismatch,
|
||||
},
|
||||
{
|
||||
name: "AMP accept valid preimage",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "AMP accept valid preimage different htlc set",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: &diffSetID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "AMP settle missing preimage",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: nil,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: nil,
|
||||
},
|
||||
},
|
||||
expErr: invpkg.ErrHTLCPreimageMissing,
|
||||
},
|
||||
{
|
||||
name: "AMP settle invalid preimage",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &fakePreimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &fakePreimage,
|
||||
},
|
||||
},
|
||||
expErr: invpkg.ErrHTLCPreimageMismatch,
|
||||
},
|
||||
{
|
||||
name: "AMP settle valid preimage",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
// With the newer AMP logic, this is now valid, as we
|
||||
// want to be able to accept multiple settle attempts
|
||||
// to a given pay_addr. In this case, the HTLC should
|
||||
// remain in the accepted state.
|
||||
name: "AMP settle valid preimage different htlc set",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: &diffSetID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "accept invoice htlc already settled",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: invpkg.ErrHTLCAlreadySettled,
|
||||
},
|
||||
{
|
||||
name: "cancel invoice htlc already settled",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractCanceled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: invpkg.ErrHTLCAlreadySettled,
|
||||
},
|
||||
{
|
||||
name: "settle invoice htlc already settled",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cancel invoice",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractCanceled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "accept invoice htlc already canceled",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractAccepted,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cancel invoice htlc already canceled",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractCanceled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "settle invoice htlc already canceled",
|
||||
input: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
},
|
||||
},
|
||||
invState: invpkg.ContractSettled,
|
||||
setID: &setID,
|
||||
output: invpkg.InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: invpkg.HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: &invpkg.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()
|
||||
stateChanged, state, err := getUpdatedHtlcState(
|
||||
htlc, test.invState, test.setID,
|
||||
)
|
||||
if stateChanged {
|
||||
htlc.State = state
|
||||
htlc.ResolveTime = testNow
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -673,7 +673,7 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
|
|||
}
|
||||
|
||||
payHash := ref.PayHash()
|
||||
updatedInvoice, err = updateInvoice(
|
||||
updatedInvoice, err = invpkg.UpdateInvoice(
|
||||
payHash, updater.invoice, now, callback, updater,
|
||||
)
|
||||
|
||||
|
@ -2064,820 +2064,6 @@ func makeInvoiceSetIDKey(invoiceNum, setID []byte) [invoiceSetIDKeyLen]byte {
|
|||
return invoiceSetIDKey
|
||||
}
|
||||
|
||||
// updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its
|
||||
// set ID), and updates the internal AMP state of an invoice, and also tallies
|
||||
// the set of HTLCs to be updated on disk.
|
||||
func acceptHtlcsAmp(invoice *invpkg.Invoice, setID invpkg.SetID,
|
||||
circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC,
|
||||
updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
newAmpState, err := getUpdatedInvoiceAmpState(
|
||||
invoice, setID, circuitKey, invpkg.HtlcStateAccepted, htlc.Amt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AMPState[setID] = newAmpState
|
||||
|
||||
// Mark the updates as needing to be written to disk.
|
||||
return updater.UpdateAmpState(setID, newAmpState, circuitKey)
|
||||
}
|
||||
|
||||
// cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP
|
||||
// HTLC set. We'll need to update the meta data in the main invoice, and also
|
||||
// apply the new update to the update MAP, since all the HTLCs for a given HTLC
|
||||
// set need to be written in-line with each other.
|
||||
func cancelHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey,
|
||||
htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
setID := htlc.AMP.Record.SetID()
|
||||
|
||||
// First, we'll update the state of the entire HTLC set
|
||||
// to cancelled.
|
||||
newAmpState, err := getUpdatedInvoiceAmpState(
|
||||
invoice, setID, circuitKey, invpkg.HtlcStateCanceled,
|
||||
htlc.Amt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AMPState[setID] = newAmpState
|
||||
|
||||
// Mark the updates as needing to be written to disk.
|
||||
err = updater.UpdateAmpState(setID, newAmpState, circuitKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll only decrement the total amount paid if the invoice was
|
||||
// already in the accepted state.
|
||||
if invoice.AmtPaid != 0 {
|
||||
return updateInvoiceAmtPaid(
|
||||
invoice, invoice.AmtPaid-htlc.Amt, updater,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP
|
||||
// invoice. We'll update some meta data in the main invoice, and also signal
|
||||
// that this HTLC set needs to be re-written back to disk.
|
||||
func settleHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey,
|
||||
htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
setID := htlc.AMP.Record.SetID()
|
||||
|
||||
// Next update the main AMP meta-data to indicate that this HTLC set
|
||||
// has been fully settled.
|
||||
newAmpState, err := getUpdatedInvoiceAmpState(
|
||||
invoice, setID, circuitKey, invpkg.HtlcStateSettled, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AMPState[setID] = newAmpState
|
||||
|
||||
// Mark the updates as needing to be written to disk.
|
||||
return updater.UpdateAmpState(setID, newAmpState, circuitKey)
|
||||
}
|
||||
|
||||
// updateInvoice fetches the invoice, obtains the update descriptor from the
|
||||
// callback and applies the updates in a single db transaction.
|
||||
func updateInvoice(hash *lntypes.Hash, invoice *invpkg.Invoice,
|
||||
updateTime time.Time, callback invpkg.InvoiceUpdateCallback,
|
||||
updater invpkg.InvoiceUpdater) (*invpkg.Invoice, error) {
|
||||
|
||||
// Create deep copy to prevent any accidental modification in the
|
||||
// callback.
|
||||
invoiceCopy, err := invpkg.CopyInvoice(invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Call the callback and obtain the update descriptor.
|
||||
update, err := callback(invoiceCopy)
|
||||
if err != nil {
|
||||
return invoice, err
|
||||
}
|
||||
|
||||
// If there is nothing to update, return early.
|
||||
if update == nil {
|
||||
return invoice, nil
|
||||
}
|
||||
|
||||
switch update.UpdateType {
|
||||
case invpkg.CancelHTLCsUpdate:
|
||||
err := cancelHTLCs(invoice, updateTime, update, updater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case invpkg.AddHTLCsUpdate:
|
||||
err := addHTLCs(invoice, hash, updateTime, update, updater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case invpkg.SettleHodlInvoiceUpdate:
|
||||
err := settleHodlInvoice(
|
||||
invoice, hash, updateTime, update.State, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case invpkg.CancelInvoiceUpdate:
|
||||
err := cancelInvoice(
|
||||
invoice, hash, updateTime, update.State, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown update type: %s",
|
||||
update.UpdateType)
|
||||
}
|
||||
|
||||
if err := updater.Finalize(update.UpdateType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoice, nil
|
||||
}
|
||||
|
||||
// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc.
|
||||
//
|
||||
// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the
|
||||
// InvoiceUpdateDesc.
|
||||
func cancelHTLCs(invoice *invpkg.Invoice, updateTime time.Time,
|
||||
update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
for key := range update.CancelHtlcs {
|
||||
htlc, exists := invoice.Htlcs[key]
|
||||
|
||||
// Verify that we don't get an action for htlcs that are not
|
||||
// present on the invoice.
|
||||
if !exists {
|
||||
return fmt.Errorf("cancel of non-existent htlc")
|
||||
}
|
||||
|
||||
err := canCancelSingleHtlc(htlc, invoice.State)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = resolveHtlc(
|
||||
key, htlc, invpkg.HtlcStateCanceled, updateTime,
|
||||
updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Tally this into the set of HTLCs that need to be updated on
|
||||
// disk, but once again, only if this is an AMP invoice.
|
||||
if invoice.IsAMP() {
|
||||
err := cancelHtlcsAmp(invoice, key, htlc, updater)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc.
|
||||
func addHTLCs(invoice *invpkg.Invoice, hash *lntypes.Hash, updateTime time.Time,
|
||||
update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
var setID *[32]byte
|
||||
invoiceIsAMP := invoice.IsAMP()
|
||||
if invoiceIsAMP && update.State != nil {
|
||||
setID = update.State.SetID
|
||||
}
|
||||
|
||||
for key, htlcUpdate := range update.AddHtlcs {
|
||||
if _, exists := invoice.Htlcs[key]; exists {
|
||||
return fmt.Errorf("duplicate add of htlc %v", key)
|
||||
}
|
||||
|
||||
// Force caller to supply htlc without custom records in a
|
||||
// consistent way.
|
||||
if htlcUpdate.CustomRecords == nil {
|
||||
return errors.New("nil custom records map")
|
||||
}
|
||||
|
||||
htlc := &invpkg.InvoiceHTLC{
|
||||
Amt: htlcUpdate.Amt,
|
||||
MppTotalAmt: htlcUpdate.MppTotalAmt,
|
||||
Expiry: htlcUpdate.Expiry,
|
||||
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
||||
AcceptTime: updateTime,
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
CustomRecords: htlcUpdate.CustomRecords,
|
||||
}
|
||||
|
||||
if invoiceIsAMP {
|
||||
if htlcUpdate.AMP == nil {
|
||||
return fmt.Errorf("unable to add htlc "+
|
||||
"without AMP data to AMP invoice(%v)",
|
||||
invoice.AddIndex)
|
||||
}
|
||||
|
||||
htlc.AMP = htlcUpdate.AMP.Copy()
|
||||
}
|
||||
|
||||
if err := updater.AddHtlc(key, htlc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.Htlcs[key] = htlc
|
||||
|
||||
// Collect the set of new HTLCs so we can write them properly
|
||||
// below, but only if this is an AMP invoice.
|
||||
if invoiceIsAMP {
|
||||
err := acceptHtlcsAmp(
|
||||
invoice, htlcUpdate.AMP.Record.SetID(), key,
|
||||
htlc, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the set of accepted HTLCs should be fully
|
||||
// populated with added HTLCs or removed of canceled ones. Update
|
||||
// invoice state if the update descriptor indicates an invoice state
|
||||
// change, which depends on having an accurate view of the accepted
|
||||
// HTLCs.
|
||||
if update.State != nil {
|
||||
newState, err := getUpdatedInvoiceState(
|
||||
invoice, hash, *update.State,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this isn't an AMP invoice, then we'll go ahead and update
|
||||
// the invoice state directly here. For AMP invoices, we instead
|
||||
// will keep the top-level invoice open, and update the state of
|
||||
// each _htlc set_ instead. However, we'll allow the invoice to
|
||||
// transition to the cancelled state regardless.
|
||||
if !invoiceIsAMP || *newState == invpkg.ContractCanceled {
|
||||
err := updater.UpdateInvoiceState(*newState, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.State = *newState
|
||||
}
|
||||
}
|
||||
|
||||
// The set of HTLC pre-images will only be set if we were actually able
|
||||
// to reconstruct all the AMP pre-images.
|
||||
var settleEligibleAMP bool
|
||||
if update.State != nil {
|
||||
settleEligibleAMP = len(update.State.HTLCPreimages) != 0
|
||||
}
|
||||
|
||||
// With any invoice level state transitions recorded, we'll now
|
||||
// finalize the process by updating the state transitions for
|
||||
// individual HTLCs
|
||||
var amtPaid lnwire.MilliSatoshi
|
||||
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
// Set the HTLC preimage for any AMP HTLCs.
|
||||
if setID != nil && update.State != nil {
|
||||
preimage, ok := update.State.HTLCPreimages[key]
|
||||
switch {
|
||||
// If we don't already have a preimage for this HTLC, we
|
||||
// can set it now.
|
||||
case ok && htlc.AMP.Preimage == nil:
|
||||
err := updater.AddAmpHtlcPreimage(
|
||||
htlc.AMP.Record.SetID(), key, preimage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
htlc.AMP.Preimage = &preimage
|
||||
|
||||
// Otherwise, prevent over-writing an existing
|
||||
// preimage. Ignore the case where the preimage is
|
||||
// identical.
|
||||
case ok && *htlc.AMP.Preimage != preimage:
|
||||
return invpkg.ErrHTLCPreimageAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// If we have all the pre-images for an AMP invoice, then we'll
|
||||
// act as if we're able to settle the entire invoice. We need
|
||||
// to do this since it's possible for us to settle AMP invoices
|
||||
// while the contract state (on disk) is still in the accept
|
||||
// state.
|
||||
htlcContextState := invoice.State
|
||||
if settleEligibleAMP {
|
||||
htlcContextState = invpkg.ContractSettled
|
||||
}
|
||||
htlcStateChanged, htlcState, err := getUpdatedHtlcState(
|
||||
htlc, htlcContextState, setID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if htlcStateChanged {
|
||||
err = resolveHtlc(
|
||||
key, htlc, htlcState, updateTime, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
htlcSettled := htlcStateChanged &&
|
||||
htlcState == invpkg.HtlcStateSettled
|
||||
|
||||
// If the HTLC has being settled for the first time, and this
|
||||
// is an AMP invoice, then we'll need to update some additional
|
||||
// meta data state.
|
||||
if htlcSettled && invoiceIsAMP {
|
||||
err = settleHtlcsAmp(invoice, key, htlc, updater)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
accepted := htlc.State == invpkg.HtlcStateAccepted
|
||||
settled := htlc.State == invpkg.HtlcStateSettled
|
||||
invoiceStateReady := accepted || settled
|
||||
|
||||
if !invoiceIsAMP {
|
||||
// Update the running amount paid to this invoice. We
|
||||
// don't include accepted htlcs when the invoice is
|
||||
// still open.
|
||||
if invoice.State != invpkg.ContractOpen &&
|
||||
invoiceStateReady {
|
||||
|
||||
amtPaid += htlc.Amt
|
||||
}
|
||||
} else {
|
||||
// For AMP invoices, since we won't always be reading
|
||||
// out the total invoice set each time, we'll instead
|
||||
// accumulate newly added invoices to the total amount
|
||||
// paid.
|
||||
if _, ok := update.AddHtlcs[key]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the running amount paid to this invoice. AMP
|
||||
// invoices never go to the settled state, so if it's
|
||||
// open, then we tally the HTLC.
|
||||
if invoice.State == invpkg.ContractOpen &&
|
||||
invoiceStateReady {
|
||||
|
||||
amtPaid += htlc.Amt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For non-AMP invoices we recalculate the amount paid from scratch
|
||||
// each time, while for AMP invoices, we'll accumulate only based on
|
||||
// newly added HTLCs.
|
||||
if invoiceIsAMP {
|
||||
amtPaid += invoice.AmtPaid
|
||||
}
|
||||
|
||||
return updateInvoiceAmtPaid(invoice, amtPaid, updater)
|
||||
}
|
||||
|
||||
func resolveHtlc(circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC,
|
||||
state invpkg.HtlcState, resolveTime time.Time,
|
||||
updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
err := updater.ResolveHtlc(circuitKey, state, resolveTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
htlc.State = state
|
||||
htlc.ResolveTime = resolveTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateInvoiceAmtPaid(invoice *invpkg.Invoice, amt lnwire.MilliSatoshi,
|
||||
updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
err := updater.UpdateInvoiceAmtPaid(amt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.AmtPaid = amt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleHodlInvoice marks a hodl invoice as settled.
|
||||
//
|
||||
// NOTE: Currently it is not possible to have HODL AMP invoices.
|
||||
|
||||
func settleHodlInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash,
|
||||
updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc,
|
||||
updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
if !invoice.HodlInvoice {
|
||||
return fmt.Errorf("unable to settle hodl invoice: %v is "+
|
||||
"not a hodl invoice", invoice.AddIndex)
|
||||
}
|
||||
|
||||
// TODO(positiveblue): because NewState can only be ContractSettled we
|
||||
// can remove it from the API and set it here directly.
|
||||
switch {
|
||||
case update == nil:
|
||||
fallthrough
|
||||
|
||||
case update.NewState != invpkg.ContractSettled:
|
||||
return fmt.Errorf("unable to settle hodl invoice: "+
|
||||
"not valid InvoiceUpdateDesc.State: %v", update)
|
||||
|
||||
case update.Preimage == nil:
|
||||
return fmt.Errorf("unable to settle hodl invoice: " +
|
||||
"preimage is nil")
|
||||
}
|
||||
|
||||
newState, err := getUpdatedInvoiceState(
|
||||
invoice, hash, *update,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newState == nil || *newState != invpkg.ContractSettled {
|
||||
return fmt.Errorf("unable to settle hodl invoice: "+
|
||||
"new computed state is not settled: %s", newState)
|
||||
}
|
||||
|
||||
err = updater.UpdateInvoiceState(
|
||||
invpkg.ContractSettled, update.Preimage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.State = invpkg.ContractSettled
|
||||
invoice.Terms.PaymentPreimage = update.Preimage
|
||||
|
||||
// TODO(positiveblue): this logic can be further simplified.
|
||||
var amtPaid lnwire.MilliSatoshi
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
settled, _, err := getUpdatedHtlcState(
|
||||
htlc, invpkg.ContractSettled, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settled {
|
||||
err = resolveHtlc(
|
||||
key, htlc, invpkg.HtlcStateSettled, updateTime,
|
||||
updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amtPaid += htlc.Amt
|
||||
}
|
||||
}
|
||||
|
||||
return updateInvoiceAmtPaid(invoice, amtPaid, updater)
|
||||
}
|
||||
|
||||
// cancelInvoice attempts to cancel the given invoice. That includes changing
|
||||
// the invoice state and the state of any relevant HTLC.
|
||||
func cancelInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash,
|
||||
updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc,
|
||||
updater invpkg.InvoiceUpdater) error {
|
||||
|
||||
switch {
|
||||
case update == nil:
|
||||
fallthrough
|
||||
|
||||
case update.NewState != invpkg.ContractCanceled:
|
||||
return fmt.Errorf("unable to cancel invoice: "+
|
||||
"InvoiceUpdateDesc.State not valid: %v", update)
|
||||
}
|
||||
|
||||
var (
|
||||
setID *[32]byte
|
||||
invoiceIsAMP bool
|
||||
)
|
||||
|
||||
invoiceIsAMP = invoice.IsAMP()
|
||||
if invoiceIsAMP {
|
||||
setID = update.SetID
|
||||
}
|
||||
|
||||
newState, err := getUpdatedInvoiceState(invoice, hash, *update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newState == nil || *newState != invpkg.ContractCanceled {
|
||||
return fmt.Errorf("unable to cancel invoice(%v): new "+
|
||||
"computed state is not canceled: %s", invoice.AddIndex,
|
||||
newState)
|
||||
}
|
||||
|
||||
err = updater.UpdateInvoiceState(invpkg.ContractCanceled, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.State = invpkg.ContractCanceled
|
||||
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
canceled, _, err := getUpdatedHtlcState(
|
||||
htlc, invpkg.ContractCanceled, setID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if canceled {
|
||||
err = resolveHtlc(
|
||||
key, htlc, invpkg.HtlcStateCanceled, updateTime,
|
||||
updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUpdatedInvoiceState validates and processes an invoice state update. The
|
||||
// new state to transition to is returned, so the caller is able to select
|
||||
// exactly how the invoice state is updated. Note that for AMP invoices this
|
||||
// function is only used to validate the state transition if we're cancelling
|
||||
// the invoice.
|
||||
func getUpdatedInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash,
|
||||
update invpkg.InvoiceStateUpdateDesc) (*invpkg.ContractState, error) {
|
||||
|
||||
// Returning to open is never allowed from any state.
|
||||
if update.NewState == invpkg.ContractOpen {
|
||||
return nil, invpkg.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 invpkg.ContractAccepted:
|
||||
if update.NewState == invpkg.ContractAccepted {
|
||||
return nil, invpkg.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 invpkg.ContractOpen:
|
||||
if update.NewState == invpkg.ContractCanceled {
|
||||
return &update.NewState, nil
|
||||
}
|
||||
|
||||
// Sanity check that the user isn't trying to settle or accept a
|
||||
// non-existent HTLC set.
|
||||
set := invoice.HTLCSet(update.SetID, invpkg.HtlcStateAccepted)
|
||||
if len(set) == 0 {
|
||||
return nil, invpkg.ErrEmptyHTLCSet
|
||||
}
|
||||
|
||||
// For AMP invoices, there are no invoice-level preimage checks.
|
||||
// However, we still sanity check that we aren't trying to
|
||||
// settle an AMP invoice with a preimage.
|
||||
if update.SetID != nil {
|
||||
if update.Preimage != nil {
|
||||
return nil, errors.New("AMP set cannot have " +
|
||||
"preimage")
|
||||
}
|
||||
|
||||
return &update.NewState, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
// If an invoice-level preimage was supplied, but the InvoiceRef
|
||||
// doesn't specify a hash (e.g. AMP invoices) we fail.
|
||||
case update.Preimage != nil && hash == nil:
|
||||
return nil, invpkg.ErrUnexpectedInvoicePreimage
|
||||
|
||||
// Validate the supplied preimage for non-AMP invoices.
|
||||
case update.Preimage != nil:
|
||||
if update.Preimage.Hash() != *hash {
|
||||
return nil, invpkg.ErrInvoicePreimageMismatch
|
||||
}
|
||||
|
||||
// Permit non-AMP invoices to be accepted without knowing the
|
||||
// preimage. When trying to settle we'll have to pass through
|
||||
// the above check in order to not hit the one below.
|
||||
case update.NewState == invpkg.ContractAccepted:
|
||||
|
||||
// Fail if we still don't have a preimage when transitioning to
|
||||
// settle the non-AMP invoice.
|
||||
case update.NewState == invpkg.ContractSettled &&
|
||||
invoice.Terms.PaymentPreimage == nil:
|
||||
|
||||
return nil, errors.New("unknown preimage")
|
||||
}
|
||||
|
||||
return &update.NewState, nil
|
||||
|
||||
// Once settled, we are in a terminal state.
|
||||
case invpkg.ContractSettled:
|
||||
return nil, invpkg.ErrInvoiceAlreadySettled
|
||||
|
||||
// Once canceled, we are in a terminal state.
|
||||
case invpkg.ContractCanceled:
|
||||
return nil, invpkg.ErrInvoiceAlreadyCanceled
|
||||
|
||||
default:
|
||||
return nil, errors.New("unknown state transition")
|
||||
}
|
||||
}
|
||||
|
||||
// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without
|
||||
// applying it), given the new state, and the amount of the HTLC that is
|
||||
// being updated.
|
||||
func getUpdatedInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID,
|
||||
circuitKey models.CircuitKey, state invpkg.HtlcState,
|
||||
amt lnwire.MilliSatoshi) (invpkg.InvoiceStateAMP, error) {
|
||||
|
||||
// Retrieve the AMP state for this set ID.
|
||||
ampState, ok := invoice.AMPState[setID]
|
||||
|
||||
// If the state is accepted then we may need to create a new entry for
|
||||
// this set ID, otherwise we expect that the entry already exists and
|
||||
// we can update it.
|
||||
if !ok && state != invpkg.HtlcStateAccepted {
|
||||
return invpkg.InvoiceStateAMP{},
|
||||
fmt.Errorf("unable to update AMP state for setID=%x ",
|
||||
setID)
|
||||
}
|
||||
|
||||
switch state {
|
||||
case invpkg.HtlcStateAccepted:
|
||||
if !ok {
|
||||
// If an entry for this set ID doesn't already exist,
|
||||
// then we'll need to create it.
|
||||
ampState = invpkg.InvoiceStateAMP{
|
||||
State: invpkg.HtlcStateAccepted,
|
||||
InvoiceKeys: make(
|
||||
map[models.CircuitKey]struct{},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
ampState.AmtPaid += amt
|
||||
|
||||
case invpkg.HtlcStateCanceled:
|
||||
ampState.State = invpkg.HtlcStateCanceled
|
||||
ampState.AmtPaid -= amt
|
||||
|
||||
case invpkg.HtlcStateSettled:
|
||||
ampState.State = invpkg.HtlcStateSettled
|
||||
}
|
||||
|
||||
ampState.InvoiceKeys[circuitKey] = struct{}{}
|
||||
|
||||
return ampState, nil
|
||||
}
|
||||
|
||||
// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is
|
||||
// returned, then the HTLC can be cancelled.
|
||||
func canCancelSingleHtlc(htlc *invpkg.InvoiceHTLC,
|
||||
invoiceState invpkg.ContractState) error {
|
||||
|
||||
// It is only possible to cancel individual htlcs on an open invoice.
|
||||
if invoiceState != invpkg.ContractOpen {
|
||||
return fmt.Errorf("htlc canceled on invoice in state %v",
|
||||
invoiceState)
|
||||
}
|
||||
|
||||
// It is only possible if the htlc is still pending.
|
||||
if htlc.State != invpkg.HtlcStateAccepted {
|
||||
return fmt.Errorf("htlc canceled in state %v", htlc.State)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUpdatedHtlcState aligns the state of an htlc with the given invoice state.
|
||||
// A boolean indicating whether the HTLCs state need to be updated, along with
|
||||
// the new state (or old state if no change is needed) is returned.
|
||||
func getUpdatedHtlcState(htlc *invpkg.InvoiceHTLC,
|
||||
invoiceState invpkg.ContractState, setID *[32]byte) (
|
||||
bool, invpkg.HtlcState, error) {
|
||||
|
||||
trySettle := func(persist bool) (bool, invpkg.HtlcState, error) {
|
||||
if htlc.State != invpkg.HtlcStateAccepted {
|
||||
return false, htlc.State, nil
|
||||
}
|
||||
|
||||
// Settle the HTLC if it matches the settled set id. If
|
||||
// there're other HTLCs with distinct setIDs, then we'll leave
|
||||
// them, as they may eventually be settled as we permit
|
||||
// multiple settles to a single pay_addr for AMP.
|
||||
settled := false
|
||||
if htlc.IsInHTLCSet(setID) {
|
||||
// Non-AMP HTLCs can be settled immediately since we
|
||||
// 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.
|
||||
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 point, 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 false, htlc.State,
|
||||
invpkg.ErrHTLCPreimageMissing
|
||||
|
||||
// Fail if the accepted AMP HTLC has an invalid
|
||||
// preimage.
|
||||
case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash):
|
||||
return false, htlc.State,
|
||||
invpkg.ErrHTLCPreimageMismatch
|
||||
}
|
||||
|
||||
settled = true
|
||||
}
|
||||
|
||||
// Only persist the changes if the invoice is moving to the
|
||||
// settled state, and we're actually updating the state to
|
||||
// settled.
|
||||
newState := htlc.State
|
||||
if settled {
|
||||
newState = invpkg.HtlcStateSettled
|
||||
}
|
||||
|
||||
return persist && settled, newState, nil
|
||||
}
|
||||
|
||||
if invoiceState == invpkg.ContractSettled {
|
||||
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
|
||||
// this will be a NOP, but for AMP HTLCs this asserts that we
|
||||
// have a valid hash/preimage pair. Passing true permits the
|
||||
// method to update the HTLC to HtlcStateSettled.
|
||||
return trySettle(true)
|
||||
}
|
||||
|
||||
// We should never find a settled HTLC on an invoice that isn't in
|
||||
// ContractSettled.
|
||||
if htlc.State == invpkg.HtlcStateSettled {
|
||||
return false, htlc.State, invpkg.ErrHTLCAlreadySettled
|
||||
}
|
||||
|
||||
switch invoiceState {
|
||||
case invpkg.ContractCanceled:
|
||||
htlcAlreadyCanceled := htlc.State == invpkg.HtlcStateCanceled
|
||||
return !htlcAlreadyCanceled, invpkg.HtlcStateCanceled, nil
|
||||
|
||||
// TODO(roasbeef): never fully passed thru now?
|
||||
case invpkg.ContractAccepted:
|
||||
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
|
||||
// this will be a NOP, but for AMP HTLCs this asserts that we
|
||||
// have a valid hash/preimage pair. Passing false prevents the
|
||||
// method from putting the HTLC in HtlcStateSettled, leaving it
|
||||
// in HtlcStateAccepted.
|
||||
return trySettle(false)
|
||||
|
||||
case invpkg.ContractOpen:
|
||||
return false, htlc.State, nil
|
||||
|
||||
default:
|
||||
return false, htlc.State, errors.New("unknown state transition")
|
||||
}
|
||||
}
|
||||
|
||||
// delAMPInvoices attempts to delete all the "sub" invoices associated with a
|
||||
// greater AMP invoices. We do this by deleting the set of keys that share the
|
||||
// invoice number as a prefix.
|
||||
|
|
824
invoices/update_invoice.go
Normal file
824
invoices/update_invoice.go
Normal file
|
@ -0,0 +1,824 @@
|
|||
package invoices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its
|
||||
// set ID), and updates the internal AMP state of an invoice, and also tallies
|
||||
// the set of HTLCs to be updated on disk.
|
||||
func acceptHtlcsAmp(invoice *Invoice, setID SetID,
|
||||
circuitKey models.CircuitKey, htlc *InvoiceHTLC,
|
||||
updater InvoiceUpdater) error {
|
||||
|
||||
newAmpState, err := getUpdatedInvoiceAmpState(
|
||||
invoice, setID, circuitKey, HtlcStateAccepted, htlc.Amt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AMPState[setID] = newAmpState
|
||||
|
||||
// Mark the updates as needing to be written to disk.
|
||||
return updater.UpdateAmpState(setID, newAmpState, circuitKey)
|
||||
}
|
||||
|
||||
// cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP
|
||||
// HTLC set. We'll need to update the meta data in the main invoice, and also
|
||||
// apply the new update to the update MAP, since all the HTLCs for a given HTLC
|
||||
// set need to be written in-line with each other.
|
||||
func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
|
||||
htlc *InvoiceHTLC, updater InvoiceUpdater) error {
|
||||
|
||||
setID := htlc.AMP.Record.SetID()
|
||||
|
||||
// First, we'll update the state of the entire HTLC set
|
||||
// to cancelled.
|
||||
newAmpState, err := getUpdatedInvoiceAmpState(
|
||||
invoice, setID, circuitKey, HtlcStateCanceled,
|
||||
htlc.Amt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AMPState[setID] = newAmpState
|
||||
|
||||
// Mark the updates as needing to be written to disk.
|
||||
err = updater.UpdateAmpState(setID, newAmpState, circuitKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll only decrement the total amount paid if the invoice was
|
||||
// already in the accepted state.
|
||||
if invoice.AmtPaid != 0 {
|
||||
return updateInvoiceAmtPaid(
|
||||
invoice, invoice.AmtPaid-htlc.Amt, updater,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP
|
||||
// invoice. We'll update some meta data in the main invoice, and also signal
|
||||
// that this HTLC set needs to be re-written back to disk.
|
||||
func settleHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
|
||||
htlc *InvoiceHTLC, updater InvoiceUpdater) error {
|
||||
|
||||
setID := htlc.AMP.Record.SetID()
|
||||
|
||||
// Next update the main AMP meta-data to indicate that this HTLC set
|
||||
// has been fully settled.
|
||||
newAmpState, err := getUpdatedInvoiceAmpState(
|
||||
invoice, setID, circuitKey, HtlcStateSettled, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AMPState[setID] = newAmpState
|
||||
|
||||
// Mark the updates as needing to be written to disk.
|
||||
return updater.UpdateAmpState(setID, newAmpState, circuitKey)
|
||||
}
|
||||
|
||||
// UpdateInvoice fetches the invoice, obtains the update descriptor from the
|
||||
// callback and applies the updates in a single db transaction.
|
||||
func UpdateInvoice(hash *lntypes.Hash, invoice *Invoice,
|
||||
updateTime time.Time, callback InvoiceUpdateCallback,
|
||||
updater InvoiceUpdater) (*Invoice, error) {
|
||||
|
||||
// Create deep copy to prevent any accidental modification in the
|
||||
// callback.
|
||||
invoiceCopy, err := CopyInvoice(invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Call the callback and obtain the update descriptor.
|
||||
update, err := callback(invoiceCopy)
|
||||
if err != nil {
|
||||
return invoice, err
|
||||
}
|
||||
|
||||
// If there is nothing to update, return early.
|
||||
if update == nil {
|
||||
return invoice, nil
|
||||
}
|
||||
|
||||
switch update.UpdateType {
|
||||
case CancelHTLCsUpdate:
|
||||
err := cancelHTLCs(invoice, updateTime, update, updater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case AddHTLCsUpdate:
|
||||
err := addHTLCs(invoice, hash, updateTime, update, updater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case SettleHodlInvoiceUpdate:
|
||||
err := settleHodlInvoice(
|
||||
invoice, hash, updateTime, update.State, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case CancelInvoiceUpdate:
|
||||
err := cancelInvoice(
|
||||
invoice, hash, updateTime, update.State, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown update type: %s",
|
||||
update.UpdateType)
|
||||
}
|
||||
|
||||
if err := updater.Finalize(update.UpdateType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoice, nil
|
||||
}
|
||||
|
||||
// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc.
|
||||
//
|
||||
// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the
|
||||
// InvoiceUpdateDesc.
|
||||
func cancelHTLCs(invoice *Invoice, updateTime time.Time,
|
||||
update *InvoiceUpdateDesc, updater InvoiceUpdater) error {
|
||||
|
||||
for key := range update.CancelHtlcs {
|
||||
htlc, exists := invoice.Htlcs[key]
|
||||
|
||||
// Verify that we don't get an action for htlcs that are not
|
||||
// present on the invoice.
|
||||
if !exists {
|
||||
return fmt.Errorf("cancel of non-existent htlc")
|
||||
}
|
||||
|
||||
err := canCancelSingleHtlc(htlc, invoice.State)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = resolveHtlc(
|
||||
key, htlc, HtlcStateCanceled, updateTime,
|
||||
updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Tally this into the set of HTLCs that need to be updated on
|
||||
// disk, but once again, only if this is an AMP invoice.
|
||||
if invoice.IsAMP() {
|
||||
err := cancelHtlcsAmp(invoice, key, htlc, updater)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc.
|
||||
func addHTLCs(invoice *Invoice, hash *lntypes.Hash, updateTime time.Time,
|
||||
update *InvoiceUpdateDesc, updater InvoiceUpdater) error {
|
||||
|
||||
var setID *[32]byte
|
||||
invoiceIsAMP := invoice.IsAMP()
|
||||
if invoiceIsAMP && update.State != nil {
|
||||
setID = update.State.SetID
|
||||
}
|
||||
|
||||
for key, htlcUpdate := range update.AddHtlcs {
|
||||
if _, exists := invoice.Htlcs[key]; exists {
|
||||
return fmt.Errorf("duplicate add of htlc %v", key)
|
||||
}
|
||||
|
||||
// Force caller to supply htlc without custom records in a
|
||||
// consistent way.
|
||||
if htlcUpdate.CustomRecords == nil {
|
||||
return errors.New("nil custom records map")
|
||||
}
|
||||
|
||||
htlc := &InvoiceHTLC{
|
||||
Amt: htlcUpdate.Amt,
|
||||
MppTotalAmt: htlcUpdate.MppTotalAmt,
|
||||
Expiry: htlcUpdate.Expiry,
|
||||
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
||||
AcceptTime: updateTime,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: htlcUpdate.CustomRecords,
|
||||
}
|
||||
|
||||
if invoiceIsAMP {
|
||||
if htlcUpdate.AMP == nil {
|
||||
return fmt.Errorf("unable to add htlc "+
|
||||
"without AMP data to AMP invoice(%v)",
|
||||
invoice.AddIndex)
|
||||
}
|
||||
|
||||
htlc.AMP = htlcUpdate.AMP.Copy()
|
||||
}
|
||||
|
||||
if err := updater.AddHtlc(key, htlc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.Htlcs[key] = htlc
|
||||
|
||||
// Collect the set of new HTLCs so we can write them properly
|
||||
// below, but only if this is an AMP invoice.
|
||||
if invoiceIsAMP {
|
||||
err := acceptHtlcsAmp(
|
||||
invoice, htlcUpdate.AMP.Record.SetID(), key,
|
||||
htlc, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the set of accepted HTLCs should be fully
|
||||
// populated with added HTLCs or removed of canceled ones. Update
|
||||
// invoice state if the update descriptor indicates an invoice state
|
||||
// change, which depends on having an accurate view of the accepted
|
||||
// HTLCs.
|
||||
if update.State != nil {
|
||||
newState, err := getUpdatedInvoiceState(
|
||||
invoice, hash, *update.State,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this isn't an AMP invoice, then we'll go ahead and update
|
||||
// the invoice state directly here. For AMP invoices, we instead
|
||||
// will keep the top-level invoice open, and update the state of
|
||||
// each _htlc set_ instead. However, we'll allow the invoice to
|
||||
// transition to the cancelled state regardless.
|
||||
if !invoiceIsAMP || *newState == ContractCanceled {
|
||||
err := updater.UpdateInvoiceState(*newState, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.State = *newState
|
||||
}
|
||||
}
|
||||
|
||||
// The set of HTLC pre-images will only be set if we were actually able
|
||||
// to reconstruct all the AMP pre-images.
|
||||
var settleEligibleAMP bool
|
||||
if update.State != nil {
|
||||
settleEligibleAMP = len(update.State.HTLCPreimages) != 0
|
||||
}
|
||||
|
||||
// With any invoice level state transitions recorded, we'll now
|
||||
// finalize the process by updating the state transitions for
|
||||
// individual HTLCs
|
||||
var amtPaid lnwire.MilliSatoshi
|
||||
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
// Set the HTLC preimage for any AMP HTLCs.
|
||||
if setID != nil && update.State != nil {
|
||||
preimage, ok := update.State.HTLCPreimages[key]
|
||||
switch {
|
||||
// If we don't already have a preimage for this HTLC, we
|
||||
// can set it now.
|
||||
case ok && htlc.AMP.Preimage == nil:
|
||||
err := updater.AddAmpHtlcPreimage(
|
||||
htlc.AMP.Record.SetID(), key, preimage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
htlc.AMP.Preimage = &preimage
|
||||
|
||||
// Otherwise, prevent over-writing an existing
|
||||
// preimage. Ignore the case where the preimage is
|
||||
// identical.
|
||||
case ok && *htlc.AMP.Preimage != preimage:
|
||||
return ErrHTLCPreimageAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// If we have all the pre-images for an AMP invoice, then we'll
|
||||
// act as if we're able to settle the entire invoice. We need
|
||||
// to do this since it's possible for us to settle AMP invoices
|
||||
// while the contract state (on disk) is still in the accept
|
||||
// state.
|
||||
htlcContextState := invoice.State
|
||||
if settleEligibleAMP {
|
||||
htlcContextState = ContractSettled
|
||||
}
|
||||
htlcStateChanged, htlcState, err := getUpdatedHtlcState(
|
||||
htlc, htlcContextState, setID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if htlcStateChanged {
|
||||
err = resolveHtlc(
|
||||
key, htlc, htlcState, updateTime, updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
htlcSettled := htlcStateChanged &&
|
||||
htlcState == HtlcStateSettled
|
||||
|
||||
// If the HTLC has being settled for the first time, and this
|
||||
// is an AMP invoice, then we'll need to update some additional
|
||||
// meta data state.
|
||||
if htlcSettled && invoiceIsAMP {
|
||||
err = settleHtlcsAmp(invoice, key, htlc, updater)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
accepted := htlc.State == HtlcStateAccepted
|
||||
settled := htlc.State == HtlcStateSettled
|
||||
invoiceStateReady := accepted || settled
|
||||
|
||||
if !invoiceIsAMP {
|
||||
// Update the running amount paid to this invoice. We
|
||||
// don't include accepted htlcs when the invoice is
|
||||
// still open.
|
||||
if invoice.State != ContractOpen &&
|
||||
invoiceStateReady {
|
||||
|
||||
amtPaid += htlc.Amt
|
||||
}
|
||||
} else {
|
||||
// For AMP invoices, since we won't always be reading
|
||||
// out the total invoice set each time, we'll instead
|
||||
// accumulate newly added invoices to the total amount
|
||||
// paid.
|
||||
if _, ok := update.AddHtlcs[key]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the running amount paid to this invoice. AMP
|
||||
// invoices never go to the settled state, so if it's
|
||||
// open, then we tally the HTLC.
|
||||
if invoice.State == ContractOpen &&
|
||||
invoiceStateReady {
|
||||
|
||||
amtPaid += htlc.Amt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For non-AMP invoices we recalculate the amount paid from scratch
|
||||
// each time, while for AMP invoices, we'll accumulate only based on
|
||||
// newly added HTLCs.
|
||||
if invoiceIsAMP {
|
||||
amtPaid += invoice.AmtPaid
|
||||
}
|
||||
|
||||
return updateInvoiceAmtPaid(invoice, amtPaid, updater)
|
||||
}
|
||||
|
||||
func resolveHtlc(circuitKey models.CircuitKey, htlc *InvoiceHTLC,
|
||||
state HtlcState, resolveTime time.Time,
|
||||
updater InvoiceUpdater) error {
|
||||
|
||||
err := updater.ResolveHtlc(circuitKey, state, resolveTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
htlc.State = state
|
||||
htlc.ResolveTime = resolveTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateInvoiceAmtPaid(invoice *Invoice, amt lnwire.MilliSatoshi,
|
||||
updater InvoiceUpdater) error {
|
||||
|
||||
err := updater.UpdateInvoiceAmtPaid(amt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.AmtPaid = amt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleHodlInvoice marks a hodl invoice as settled.
|
||||
//
|
||||
// NOTE: Currently it is not possible to have HODL AMP invoices.
|
||||
func settleHodlInvoice(invoice *Invoice, hash *lntypes.Hash,
|
||||
updateTime time.Time, update *InvoiceStateUpdateDesc,
|
||||
updater InvoiceUpdater) error {
|
||||
|
||||
if !invoice.HodlInvoice {
|
||||
return fmt.Errorf("unable to settle hodl invoice: %v is "+
|
||||
"not a hodl invoice", invoice.AddIndex)
|
||||
}
|
||||
|
||||
// TODO(positiveblue): because NewState can only be ContractSettled we
|
||||
// can remove it from the API and set it here directly.
|
||||
switch {
|
||||
case update == nil:
|
||||
fallthrough
|
||||
|
||||
case update.NewState != ContractSettled:
|
||||
return fmt.Errorf("unable to settle hodl invoice: "+
|
||||
"not valid InvoiceUpdateDesc.State: %v", update)
|
||||
|
||||
case update.Preimage == nil:
|
||||
return fmt.Errorf("unable to settle hodl invoice: " +
|
||||
"preimage is nil")
|
||||
}
|
||||
|
||||
newState, err := getUpdatedInvoiceState(
|
||||
invoice, hash, *update,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newState == nil || *newState != ContractSettled {
|
||||
return fmt.Errorf("unable to settle hodl invoice: "+
|
||||
"new computed state is not settled: %s", newState)
|
||||
}
|
||||
|
||||
err = updater.UpdateInvoiceState(
|
||||
ContractSettled, update.Preimage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.State = ContractSettled
|
||||
invoice.Terms.PaymentPreimage = update.Preimage
|
||||
|
||||
// TODO(positiveblue): this logic can be further simplified.
|
||||
var amtPaid lnwire.MilliSatoshi
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
settled, _, err := getUpdatedHtlcState(
|
||||
htlc, ContractSettled, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settled {
|
||||
err = resolveHtlc(
|
||||
key, htlc, HtlcStateSettled, updateTime,
|
||||
updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amtPaid += htlc.Amt
|
||||
}
|
||||
}
|
||||
|
||||
return updateInvoiceAmtPaid(invoice, amtPaid, updater)
|
||||
}
|
||||
|
||||
// cancelInvoice attempts to cancel the given invoice. That includes changing
|
||||
// the invoice state and the state of any relevant HTLC.
|
||||
func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
|
||||
updateTime time.Time, update *InvoiceStateUpdateDesc,
|
||||
updater InvoiceUpdater) error {
|
||||
|
||||
switch {
|
||||
case update == nil:
|
||||
fallthrough
|
||||
|
||||
case update.NewState != ContractCanceled:
|
||||
return fmt.Errorf("unable to cancel invoice: "+
|
||||
"InvoiceUpdateDesc.State not valid: %v", update)
|
||||
}
|
||||
|
||||
var (
|
||||
setID *[32]byte
|
||||
invoiceIsAMP bool
|
||||
)
|
||||
|
||||
invoiceIsAMP = invoice.IsAMP()
|
||||
if invoiceIsAMP {
|
||||
setID = update.SetID
|
||||
}
|
||||
|
||||
newState, err := getUpdatedInvoiceState(invoice, hash, *update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newState == nil || *newState != ContractCanceled {
|
||||
return fmt.Errorf("unable to cancel invoice(%v): new "+
|
||||
"computed state is not canceled: %s", invoice.AddIndex,
|
||||
newState)
|
||||
}
|
||||
|
||||
err = updater.UpdateInvoiceState(ContractCanceled, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.State = ContractCanceled
|
||||
|
||||
for key, htlc := range invoice.Htlcs {
|
||||
canceled, _, err := getUpdatedHtlcState(
|
||||
htlc, ContractCanceled, setID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if canceled {
|
||||
err = resolveHtlc(
|
||||
key, htlc, HtlcStateCanceled, updateTime,
|
||||
updater,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUpdatedInvoiceState validates and processes an invoice state update. The
|
||||
// new state to transition to is returned, so the caller is able to select
|
||||
// exactly how the invoice state is updated. Note that for AMP invoices this
|
||||
// function is only used to validate the state transition if we're cancelling
|
||||
// the invoice.
|
||||
func getUpdatedInvoiceState(invoice *Invoice, hash *lntypes.Hash,
|
||||
update InvoiceStateUpdateDesc) (*ContractState, error) {
|
||||
|
||||
// Returning to open is never allowed from any state.
|
||||
if update.NewState == ContractOpen {
|
||||
return nil, 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 nil, 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 == ContractCanceled {
|
||||
return &update.NewState, nil
|
||||
}
|
||||
|
||||
// Sanity check that the user isn't trying to settle or accept a
|
||||
// non-existent HTLC set.
|
||||
set := invoice.HTLCSet(update.SetID, HtlcStateAccepted)
|
||||
if len(set) == 0 {
|
||||
return nil, ErrEmptyHTLCSet
|
||||
}
|
||||
|
||||
// For AMP invoices, there are no invoice-level preimage checks.
|
||||
// However, we still sanity check that we aren't trying to
|
||||
// settle an AMP invoice with a preimage.
|
||||
if update.SetID != nil {
|
||||
if update.Preimage != nil {
|
||||
return nil, errors.New("AMP set cannot have " +
|
||||
"preimage")
|
||||
}
|
||||
|
||||
return &update.NewState, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
// If an invoice-level preimage was supplied, but the InvoiceRef
|
||||
// doesn't specify a hash (e.g. AMP invoices) we fail.
|
||||
case update.Preimage != nil && hash == nil:
|
||||
return nil, ErrUnexpectedInvoicePreimage
|
||||
|
||||
// Validate the supplied preimage for non-AMP invoices.
|
||||
case update.Preimage != nil:
|
||||
if update.Preimage.Hash() != *hash {
|
||||
return nil, ErrInvoicePreimageMismatch
|
||||
}
|
||||
|
||||
// Permit non-AMP invoices to be accepted without knowing the
|
||||
// preimage. When trying to settle we'll have to pass through
|
||||
// the above check in order to not hit the one below.
|
||||
case update.NewState == ContractAccepted:
|
||||
|
||||
// Fail if we still don't have a preimage when transitioning to
|
||||
// settle the non-AMP invoice.
|
||||
case update.NewState == ContractSettled &&
|
||||
invoice.Terms.PaymentPreimage == nil:
|
||||
|
||||
return nil, errors.New("unknown preimage")
|
||||
}
|
||||
|
||||
return &update.NewState, nil
|
||||
|
||||
// Once settled, we are in a terminal state.
|
||||
case ContractSettled:
|
||||
return nil, ErrInvoiceAlreadySettled
|
||||
|
||||
// Once canceled, we are in a terminal state.
|
||||
case ContractCanceled:
|
||||
return nil, ErrInvoiceAlreadyCanceled
|
||||
|
||||
default:
|
||||
return nil, errors.New("unknown state transition")
|
||||
}
|
||||
}
|
||||
|
||||
// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without
|
||||
// applying it), given the new state, and the amount of the HTLC that is
|
||||
// being updated.
|
||||
func getUpdatedInvoiceAmpState(invoice *Invoice, setID SetID,
|
||||
circuitKey models.CircuitKey, state HtlcState,
|
||||
amt lnwire.MilliSatoshi) (InvoiceStateAMP, error) {
|
||||
|
||||
// Retrieve the AMP state for this set ID.
|
||||
ampState, ok := invoice.AMPState[setID]
|
||||
|
||||
// If the state is accepted then we may need to create a new entry for
|
||||
// this set ID, otherwise we expect that the entry already exists and
|
||||
// we can update it.
|
||||
if !ok && state != HtlcStateAccepted {
|
||||
return InvoiceStateAMP{},
|
||||
fmt.Errorf("unable to update AMP state for setID=%x ",
|
||||
setID)
|
||||
}
|
||||
|
||||
switch state {
|
||||
case HtlcStateAccepted:
|
||||
if !ok {
|
||||
// If an entry for this set ID doesn't already exist,
|
||||
// then we'll need to create it.
|
||||
ampState = InvoiceStateAMP{
|
||||
State: HtlcStateAccepted,
|
||||
InvoiceKeys: make(
|
||||
map[models.CircuitKey]struct{},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
ampState.AmtPaid += amt
|
||||
|
||||
case HtlcStateCanceled:
|
||||
ampState.State = HtlcStateCanceled
|
||||
ampState.AmtPaid -= amt
|
||||
|
||||
case HtlcStateSettled:
|
||||
ampState.State = HtlcStateSettled
|
||||
}
|
||||
|
||||
ampState.InvoiceKeys[circuitKey] = struct{}{}
|
||||
|
||||
return ampState, nil
|
||||
}
|
||||
|
||||
// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is
|
||||
// returned, then the HTLC can be cancelled.
|
||||
func canCancelSingleHtlc(htlc *InvoiceHTLC,
|
||||
invoiceState ContractState) error {
|
||||
|
||||
// It is only possible to cancel individual htlcs on an open invoice.
|
||||
if invoiceState != ContractOpen {
|
||||
return fmt.Errorf("htlc canceled on invoice in state %v",
|
||||
invoiceState)
|
||||
}
|
||||
|
||||
// It is only possible if the htlc is still pending.
|
||||
if htlc.State != HtlcStateAccepted {
|
||||
return fmt.Errorf("htlc canceled in state %v", htlc.State)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUpdatedHtlcState aligns the state of an htlc with the given invoice state.
|
||||
// A boolean indicating whether the HTLCs state need to be updated, along with
|
||||
// the new state (or old state if no change is needed) is returned.
|
||||
func getUpdatedHtlcState(htlc *InvoiceHTLC,
|
||||
invoiceState ContractState, setID *[32]byte) (
|
||||
bool, HtlcState, error) {
|
||||
|
||||
trySettle := func(persist bool) (bool, HtlcState, error) {
|
||||
if htlc.State != HtlcStateAccepted {
|
||||
return false, htlc.State, nil
|
||||
}
|
||||
|
||||
// Settle the HTLC if it matches the settled set id. If
|
||||
// there're other HTLCs with distinct setIDs, then we'll leave
|
||||
// them, as they may eventually be settled as we permit
|
||||
// multiple settles to a single pay_addr for AMP.
|
||||
settled := false
|
||||
if htlc.IsInHTLCSet(setID) {
|
||||
// Non-AMP HTLCs can be settled immediately since we
|
||||
// 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.
|
||||
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 point, 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 false, htlc.State,
|
||||
ErrHTLCPreimageMissing
|
||||
|
||||
// Fail if the accepted AMP HTLC has an invalid
|
||||
// preimage.
|
||||
case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash):
|
||||
return false, htlc.State,
|
||||
ErrHTLCPreimageMismatch
|
||||
}
|
||||
|
||||
settled = true
|
||||
}
|
||||
|
||||
// Only persist the changes if the invoice is moving to the
|
||||
// settled state, and we're actually updating the state to
|
||||
// settled.
|
||||
newState := htlc.State
|
||||
if settled {
|
||||
newState = HtlcStateSettled
|
||||
}
|
||||
|
||||
return persist && settled, newState, nil
|
||||
}
|
||||
|
||||
if invoiceState == ContractSettled {
|
||||
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
|
||||
// this will be a NOP, but for AMP HTLCs this asserts that we
|
||||
// have a valid hash/preimage pair. Passing true permits the
|
||||
// method to update the HTLC to HtlcStateSettled.
|
||||
return trySettle(true)
|
||||
}
|
||||
|
||||
// We should never find a settled HTLC on an invoice that isn't in
|
||||
// ContractSettled.
|
||||
if htlc.State == HtlcStateSettled {
|
||||
return false, htlc.State, ErrHTLCAlreadySettled
|
||||
}
|
||||
|
||||
switch invoiceState {
|
||||
case ContractCanceled:
|
||||
htlcAlreadyCanceled := htlc.State == HtlcStateCanceled
|
||||
return !htlcAlreadyCanceled, HtlcStateCanceled, nil
|
||||
|
||||
// TODO(roasbeef): never fully passed thru now?
|
||||
case ContractAccepted:
|
||||
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
|
||||
// this will be a NOP, but for AMP HTLCs this asserts that we
|
||||
// have a valid hash/preimage pair. Passing false prevents the
|
||||
// method from putting the HTLC in HtlcStateSettled, leaving it
|
||||
// in HtlcStateAccepted.
|
||||
return trySettle(false)
|
||||
|
||||
case ContractOpen:
|
||||
return false, htlc.State, nil
|
||||
|
||||
default:
|
||||
return false, htlc.State, errors.New("unknown state transition")
|
||||
}
|
||||
}
|
687
invoices/update_invoice_test.go
Normal file
687
invoices/update_invoice_test.go
Normal file
|
@ -0,0 +1,687 @@
|
|||
package invoices
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
testNow := time.Now()
|
||||
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,
|
||||
},
|
||||
{
|
||||
// With the newer AMP logic, this is now valid, as we
|
||||
// want to be able to accept multiple settle attempts
|
||||
// to a given pay_addr. In this case, the HTLC should
|
||||
// remain in the accepted state.
|
||||
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: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
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, testNow)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateHTLC(t *testing.T, test updateHTLCTest, now time.Time) {
|
||||
htlc := test.input.Copy()
|
||||
stateChanged, state, err := getUpdatedHtlcState(
|
||||
htlc, test.invState, test.setID,
|
||||
)
|
||||
if stateChanged {
|
||||
htlc.State = state
|
||||
htlc.ResolveTime = now
|
||||
}
|
||||
|
||||
require.Equal(t, test.expErr, err)
|
||||
require.Equal(t, test.output, *htlc)
|
||||
}
|
Loading…
Add table
Reference in a new issue