From 4207be6e50bcfdba8e30669430163c5771fdb417 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Tue, 23 May 2023 10:08:49 -0500 Subject: [PATCH 1/5] zpay32: remove unused method calls The fuzz tests call inv.MinFinalCLTVExpiry() and inv.Expiry() supposedly to ensure the invoice is well-formed. However, those methods can never panic or return errors and therefore provide no benefit for this purpose. --- zpay32/fuzz_test.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/zpay32/fuzz_test.go b/zpay32/fuzz_test.go index bb948c20f4..f98af58d4a 100644 --- a/zpay32/fuzz_test.go +++ b/zpay32/fuzz_test.go @@ -12,15 +12,7 @@ import ( func FuzzDecode(f *testing.F) { f.Fuzz(func(t *testing.T, data string) { - inv, err := Decode(data, &chaincfg.TestNet3Params) - if err != nil { - return - } - - // Call these functions as a sanity check to make sure the - // invoice is well-formed. - _ = inv.MinFinalCLTVExpiry() - _ = inv.Expiry() + _, _ = Decode(data, &chaincfg.TestNet3Params) }) } @@ -31,11 +23,6 @@ func FuzzEncode(f *testing.F) { return } - // Call these functions as a sanity check to make sure the - // invoice is well-formed. - _ = inv.MinFinalCLTVExpiry() - _ = inv.Expiry() - // Initialize the static key we will be using for this fuzz // test. testPrivKey, _ := btcec.PrivKeyFromBytes(testPrivKeyBytes) From 82753f091bcb0a0d7924c4bd0037c5690c11f17c Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Tue, 23 May 2023 10:16:34 -0500 Subject: [PATCH 2/5] zpay32: use message signer from unit tests The message signer from invoice_test.go is identical to the one created in the fuzz test. We're already using the private key from invoice_test.go, so we may as well use the complete message signer for simplicity. --- zpay32/fuzz_test.go | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/zpay32/fuzz_test.go b/zpay32/fuzz_test.go index f98af58d4a..5b78979989 100644 --- a/zpay32/fuzz_test.go +++ b/zpay32/fuzz_test.go @@ -1,13 +1,9 @@ package zpay32 import ( - "fmt" "testing" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" ) func FuzzDecode(f *testing.F) { @@ -23,26 +19,7 @@ func FuzzEncode(f *testing.F) { return } - // Initialize the static key we will be using for this fuzz - // test. - testPrivKey, _ := btcec.PrivKeyFromBytes(testPrivKeyBytes) - - // Then, initialize the testMessageSigner so we can encode out - // invoices with this private key. - testMessageSigner := MessageSigner{ - SignCompact: func(msg []byte) ([]byte, error) { - hash := chainhash.HashB(msg) - sig, err := ecdsa.SignCompact(testPrivKey, hash, - true) - if err != nil { - return nil, - fmt.Errorf("can't sign the "+ - "message: %v", err) - } - - return sig, nil - }, - } + // Re-encode the invoice using our private key from unit tests. _, err = inv.Encode(testMessageSigner) if err != nil { return From bd7ec84497cab023cdb81c2a45925a9bee9df578 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Tue, 23 May 2023 10:19:50 -0500 Subject: [PATCH 3/5] zpay32: help fuzzer generate valid encodings It is very difficult for the fuzzer to create a valid checksum for each serialized invoice, and we were therefore unable to fuzz deeper than invoice decoding. We can help the fuzzer generate valid serialized invoices by calculating and appending the checksum ourselves. We also switch to using mainnet invoices to make it easier to find valid invoices for seeding the fuzzer. We prepend the required "lnbc" prefix ourselves to further help the fuzzer generate valid invoices. --- zpay32/fuzz_test.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/zpay32/fuzz_test.go b/zpay32/fuzz_test.go index 5b78979989..696b09c3a5 100644 --- a/zpay32/fuzz_test.go +++ b/zpay32/fuzz_test.go @@ -1,6 +1,7 @@ package zpay32 import ( + "strings" "testing" "github.com/btcsuite/btcd/chaincfg" @@ -12,9 +13,44 @@ func FuzzDecode(f *testing.F) { }) } +// appendChecksum returns a string containing bech followed by its bech32 +// checksum if a checksum could be calculated. Otherwise, the function returns +// bech unchanged. +// +// This code is based on checksum calculation in zpay32/bech32.go. +func appendChecksum(bech string) string { + lower := strings.ToLower(bech) + + // The string is invalid if the last '1' is non-existent or it is the + // first character of the string (no human-readable part). + one := strings.LastIndexByte(lower, '1') + if one < 1 { + return bech + } + hrp := lower[:one] + data := lower[one+1:] + + decoded, err := toBytes(data) + if err != nil { + return bech + } + + checksum, err := toChars(bech32Checksum(hrp, decoded)) + if err != nil { + return bech + } + + return bech + checksum +} + func FuzzEncode(f *testing.F) { f.Fuzz(func(t *testing.T, data string) { - inv, err := Decode(data, &chaincfg.TestNet3Params) + // Make it easier for the fuzzer to generate valid invoice + // encodings by adding the required prefix and valid checksum. + data = "lnbc" + data + data = appendChecksum(data) + + inv, err := Decode(data, &chaincfg.MainNetParams) if err != nil { return } From e5fcc57bbc05eed3fd68cfef072c6ecf1b750e04 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Wed, 24 May 2023 09:18:02 -0500 Subject: [PATCH 4/5] docs: release note for #7723 --- docs/release-notes/release-notes-0.17.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md index ff4711e7a6..61b5d0eca7 100644 --- a/docs/release-notes/release-notes-0.17.0.md +++ b/docs/release-notes/release-notes-0.17.0.md @@ -136,6 +136,9 @@ unlock or create. * [Derandomized](https://github.com/lightningnetwork/lnd/pull/7618) the BOLT 8 fuzz tests. +* [Improved](https://github.com/lightningnetwork/lnd/pull/7723) invoice fuzz + tests. + * [Added fuzz tests](https://github.com/lightningnetwork/lnd/pull/7649) for signature parsing and conversion. From 9200abf96eabd82c7e92d49320ffdaf106b0c5ef Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Tue, 13 Jun 2023 11:46:18 -0500 Subject: [PATCH 5/5] zpay32: allow fuzzer to choose invoice net We add a parameter to select which network will be used for the fuzz tests, rather than hardcoding the network. --- zpay32/fuzz_test.go | 46 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/zpay32/fuzz_test.go b/zpay32/fuzz_test.go index 696b09c3a5..9855b8c1ae 100644 --- a/zpay32/fuzz_test.go +++ b/zpay32/fuzz_test.go @@ -7,9 +7,44 @@ import ( "github.com/btcsuite/btcd/chaincfg" ) +// getPrefixAndChainParams selects network chain parameters based on the fuzzer- +// selected input byte "net". 50% of the time mainnet is selected, while the +// other 50% of the time one of the test networks is selected. For each network +// the appropriate invoice HRP prefix is also returned, with a small chance that +// no prefix is returned, allowing the fuzzer to generate invalid prefixes too. +func getPrefixAndChainParams(net byte) (string, *chaincfg.Params) { + switch { + case net == 0x00: + return "", &chaincfg.RegressionNetParams + case net < 0x20: + return "lnbcrt", &chaincfg.RegressionNetParams + + case net == 0x20: + return "", &chaincfg.TestNet3Params + case net < 0x40: + return "lntb", &chaincfg.TestNet3Params + + case net == 0x40: + return "", &chaincfg.SimNetParams + case net < 0x60: + return "lnsb", &chaincfg.SimNetParams + + case net == 0x60: + return "", &chaincfg.SigNetParams + case net < 0x80: + return "lntbs", &chaincfg.SigNetParams + + case net == 0x80: + return "", &chaincfg.MainNetParams + default: + return "lnbc", &chaincfg.MainNetParams + } +} + func FuzzDecode(f *testing.F) { - f.Fuzz(func(t *testing.T, data string) { - _, _ = Decode(data, &chaincfg.TestNet3Params) + f.Fuzz(func(t *testing.T, net byte, data string) { + _, chainParams := getPrefixAndChainParams(net) + _, _ = Decode(data, chainParams) }) } @@ -44,13 +79,14 @@ func appendChecksum(bech string) string { } func FuzzEncode(f *testing.F) { - f.Fuzz(func(t *testing.T, data string) { + f.Fuzz(func(t *testing.T, net byte, data string) { // Make it easier for the fuzzer to generate valid invoice // encodings by adding the required prefix and valid checksum. - data = "lnbc" + data + hrpPrefix, chainParams := getPrefixAndChainParams(net) + data = hrpPrefix + data data = appendChecksum(data) - inv, err := Decode(data, &chaincfg.MainNetParams) + inv, err := Decode(data, chainParams) if err != nil { return }