From 198f9323ccb5b82685c4b21f7e789a246482b6f8 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Wed, 19 Dec 2018 21:51:29 -0800 Subject: [PATCH 1/2] lncfg/address: add LN address parsing --- lncfg/address.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/lncfg/address.go b/lncfg/address.go index 266e3714..ccf3e953 100644 --- a/lncfg/address.go +++ b/lncfg/address.go @@ -2,12 +2,15 @@ package lncfg import ( "crypto/tls" + "encoding/hex" "fmt" "net" "strconv" "strings" "time" + "github.com/btcsuite/btcd/btcec" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tor" ) @@ -162,6 +165,59 @@ func ParseAddressString(strAddress string, defaultPort string, } } +// ParseLNAddressString converts a string of the form @ into an +// lnwire.NetAddress. The must be presented in hex, and result in a +// 33-byte, compressed public key that lies on the secp256k1 curve. The +// may be any address supported by ParseAddressString. If no port is specified, +// the defaultPort will be used. Any tcp addresses that need resolving will be +// resolved using the custom tcpResolver. +func ParseLNAddressString(strAddress string, defaultPort string, + tcpResolver tcpResolver) (*lnwire.NetAddress, error) { + + // Split the address string around the @ sign. + parts := strings.Split(strAddress, "@") + + // The string is malformed if there are not exactly two parts. + if len(parts) != 2 { + return nil, fmt.Errorf("invalid lightning address %s: "+ + "must be of the form @", strAddress) + } + + // Now, take the first portion as the hex pubkey, and the latter as the + // address string. + parsedPubKey, parsedAddr := parts[0], parts[1] + + // Decode the hex pubkey to get the raw compressed pubkey bytes. + pubKeyBytes, err := hex.DecodeString(parsedPubKey) + if err != nil { + return nil, fmt.Errorf("invalid lightning address pubkey: %v", err) + } + + // The compressed pubkey should have a length of exactly 33 bytes. + if len(pubKeyBytes) != 33 { + return nil, fmt.Errorf("invalid lightning address pubkey: "+ + "length must be 33 bytes, found %d", len(pubKeyBytes)) + } + + // Parse the pubkey bytes to verify that it corresponds to valid public + // key on the secp256k1 curve. + pubKey, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256()) + if err != nil { + return nil, fmt.Errorf("invalid lightning address pubkey: %v", err) + } + + // Finally, parse the address string using our generic address parser. + addr, err := ParseAddressString(parsedAddr, defaultPort, tcpResolver) + if err != nil { + return nil, fmt.Errorf("invalid lightning address address: %v", err) + } + + return &lnwire.NetAddress{ + IdentityKey: pubKey, + Address: addr, + }, nil +} + // verifyPort makes sure that an address string has both a host and a port. If // there is no port found, the default port is appended. If the address is just // a port, then we'll assume that the user is using the short cut to specify a From 381b5275c46563bd150b44919c571580112fb0a0 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Wed, 9 Jan 2019 16:30:21 -0800 Subject: [PATCH 2/2] lncfg/address_test: test LNAddress parsing, refactor addr tests --- lncfg/address_test.go | 229 +++++++++++++++++++++++++++++++++--------- 1 file changed, 182 insertions(+), 47 deletions(-) diff --git a/lncfg/address_test.go b/lncfg/address_test.go index 92ed488d..c35d7199 100644 --- a/lncfg/address_test.go +++ b/lncfg/address_test.go @@ -3,8 +3,12 @@ package lncfg import ( + "bytes" + "encoding/hex" "net" "testing" + + "github.com/btcsuite/btcd/btcec" ) // addressTest defines a test vector for an address that contains the non- @@ -78,55 +82,186 @@ var ( // normalized correctly. func TestAddresses(t *testing.T) { // First, test all correct addresses. - for i, testVector := range addressTestVectors { - addr := []string{testVector.address} - normalized, err := NormalizeAddresses( - addr, defaultTestPort, net.ResolveTCPAddr, - ) - if err != nil { - t.Fatalf("#%v: unable to normalize address %s: %v", - i, testVector.address, err) - } - netAddr := normalized[0] - if err != nil { - t.Fatalf("#%v: unable to split normalized address: %v", i, err) - } - if netAddr.Network() != testVector.expectedNetwork || - netAddr.String() != testVector.expectedAddress { - t.Fatalf("#%v: mismatched address: expected %s://%s, "+ - "got %s://%s", - i, testVector.expectedNetwork, - testVector.expectedAddress, - netAddr.Network(), netAddr.String(), - ) - } - isAddrLoopback := IsLoopback(normalized[0].String()) - if testVector.isLoopback != isAddrLoopback { - t.Fatalf("#%v: mismatched loopback detection: expected "+ - "%v, got %v for addr %s", - i, testVector.isLoopback, isAddrLoopback, - testVector.address, - ) - } - isAddrUnix := IsUnix(normalized[0]) - if testVector.isUnix != isAddrUnix { - t.Fatalf("#%v: mismatched unix detection: expected "+ - "%v, got %v for addr %s", - i, testVector.isUnix, isAddrUnix, - testVector.address, - ) - } + for _, test := range addressTestVectors { + t.Run(test.address, func(t *testing.T) { + testAddress(t, test) + }) } // Finally, test invalid inputs to see if they are handled correctly. - for _, testVector := range invalidTestVectors { - addr := []string{testVector} - _, err := NormalizeAddresses( - addr, defaultTestPort, net.ResolveTCPAddr, - ) - if err == nil { - - t.Fatalf("expected error when parsing %v", testVector) - } + for _, invalidAddr := range invalidTestVectors { + t.Run(invalidAddr, func(t *testing.T) { + testInvalidAddress(t, invalidAddr) + }) + } +} + +// testAddress parses an address from its string representation, and +// asserts that the parsed net.Addr is correct against the given test case. +func testAddress(t *testing.T, test addressTest) { + addr := []string{test.address} + normalized, err := NormalizeAddresses( + addr, defaultTestPort, net.ResolveTCPAddr, + ) + if err != nil { + t.Fatalf("unable to normalize address %s: %v", + test.address, err) + } + + if len(addr) == 0 { + t.Fatalf("no normalized addresses returned") + } + + netAddr := normalized[0] + validateAddr(t, netAddr, test) +} + +// testInvalidAddress asserts that parsing the invalidAddr string using +// NormalizeAddresses results in an error. +func testInvalidAddress(t *testing.T, invalidAddr string) { + addr := []string{invalidAddr} + _, err := NormalizeAddresses( + addr, defaultTestPort, net.ResolveTCPAddr, + ) + if err == nil { + t.Fatalf("expected error when parsing %v", invalidAddr) + } +} + +var ( + pubKeyBytes = []byte{0x03, + 0xc7, 0x82, 0x86, 0xd0, 0xbf, 0xe0, 0xb2, 0x33, + 0x77, 0xe3, 0x47, 0xd7, 0xd9, 0x63, 0x94, 0x3c, + 0x4f, 0x57, 0x5d, 0xdd, 0xd5, 0x7e, 0x2f, 0x1d, + 0x52, 0xa5, 0xbe, 0x1e, 0xb7, 0xf6, 0x25, 0xa4, + } + + pubKeyHex = hex.EncodeToString(pubKeyBytes) + + pubKey, _ = btcec.ParsePubKey(pubKeyBytes, btcec.S256()) +) + +type lnAddressCase struct { + lnAddress string + expectedPubKey *btcec.PublicKey + + addressTest +} + +// lnAddressTests constructs valid LNAddress test vectors from the existing set +// of valid address test vectors. All addresses will use the same public key for +// the positive tests. +var lnAddressTests = func() []lnAddressCase { + var cases []lnAddressCase + for _, addrTest := range addressTestVectors { + cases = append(cases, lnAddressCase{ + lnAddress: pubKeyHex + "@" + addrTest.address, + expectedPubKey: pubKey, + addressTest: addrTest, + }) + } + + return cases +}() + +var invalidLNAddressTests = []string{ + "", // empty string + "@", // empty pubkey + "nonhexpubkey@", // non-hex public key + pubKeyHex[:len(pubKeyHex)-2] + "@", // pubkey too short + pubKeyHex + "aa@", // pubkey too long + pubKeyHex[:len(pubKeyHex)-1] + "7@", // pubkey not on curve + pubKeyHex + "@some string", // invalid address + pubKeyHex + "@://", // invalid address + pubKeyHex + "@21.21.21.21.21", // invalid address +} + +// TestLNAddresses performs both positive and negative tests against +// ParseLNAddressString. +func TestLNAddresses(t *testing.T) { + for _, test := range lnAddressTests { + t.Run(test.lnAddress, func(t *testing.T) { + testLNAddress(t, test) + }) + } + + for _, invalidAddr := range invalidLNAddressTests { + t.Run(invalidAddr, func(t *testing.T) { + testInvalidLNAddress(t, invalidAddr) + }) + } +} + +// testLNAddress parses an LNAddress from its string representation, and asserts +// that the parsed IdentityKey and Address are correct according to its test +// case. +func testLNAddress(t *testing.T, test lnAddressCase) { + // Parse the LNAddress using the default port and TCP resolver. + lnAddr, err := ParseLNAddressString( + test.lnAddress, defaultTestPort, net.ResolveTCPAddr, + ) + if err != nil { + t.Fatalf("unable to parse ln address: %v", err) + } + + // Assert that the public key matches the expected public key. + pkBytes := lnAddr.IdentityKey.SerializeCompressed() + if !bytes.Equal(pkBytes, pubKeyBytes) { + t.Fatalf("mismatched pubkey, want: %x, got: %v", + pubKeyBytes, pkBytes) + } + + // Assert that the address after the @ is parsed properly, as if it were + // just a standalone address parsed by ParseAddressString. + validateAddr(t, lnAddr.Address, test.addressTest) +} + +// testLNAddressCase asserts that parsing the given invalidAddr string results +// in an error when parsed with ParseLNAddressString. +func testInvalidLNAddress(t *testing.T, invalidAddr string) { + _, err := ParseLNAddressString( + invalidAddr, defaultTestPort, net.ResolveTCPAddr, + ) + if err == nil { + t.Fatalf("expected error when parsing invalid lnaddress: %v", + invalidAddr) + } +} + +// validateAddr asserts that an addr parsed by ParseAddressString matches the +// properties expected by its addressTest. In particular, it validates that the +// Network() and String() methods match the expectedNetwork and expectedAddress, +// respectively. Further, we test the IsLoopback and IsUnix detection methods +// against addr and assert that they match the expected values in the test case. +func validateAddr(t *testing.T, addr net.Addr, test addressTest) { + + t.Helper() + + // Assert that the parsed network and address match what we expect. + if addr.Network() != test.expectedNetwork || + addr.String() != test.expectedAddress { + t.Fatalf("mismatched address: expected %s://%s, "+ + "got %s://%s", + test.expectedNetwork, test.expectedAddress, + addr.Network(), addr.String(), + ) + } + + // Assert whether we expect this address to be a loopback address. + isAddrLoopback := IsLoopback(addr.String()) + if test.isLoopback != isAddrLoopback { + t.Fatalf("mismatched loopback detection: expected "+ + "%v, got %v for addr %s", + test.isLoopback, isAddrLoopback, test.address, + ) + } + + // Assert whether we expect this address to be a unix address. + isAddrUnix := IsUnix(addr) + if test.isUnix != isAddrUnix { + t.Fatalf("mismatched unix detection: expected "+ + "%v, got %v for addr %s", + test.isUnix, isAddrUnix, test.address, + ) } }