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 authored and GitHub committed 2024-02-27 16:50:02 -08:00
commit 72764b1473
12 files changed
+4793 -4245

No files matched your search

+1 -3158
View File
File diff suppressed because it is too large. Load diff
+344 -927
View File
File diff suppressed because it is too large. Load diff
@@ -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)
+3 -10
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
+7 -31
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=
+36
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
}
+223 -87
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)
File diff suppressed because it is too large. Load diff
+11
View File
@@ -0,0 +1,11 @@
package invoices
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
)
func TestMain(m *testing.M) {
kvdb.RunTests(m)
}
+6 -32
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
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")
}
}
+687
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)
}