Merge pull request #8100 from bhandras/updateinvoice-refactor

channeldb: refactor `UpdateInvoice` to make it simpler to create SQL specific implementation
This commit is contained in:
Olaoluwa Osuntokun 2024-02-27 16:50:02 -08:00 committed by GitHub
commit 72764b1473
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 4793 additions and 4245 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -324,6 +324,9 @@
* [Update](https://github.com/lightningnetwork/lnd/pull/8419) the embedded
Postgres version and raise max connections.
* [Refactor UpdateInvoice](https://github.com/lightningnetwork/lnd/pull/8100) to
make it simpler to adjust code to also support SQL InvoiceDB implementation.
## Code Health
* [Remove database pointers](https://github.com/lightningnetwork/lnd/pull/8117)

13
go.mod
View File

@ -33,7 +33,7 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/jrick/logrotate v1.0.0
github.com/kkdai/bstream v1.0.0
github.com/lib/pq v1.10.3
github.com/lib/pq v1.10.4
github.com/lightninglabs/neutrino v0.16.0
github.com/lightninglabs/neutrino/cache v1.1.2
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f
@ -41,7 +41,7 @@ require (
github.com/lightningnetwork/lnd/clock v1.1.1
github.com/lightningnetwork/lnd/fn v1.0.4
github.com/lightningnetwork/lnd/healthcheck v1.2.3
github.com/lightningnetwork/lnd/kvdb v1.4.4
github.com/lightningnetwork/lnd/kvdb v1.4.5
github.com/lightningnetwork/lnd/queue v1.1.1
github.com/lightningnetwork/lnd/ticker v1.1.1
github.com/lightningnetwork/lnd/tlv v1.2.1
@ -93,9 +93,8 @@ require (
github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
github.com/fergusstrange/embedded-postgres v1.25.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
@ -120,24 +119,19 @@ require (
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/archiver/v3 v3.5.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/gomega v1.26.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
@ -154,7 +148,6 @@ require (
github.com/stretchr/objx v0.5.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect

38
go.sum
View File

@ -61,7 +61,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -176,9 +175,6 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -190,8 +186,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8=
github.com/fergusstrange/embedded-postgres v1.10.0 h1:YnwF6xAQYmKLAXXrrRx4rHDLih47YJwVPvg8jeKfdNg=
github.com/fergusstrange/embedded-postgres v1.10.0/go.mod h1:a008U8/Rws5FtIOTGYDYa7beVWsT3qVKyqExqYYjL+c=
github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0=
github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
@ -251,7 +247,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -402,14 +397,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8=
github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -424,10 +411,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/neutrino v0.16.0 h1:YNTQG32fPR/Zg0vvJVI65OBH8l3U18LSXXtX91hx0q0=
@ -446,8 +432,8 @@ github.com/lightningnetwork/lnd/fn v1.0.4 h1:n4iGRRoS+XHqNbOrsXIvweps/QfWk+moO7F
github.com/lightningnetwork/lnd/fn v1.0.4/go.mod h1:K9gbvdl5z4XmRcqWUVqvvVcuRKtmq9BNQ+cWYlk+vjw=
github.com/lightningnetwork/lnd/healthcheck v1.2.3 h1:oqhOOy8WmIEa6RBkYKC0mmYZkhl8T2kGD97n9jpML8o=
github.com/lightningnetwork/lnd/healthcheck v1.2.3/go.mod h1:eDxH3dEwV9DeBW/6inrmlVh1qBOFV0AI14EEPnGt9gc=
github.com/lightningnetwork/lnd/kvdb v1.4.4 h1:bCv63rVCvzqj1BkagN/EWTov6NDDgYEG/t0z2HepRMk=
github.com/lightningnetwork/lnd/kvdb v1.4.4/go.mod h1:9SuaIqMA9ugrVkdvgQkYXa8CAKYNYd4vsEYORP4V698=
github.com/lightningnetwork/lnd/kvdb v1.4.5 h1:wwX3hbFTsnxEIL5X2Pszq1o3Fd2OZGdyWIMr9QrMxL8=
github.com/lightningnetwork/lnd/kvdb v1.4.5/go.mod h1:oaGL6R/qwazM7hPurg8jSPYsWw3cGEOt6YJDs5TUNos=
github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI=
github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4=
github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
@ -474,8 +460,6 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
@ -493,9 +477,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M=
github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@ -521,9 +502,6 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -592,7 +570,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -606,8 +583,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw=
github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@ -674,6 +649,7 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=

View File

@ -2,9 +2,11 @@ package invoices
import (
"context"
"time"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
)
@ -162,3 +164,37 @@ type InvoiceSlice struct {
// CircuitKey is a tuple of channel ID and HTLC ID, used to uniquely identify
// HTLCs in a circuit.
type CircuitKey = models.CircuitKey
// InvoiceUpdater is an interface to abstract away the details of updating an
// invoice in the database. The methods of this interface are called during the
// in-memory update of an invoice when the database needs to be updated or the
// updated state needs to be marked as needing to be written to the database.
type InvoiceUpdater interface {
// AddHtlc adds a new htlc to the invoice.
AddHtlc(circuitKey CircuitKey, newHtlc *InvoiceHTLC) error
// ResolveHtlc marks an htlc as resolved with the given state.
ResolveHtlc(circuitKey CircuitKey, state HtlcState,
resolveTime time.Time) error
// AddAmpHtlcPreimage adds a preimage of an AMP htlc to the AMP invoice
// identified by the setID.
AddAmpHtlcPreimage(setID [32]byte, circuitKey CircuitKey,
preimage lntypes.Preimage) error
// UpdateInvoiceState updates the invoice state to the new state.
UpdateInvoiceState(newState ContractState,
preimage *lntypes.Preimage) error
// UpdateInvoiceAmtPaid updates the invoice amount paid to the new
// amount.
UpdateInvoiceAmtPaid(amtPaid lnwire.MilliSatoshi) error
// UpdateAmpState updates the state of the AMP invoice identified by
// the setID.
UpdateAmpState(setID [32]byte, newState InvoiceStateAMP,
circuitKey models.CircuitKey) error
// Finalize finalizes the update before it is written to the database.
Finalize(updateType UpdateType) error
}

View File

@ -11,6 +11,7 @@ import (
"github.com/lightningnetwork/lnd/amp"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lntest/wait"
@ -20,11 +21,115 @@ import (
"github.com/stretchr/testify/require"
)
// TestSettleInvoice tests settling of an invoice and related notifications.
func TestSettleInvoice(t *testing.T) {
// TestInvoiceRegistry is a master test which encompasses all tests using an
// InvoiceDB instance. The purpose of this test is to be able to run all tests
// with a custom DB instance, so that we can test the same logic with different
// DB implementations.
func TestInvoiceRegistry(t *testing.T) {
testList := []struct {
name string
test func(t *testing.T,
makeDB func(t *testing.T) (
invpkg.InvoiceDB, *clock.TestClock))
}{
{
name: "SettleInvoice",
test: testSettleInvoice,
},
{
name: "CancelInvoice",
test: testCancelInvoice,
},
{
name: "SettleHoldInvoice",
test: testSettleHoldInvoice,
},
{
name: "CancelHoldInvoice",
test: testCancelHoldInvoice,
},
{
name: "UnknownInvoice",
test: testUnknownInvoice,
},
{
name: "KeySend",
test: testKeySend,
},
{
name: "HoldKeysend",
test: testHoldKeysend,
},
{
name: "MppPayment",
test: testMppPayment,
},
{
name: "MppPaymentWithOverpayment",
test: testMppPaymentWithOverpayment,
},
{
name: "InvoiceExpiryWithRegistry",
test: testInvoiceExpiryWithRegistry,
},
{
name: "OldInvoiceRemovalOnStart",
test: testOldInvoiceRemovalOnStart,
},
{
name: "HeightExpiryWithRegistry",
test: testHeightExpiryWithRegistry,
},
{
name: "MultipleSetHeightExpiry",
test: testMultipleSetHeightExpiry,
},
{
name: "SettleInvoicePaymentAddrRequired",
test: testSettleInvoicePaymentAddrRequired,
},
{
name: "SettleInvoicePaymentAddrRequiredOptionalGrace",
test: testSettleInvoicePaymentAddrRequiredOptionalGrace,
},
{
name: "AMPWithoutMPPPayload",
test: testAMPWithoutMPPPayload,
},
{
name: "SpontaneousAmpPayment",
test: testSpontaneousAmpPayment,
},
}
makeKeyValueDB := func(t *testing.T) (invpkg.InvoiceDB,
*clock.TestClock) {
testClock := clock.NewTestClock(testNow)
db, err := channeldb.MakeTestInvoiceDB(
t, channeldb.OptionClock(testClock),
)
require.NoError(t, err, "unable to make test db")
return db, testClock
}
for _, test := range testList {
test := test
t.Run(test.name, func(t *testing.T) {
test.test(t, makeKeyValueDB)
})
}
}
// testSettleInvoice tests settling of an invoice and related notifications.
func testSettleInvoice(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
ctxb := context.Background()
allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0)
@ -199,7 +304,9 @@ func TestSettleInvoice(t *testing.T) {
}
}
func testCancelInvoice(t *testing.T, gc bool) {
func testCancelInvoiceImpl(t *testing.T, gc bool,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
cfg := defaultRegistryConfig()
@ -207,7 +314,7 @@ func testCancelInvoice(t *testing.T, gc bool) {
// If set to true, then also delete the invoice from the DB after
// cancellation.
cfg.GcCanceledInvoicesOnTheFly = gc
ctx := newTestContext(t, &cfg)
ctx := newTestContext(t, &cfg, makeDB)
ctxb := context.Background()
allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0)
@ -329,36 +436,37 @@ func testCancelInvoice(t *testing.T, gc bool) {
require.Equal(t, testCurrentHeight, failResolution.AcceptHeight)
}
// TestCancelInvoice tests cancellation of an invoice and related notifications.
func TestCancelInvoice(t *testing.T) {
// testCancelInvoice tests cancellation of an invoice and related notifications.
func testCancelInvoice(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
// Test cancellation both with garbage collection (meaning that canceled
// invoice will be deleted) and without (meaning it'll be kept).
t.Run("garbage collect", func(t *testing.T) {
testCancelInvoice(t, true)
testCancelInvoiceImpl(t, true, makeDB)
})
t.Run("no garbage collect", func(t *testing.T) {
testCancelInvoice(t, false)
testCancelInvoiceImpl(t, false, makeDB)
})
}
// TestSettleHoldInvoice tests settling of a hold invoice and related
// testSettleHoldInvoice tests settling of a hold invoice and related
// notifications.
func TestSettleHoldInvoice(t *testing.T) {
func testSettleHoldInvoice(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
idb, err := newTestChannelDB(t, clock.NewTestClock(time.Time{}))
if err != nil {
t.Fatal(err)
}
idb, clock := makeDB(t)
// Instantiate and start the invoice ctx.registry.
cfg := invpkg.RegistryConfig{
FinalCltvRejectDelta: testFinalCltvRejectDelta,
Clock: clock.NewTestClock(testTime),
Clock: clock,
}
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
@ -366,7 +474,7 @@ func TestSettleHoldInvoice(t *testing.T) {
)
registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg)
err = registry.Start()
err := registry.Start()
require.NoError(t, err)
defer registry.Stop()
@ -511,15 +619,15 @@ func TestSettleHoldInvoice(t *testing.T) {
)
}
// TestCancelHoldInvoice tests canceling of a hold invoice and related
// testCancelHoldInvoice tests canceling of a hold invoice and related
// notifications.
func TestCancelHoldInvoice(t *testing.T) {
func testCancelHoldInvoice(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
testClock := clock.NewTestClock(testTime)
idb, err := newTestChannelDB(t, testClock)
require.NoError(t, err)
idb, testClock := makeDB(t)
// Instantiate and start the invoice ctx.registry.
cfg := invpkg.RegistryConfig{
@ -531,7 +639,7 @@ func TestCancelHoldInvoice(t *testing.T) {
)
registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg)
err = registry.Start()
err := registry.Start()
if err != nil {
t.Fatal(err)
}
@ -587,14 +695,16 @@ func TestCancelHoldInvoice(t *testing.T) {
require.Equal(t, testCurrentHeight, failResolution.AcceptHeight)
}
// TestUnknownInvoice tests that invoice registry returns an error when the
// testUnknownInvoice tests that invoice registry returns an error when the
// invoice is unknown. This is to guard against returning a cancel htlc
// resolution for forwarded htlcs. In the link, NotifyExitHopHtlc is only called
// if we are the exit hop, but in htlcIncomingContestResolver it is called with
// forwarded htlc hashes as well.
func TestUnknownInvoice(t *testing.T) {
func testUnknownInvoice(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
// Notify arrival of a new htlc paying to this invoice. This should
// succeed.
@ -611,28 +721,32 @@ func TestUnknownInvoice(t *testing.T) {
checkFailResolution(t, resolution, invpkg.ResultInvoiceNotFound)
}
// TestKeySend tests receiving a spontaneous payment with and without keysend
// testKeySend tests receiving a spontaneous payment with and without keysend
// enabled.
func TestKeySend(t *testing.T) {
func testKeySend(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
t.Run("enabled", func(t *testing.T) {
testKeySend(t, true)
testKeySendImpl(t, true, makeDB)
})
t.Run("disabled", func(t *testing.T) {
testKeySend(t, false)
testKeySendImpl(t, false, makeDB)
})
}
// testKeySend is the inner test function that tests keysend for a particular
// enabled state on the receiver end.
func testKeySend(t *testing.T, keySendEnabled bool) {
// testKeySendImpl is the inner test function that tests keysend for a
// particular enabled state on the receiver end.
func testKeySendImpl(t *testing.T, keySendEnabled bool,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
cfg := defaultRegistryConfig()
cfg.AcceptKeySend = keySendEnabled
ctx := newTestContext(t, &cfg)
ctx := newTestContext(t, &cfg, makeDB)
allSubscriptions, err := ctx.registry.SubscribeNotifications(
context.Background(), 0, 0,
@ -742,20 +856,24 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
checkSubscription()
}
// TestHoldKeysend tests receiving a spontaneous payment that is held.
func TestHoldKeysend(t *testing.T) {
// testHoldKeysend tests receiving a spontaneous payment that is held.
func testHoldKeysend(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
t.Run("settle", func(t *testing.T) {
testHoldKeysend(t, false)
testHoldKeysendImpl(t, false, makeDB)
})
t.Run("timeout", func(t *testing.T) {
testHoldKeysend(t, true)
testHoldKeysendImpl(t, true, makeDB)
})
}
// testHoldKeysend is the inner test function that tests hold-keysend.
func testHoldKeysend(t *testing.T, timeoutKeysend bool) {
// testHoldKeysendImpl is the inner test function that tests hold-keysend.
func testHoldKeysendImpl(t *testing.T, timeoutKeysend bool,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
@ -764,7 +882,7 @@ func testHoldKeysend(t *testing.T, timeoutKeysend bool) {
cfg := defaultRegistryConfig()
cfg.AcceptKeySend = true
cfg.KeysendHoldTime = holdDuration
ctx := newTestContext(t, &cfg)
ctx := newTestContext(t, &cfg, makeDB)
ctxb := context.Background()
allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0)
@ -844,14 +962,16 @@ func testHoldKeysend(t *testing.T, timeoutKeysend bool) {
require.Equal(t, settledInvoice.State, invpkg.ContractSettled)
}
// TestMppPayment tests settling of an invoice with multiple partial payments.
// testMppPayment tests settling of an invoice with multiple partial payments.
// It covers the case where there is a mpp timeout before the whole invoice is
// paid and the case where the invoice is settled in time.
func TestMppPayment(t *testing.T) {
func testMppPayment(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
ctxb := context.Background()
// Add the invoice.
@ -940,15 +1060,17 @@ func TestMppPayment(t *testing.T) {
}
}
// TestMppPaymentWithOverpayment tests settling of an invoice with multiple
// testMppPaymentWithOverpayment tests settling of an invoice with multiple
// partial payments. It covers the case where the mpp overpays what is in the
// invoice.
func TestMppPaymentWithOverpayment(t *testing.T) {
func testMppPaymentWithOverpayment(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
ctxb := context.Background()
f := func(overpaymentRand uint64) bool {
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
// Add the invoice.
testInvoice := newInvoice(t, false)
@ -1017,13 +1139,13 @@ func TestMppPaymentWithOverpayment(t *testing.T) {
}
}
// Tests that invoices are canceled after expiration.
func TestInvoiceExpiryWithRegistry(t *testing.T) {
t.Parallel()
// testInvoiceExpiryWithRegistry tests that invoices are canceled after
// expiration.
func testInvoiceExpiryWithRegistry(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
testClock := clock.NewTestClock(testTime)
idb, err := newTestChannelDB(t, testClock)
require.NoError(t, err)
t.Parallel()
idb, testClock := makeDB(t)
cfg := invpkg.RegistryConfig{
FinalCltvRejectDelta: testFinalCltvRejectDelta,
@ -1119,21 +1241,20 @@ func TestInvoiceExpiryWithRegistry(t *testing.T) {
// Retrospectively check that all invoices that were expected to be
// canceled are indeed canceled.
err = wait.NoError(canceled, testTimeout)
err := wait.NoError(canceled, testTimeout)
require.NoError(t, err, "timeout checking invoice state")
// Finally stop the registry.
require.NoError(t, registry.Stop(), "failed to stop invoice registry")
}
// TestOldInvoiceRemovalOnStart tests that we'll attempt to remove old canceled
// testOldInvoiceRemovalOnStart tests that we'll attempt to remove old canceled
// invoices upon start while keeping all settled ones.
func TestOldInvoiceRemovalOnStart(t *testing.T) {
t.Parallel()
func testOldInvoiceRemovalOnStart(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
testClock := clock.NewTestClock(testTime)
idb, err := newTestChannelDB(t, testClock)
require.NoError(t, err)
t.Parallel()
idb, testClock := makeDB(t)
cfg := invpkg.RegistryConfig{
FinalCltvRejectDelta: testFinalCltvRejectDelta,
@ -1203,35 +1324,39 @@ func TestOldInvoiceRemovalOnStart(t *testing.T) {
require.Equal(t, expected, response.Invoices)
}
// TestHeightExpiryWithRegistry tests our height-based invoice expiry for
// testHeightExpiryWithRegistry tests our height-based invoice expiry for
// invoices paid with single and multiple htlcs, testing the case where the
// invoice is settled before expiry (and thus not canceled), and the case
// where the invoice is expired.
func TestHeightExpiryWithRegistry(t *testing.T) {
func testHeightExpiryWithRegistry(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
t.Run("single shot settled before expiry", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 1, true)
testHeightExpiryWithRegistryImpl(t, 1, true, makeDB)
})
t.Run("single shot expires", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 1, false)
testHeightExpiryWithRegistryImpl(t, 1, false, makeDB)
})
t.Run("mpp settled before expiry", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 2, true)
testHeightExpiryWithRegistryImpl(t, 2, true, makeDB)
})
t.Run("mpp expires", func(t *testing.T) {
testHeightExpiryWithRegistry(t, 2, false)
testHeightExpiryWithRegistryImpl(t, 2, false, makeDB)
})
}
func testHeightExpiryWithRegistry(t *testing.T, numParts int, settle bool) {
func testHeightExpiryWithRegistryImpl(t *testing.T, numParts int, settle bool,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
require.Greater(t, numParts, 0, "test requires at least one part")
@ -1337,15 +1462,17 @@ func testHeightExpiryWithRegistry(t *testing.T, numParts int, settle bool) {
"hold invoice: %v, got: %v", expectedState, inv.State)
}
// TestMultipleSetHeightExpiry pays a hold invoice with two mpp sets, testing
// testMultipleSetHeightExpiry pays a hold invoice with two mpp sets, testing
// that the invoice expiry watcher only uses the expiry height of the second,
// successful set to cancel the invoice, and does not cancel early using the
// expiry height of the first set that was canceled back due to mpp timeout.
func TestMultipleSetHeightExpiry(t *testing.T) {
func testMultipleSetHeightExpiry(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
// Add a hold invoice.
testInvoice := newInvoice(t, true)
@ -1431,13 +1558,15 @@ func TestMultipleSetHeightExpiry(t *testing.T) {
}, testTimeout, time.Millisecond*100, "invoice not canceled")
}
// TestSettleInvoicePaymentAddrRequired tests that if an incoming payment has
// testSettleInvoicePaymentAddrRequired tests that if an incoming payment has
// an invoice that requires the payment addr bit to be set, and the incoming
// payment doesn't include an mpp payload, then the payment is rejected.
func TestSettleInvoicePaymentAddrRequired(t *testing.T) {
func testSettleInvoicePaymentAddrRequired(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
ctxb := context.Background()
allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0)
@ -1519,14 +1648,16 @@ func TestSettleInvoicePaymentAddrRequired(t *testing.T) {
require.Equal(t, failResolution.Outcome, invpkg.ResultAddressMismatch)
}
// TestSettleInvoicePaymentAddrRequiredOptionalGrace tests that if an invoice
// testSettleInvoicePaymentAddrRequiredOptionalGrace tests that if an invoice
// in the database has an optional payment addr required bit set, then we'll
// still allow it to be paid by an incoming HTLC that doesn't include the MPP
// payload. This ensures we don't break payment for any invoices in the wild.
func TestSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T) {
func testSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
ctx := newTestContext(t, nil)
ctx := newTestContext(t, nil, makeDB)
ctxb := context.Background()
allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0)
@ -1633,15 +1764,17 @@ func TestSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T) {
}
}
// TestAMPWithoutMPPPayload asserts that we correctly reject an AMP HTLC that
// testAMPWithoutMPPPayload asserts that we correctly reject an AMP HTLC that
// does not include an MPP record.
func TestAMPWithoutMPPPayload(t *testing.T) {
func testAMPWithoutMPPPayload(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
cfg := defaultRegistryConfig()
cfg.AcceptAMP = true
ctx := newTestContext(t, &cfg)
ctx := newTestContext(t, &cfg, makeDB)
const (
shardAmt = lnwire.MilliSatoshi(10)
@ -1666,9 +1799,11 @@ func TestAMPWithoutMPPPayload(t *testing.T) {
checkFailResolution(t, resolution, invpkg.ResultAmpError)
}
// TestSpontaneousAmpPayment tests receiving a spontaneous AMP payment with both
// testSpontaneousAmpPayment tests receiving a spontaneous AMP payment with both
// valid and invalid reconstructions.
func TestSpontaneousAmpPayment(t *testing.T) {
func testSpontaneousAmpPayment(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
tests := []struct {
@ -1712,24 +1847,25 @@ func TestSpontaneousAmpPayment(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
testSpontaneousAmpPayment(
testSpontaneousAmpPaymentImpl(
t, test.ampEnabled, test.failReconstruction,
test.numShards,
test.numShards, makeDB,
)
})
}
}
// testSpontaneousAmpPayment runs a specific spontaneous AMP test case.
func testSpontaneousAmpPayment(
t *testing.T, ampEnabled, failReconstruction bool, numShards int) {
func testSpontaneousAmpPaymentImpl(
t *testing.T, ampEnabled, failReconstruction bool, numShards int,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
defer timeout()()
cfg := defaultRegistryConfig()
cfg.AcceptAMP = ampEnabled
ctx := newTestContext(t, &cfg)
ctx := newTestContext(t, &cfg, makeDB)
ctxb := context.Background()
allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0)

2646
invoices/invoices_test.go Normal file

File diff suppressed because it is too large Load Diff

11
invoices/setup_test.go Normal file
View File

@ -0,0 +1,11 @@
package invoices
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
)
func TestMain(m *testing.M) {
kvdb.RunTests(m)
}

View File

@ -16,7 +16,6 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lntypes"
@ -131,26 +130,8 @@ var (
testInvoiceCreationDate = testTime
)
func newTestChannelDB(t *testing.T, clock clock.Clock) (*channeldb.DB, error) {
t.Helper()
// Create channeldb for the first time.
cdb, err := channeldb.Open(
t.TempDir(), channeldb.OptionClock(clock),
)
if err != nil {
return nil, err
}
t.Cleanup(func() {
cdb.Close()
})
return cdb, nil
}
type testContext struct {
idb *channeldb.DB
idb invpkg.InvoiceDB
registry *invpkg.InvoiceRegistry
notifier *mockChainNotifier
clock *clock.TestClock
@ -166,17 +147,13 @@ func defaultRegistryConfig() invpkg.RegistryConfig {
}
func newTestContext(t *testing.T,
registryCfg *invpkg.RegistryConfig) *testContext {
registryCfg *invpkg.RegistryConfig,
makeDB func(t *testing.T) (invpkg.InvoiceDB,
*clock.TestClock)) *testContext {
t.Helper()
clock := clock.NewTestClock(testTime)
idb, err := newTestChannelDB(t, clock)
if err != nil {
t.Fatal(err)
}
idb, clock := makeDB(t)
notifier := newMockNotifier()
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
@ -192,10 +169,7 @@ func newTestContext(t *testing.T,
// Instantiate and start the invoice ctx.registry.
registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg)
err = registry.Start()
if err != nil {
t.Fatal(err)
}
require.NoError(t, registry.Start())
t.Cleanup(func() {
require.NoError(t, registry.Stop())
})

826
invoices/update_invoice.go Normal file
View File

@ -0,0 +1,826 @@
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.
//
//nolint:funlen
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")
}
}

View 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)
}