diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1d672f84b..e848640298a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.7.0] - Unreleased + +- Added: [#5537](https://github.com/ethereum/aleth/pull/5537) Creating Ethereum Node Record (ENR) at program start. + +[1.6.0]: https://github.com/ethereum/aleth/compare/v1.6.0-alpha.1...master + ## [1.6.0] - Unreleased - Added: [#5485](https://github.com/ethereum/aleth/pull/5485) aleth-bootnode now by default connects to official Ethereum bootnodes. This can be disabled with `--no-bootstrap` flag. @@ -17,4 +23,5 @@ - Fixed: [#5539](https://github.com/ethereum/aleth/pull/5539) Fix logic for determining if dao hard fork block header should be requested. - Fixed: [#5547](https://github.com/ethereum/aleth/pull/5547) Fix unnecessary slow-down of eth_flush RPC method. -[1.6.0]: https://github.com/ethereum/aleth/compare/v1.6.0-alpha.1...master \ No newline at end of file +[1.6.0]: https://github.com/ethereum/aleth/compare/v1.6.0-alpha.1...release/1.6 +[1.7.0]: https://github.com/ethereum/aleth/compare/release/1.6...master \ No newline at end of file diff --git a/libdevcore/CommonData.h b/libdevcore/CommonData.h index b31204edeb2..9c864aee35d 100644 --- a/libdevcore/CommonData.h +++ b/libdevcore/CommonData.h @@ -343,4 +343,10 @@ bool contains(std::set const& _set, V const& _v) { return _set.find(_v) != _set.end(); } + +template +bool contains(std::map const& _map, K const& _k) +{ + return _map.find(_k) != _map.end(); +} } diff --git a/libp2p/ENR.cpp b/libp2p/ENR.cpp new file mode 100644 index 00000000000..eddb2465261 --- /dev/null +++ b/libp2p/ENR.cpp @@ -0,0 +1,150 @@ +// Aleth: Ethereum C++ client, tools and libraries. +// Copyright 2019 Aleth Authors. +// Licensed under the GNU General Public License, Version 3. + +#include "ENR.h" +#include + +namespace dev +{ +namespace p2p +{ +namespace +{ +constexpr char c_keyID[] = "id"; +constexpr char c_keySec256k1[] = "secp256k1"; +constexpr char c_keyIP[] = "ip"; +constexpr char c_keyTCP[] = "tcp"; +constexpr char c_keyUDP[] = "udp"; +constexpr char c_IDV4[] = "v4"; +constexpr size_t c_ENRMaxSizeBytes = 300; + + +// Address can be either boost::asio::ip::address_v4 or boost::asio::ip::address_v6 +template +bytes addressToBytes(Address const& _address) +{ + auto const addressBytes = _address.to_bytes(); + return bytes(addressBytes.begin(), addressBytes.end()); +} +} // namespace + +ENR::ENR(RLP _rlp, VerifyFunction const& _verifyFunction) +{ + if (_rlp.data().size() > c_ENRMaxSizeBytes) + BOOST_THROW_EXCEPTION(ENRIsTooBig()); + + m_signature = _rlp[0].toBytes(RLP::VeryStrict); + + m_seq = _rlp[1].toInt(RLP::VeryStrict); + + // read key-values into vector first, to check the order + std::vector> keyValuePairs; + for (size_t i = 2; i < _rlp.itemCount(); i += 2) + { + auto const key = _rlp[i].toString(RLP::VeryStrict); + auto const value = _rlp[i + 1].data().toBytes(); + keyValuePairs.push_back({key, value}); + } + + // transfer to map, this will order them + m_map.insert(keyValuePairs.begin(), keyValuePairs.end()); + + if (!std::equal(keyValuePairs.begin(), keyValuePairs.end(), m_map.begin())) + BOOST_THROW_EXCEPTION(ENRKeysAreNotUniqueSorted()); + + if (!_verifyFunction(m_map, dev::ref(m_signature), dev::ref(content()))) + BOOST_THROW_EXCEPTION(ENRSignatureIsInvalid()); +} + +ENR::ENR(uint64_t _seq, std::map const& _keyValuePairs, + SignFunction const& _signFunction) + : m_seq{_seq}, m_map{_keyValuePairs}, m_signature{_signFunction(dev::ref(content()))} +{ +} + +bytes ENR::content() const +{ + RLPStream stream{contentRlpListItemCount()}; + streamContent(stream); + return stream.out(); +} + + +void ENR::streamRLP(RLPStream& _s) const +{ + _s.appendList(contentRlpListItemCount() + 1); + _s << m_signature; + streamContent(_s); +} + +void ENR::streamContent(RLPStream& _s) const +{ + _s << m_seq; + for (auto const& keyValue : m_map) + { + _s << keyValue.first; + _s.appendRaw(keyValue.second); + } +} + +ENR createV4ENR(Secret const& _secret, boost::asio::ip::address const& _ip, uint16_t _tcpPort, uint16_t _udpPort) +{ + ENR::SignFunction signFunction = [&_secret](bytesConstRef _data) { + // dev::sign returns 65 bytes signature containing r,s,v values + Signature s = dev::sign(_secret, sha3(_data)); + // The resulting 64-byte signature is encoded as the concatenation of the r and s signature values. + return bytes(&s[0], &s[64]); + }; + + PublicCompressed const publicKey = toPublicCompressed(_secret); + + auto const address = _ip.is_v4() ? addressToBytes(_ip.to_v4()) : addressToBytes(_ip.to_v6()); + + // Values are of different types (string, bytes, uint16_t), + // so we store them as RLP representation + std::map const keyValuePairs = {{c_keyID, rlp(c_IDV4)}, + {c_keySec256k1, rlp(publicKey.asBytes())}, {c_keyIP, rlp(address)}, + {c_keyTCP, rlp(_tcpPort)}, {c_keyUDP, rlp(_udpPort)}}; + + return ENR{0 /* sequence number */, keyValuePairs, signFunction}; +} + +ENR parseV4ENR(RLP _rlp) +{ + ENR::VerifyFunction verifyFunction = [](std::map const& _keyValuePairs, + bytesConstRef _signature, bytesConstRef _data) { + auto itID = _keyValuePairs.find(c_keyID); + if (itID == _keyValuePairs.end()) + return false; + auto const id = RLP(itID->second).toString(RLP::VeryStrict); + if (id != c_IDV4) + return false; + + auto itKey = _keyValuePairs.find(c_keySec256k1); + if (itKey == _keyValuePairs.end()) + return false; + + auto const key = RLP(itKey->second).toHash(RLP::VeryStrict); + h512 const signature{_signature}; + + return dev::verify(key, signature, sha3(_data)); + }; + + return ENR{_rlp, verifyFunction}; +} + +std::ostream& operator<<(std::ostream& _out, ENR const& _enr) +{ + _out << "[ " << toHexPrefixed(_enr.signature()) << " seq=" << _enr.sequenceNumber() << " "; + for (auto const& keyValue : _enr.keyValuePairs()) + { + _out << keyValue.first << "="; + _out << toHexPrefixed(RLP{keyValue.second}.toBytes()) << " "; + } + _out << "]"; + return _out; +} + +} // namespace p2p +} // namespace dev diff --git a/libp2p/ENR.h b/libp2p/ENR.h new file mode 100644 index 00000000000..8174e89021b --- /dev/null +++ b/libp2p/ENR.h @@ -0,0 +1,62 @@ +// Aleth: Ethereum C++ client, tools and libraries. +// Copyright 2019 Aleth Authors. +// Licensed under the GNU General Public License, Version 3. + +#pragma once + +#include "Common.h" + +namespace dev +{ +namespace p2p +{ +DEV_SIMPLE_EXCEPTION(ENRIsTooBig); +DEV_SIMPLE_EXCEPTION(ENRSignatureIsInvalid); +DEV_SIMPLE_EXCEPTION(ENRKeysAreNotUniqueSorted); + +/// Class representing Ethereum Node Record - see EIP-778 +class ENR +{ +public: + // ENR class implementation is independent of Identity Scheme. + // Identity Scheme specifics are passed to ENR as functions. + + // Sign function gets serialized ENR contents and signs it according to some Identity Scheme + using SignFunction = std::function; + // Verify function gets ENR key-value pairs, signature, and serialized content and validates the + // signature according to some Identity Scheme + using VerifyFunction = + std::function const&, bytesConstRef, bytesConstRef)>; + + // Parse from RLP with given signature verification function + ENR(RLP _rlp, VerifyFunction const& _verifyFunction); + // Create with given sign function + ENR(uint64_t _seq, std::map const& _keyValues, + SignFunction const& _signFunction); + + uint64_t sequenceNumber() const { return m_seq; } + std::map const& keyValuePairs() const { return m_map; } + bytes const& signature() const { return m_signature; } + + // Serialize to given RLP stream + void streamRLP(RLPStream& _s) const; + +private: + uint64_t m_seq = 0; + std::map m_map; + bytes m_signature; + + bytes content() const; + size_t contentRlpListItemCount() const { return m_map.size() * 2 + 1; } + void streamContent(RLPStream& _s) const; +}; + + +ENR createV4ENR(Secret const& _secret, boost::asio::ip::address const& _ip, uint16_t _tcpPort, uint16_t _udpPort); + +ENR parseV4ENR(RLP _rlp); + +std::ostream& operator<<(std::ostream& _out, ENR const& _enr); + +} +} diff --git a/libp2p/Host.cpp b/libp2p/Host.cpp index 50caf88d644..204a318cb42 100644 --- a/libp2p/Host.cpp +++ b/libp2p/Host.cpp @@ -82,7 +82,8 @@ bytes ReputationManager::data(SessionFace const& _s, string const& _sub) const return bytes(); } -Host::Host(string const& _clientVersion, KeyPair const& _alias, NetworkConfig const& _n) +Host::Host( + string const& _clientVersion, pair const& _secretAndENR, NetworkConfig const& _n) : Worker("p2p", 0), m_clientVersion(_clientVersion), m_netConfig(_n), @@ -91,15 +92,17 @@ Host::Host(string const& _clientVersion, KeyPair const& _alias, NetworkConfig co // simultaneously m_tcp4Acceptor(m_ioService), m_runTimer(m_ioService), - m_alias(_alias), + m_alias{_secretAndENR.first}, + m_enr{_secretAndENR.second}, m_lastPing(chrono::steady_clock::time_point::min()), m_capabilityHost(createCapabilityHost(*this)) { cnetnote << "Id: " << id(); + cnetnote << "ENR: " << m_enr; } -Host::Host(string const& _clientVersion, NetworkConfig const& _n, bytesConstRef _restoreNetwork): - Host(_clientVersion, networkAlias(_restoreNetwork), _n) +Host::Host(string const& _clientVersion, NetworkConfig const& _n, bytesConstRef _restoreNetwork) + : Host(_clientVersion, restoreENR(_restoreNetwork, _n), _n) { m_restoreNetwork = _restoreNetwork.toBytes(); } @@ -919,7 +922,12 @@ bytes Host::saveNetwork() const } RLPStream ret(3); - ret << dev::p2p::c_protocolVersion << m_alias.secret().ref(); + ret << dev::p2p::c_protocolVersion; + + ret.appendList(2); + ret << m_alias.secret().ref(); + m_enr.streamRLP(ret); + ret.appendList(count); if (!!count) ret.appendRaw(network.out(), count); @@ -989,13 +997,36 @@ bool Host::peerSlotsAvailable(Host::PeerSlotType _type /*= Ingress*/) return peerCount() + m_pendingPeerConns.size() < peerSlots(_type); } -KeyPair Host::networkAlias(bytesConstRef _b) +std::pair Host::restoreENR(bytesConstRef _b, NetworkConfig const& _netConfig) { RLP r(_b); + Secret secret; if (r.itemCount() == 3 && r[0].isInt() && r[0].toInt() >= 3) - return KeyPair(Secret(r[1].toBytes())); + { + if (r[1].isList()) + { + secret = Secret{r[1][0].toBytes()}; + auto enrRlp = r[1][1]; + + return make_pair(secret, parseV4ENR(enrRlp)); + } + + // Support for older format without ENR + secret = Secret{r[1].toBytes()}; + } else - return KeyPair::create(); + { + // no private key found, create new one + secret = KeyPair::create().secret(); + } + + // TODO(gumb0): update ENR in case new address given in config + // https://github.com/ethereum/aleth/issues/5551 + auto const address = _netConfig.publicIPAddress.empty() ? + bi::address{} : + bi::address::from_string(_netConfig.publicIPAddress); + return make_pair( + secret, createV4ENR(secret, address, _netConfig.listenPort, _netConfig.listenPort)); } bool Host::nodeTableHasNode(Public const& _id) const diff --git a/libp2p/Host.h b/libp2p/Host.h index 98f384eb190..f7543256c10 100644 --- a/libp2p/Host.h +++ b/libp2p/Host.h @@ -5,6 +5,7 @@ #pragma once #include "Common.h" +#include "ENR.h" #include "Network.h" #include "NodeTable.h" #include "Peer.h" @@ -123,11 +124,8 @@ class Host: public Worker /// Alternative constructor that allows providing the node key directly /// without restoring the network. - Host( - std::string const& _clientVersion, - KeyPair const& _alias, - NetworkConfig const& _n = NetworkConfig{} - ); + Host(std::string const& _clientVersion, std::pair const& _secretAndENR, + NetworkConfig const& _n = NetworkConfig{}); /// Will block on network process events. virtual ~Host(); @@ -227,6 +225,9 @@ class Host: public Worker /// Get the node information. p2p::NodeInfo nodeInfo() const { return NodeInfo(id(), (networkConfig().publicIPAddress.empty() ? m_tcpPublic.address().to_string() : networkConfig().publicIPAddress), m_tcpPublic.port(), m_clientVersion); } + /// Get Ethereum Node Record of the host + ENR enr() const { return m_enr; } + /// Apply function to each session void forEachPeer( std::string const& _capabilityName, std::function _f) const; @@ -286,8 +287,8 @@ class Host: public Worker /// Shutdown network. Not thread-safe; to be called only by worker. virtual void doneWorking(); - /// Get or create host identifier (KeyPair). - static KeyPair networkAlias(bytesConstRef _b); + /// Get or create host's Ethereum Node record. + std::pair restoreENR(bytesConstRef _b, NetworkConfig const& _networkConfig); bool nodeTableHasNode(Public const& _id) const; Node nodeFromNodeTable(Public const& _id) const; @@ -334,7 +335,9 @@ class Host: public Worker std::set m_pendingPeerConns; /// Used only by connect(Peer&) to limit concurrently connecting to same node. See connect(shared_ptrconst&). bi::tcp::endpoint m_tcpPublic; ///< Our public listening endpoint. - KeyPair m_alias; ///< Alias for network communication. Network address is k*G. k is key material. TODO: Replace KeyPair. + /// Alias for network communication. + KeyPair m_alias; + ENR m_enr; std::shared_ptr m_nodeTable; ///< Node table (uses kademlia-like discovery). mutable std::mutex x_nodeTable; std::shared_ptr nodeTable() const { Guard l(x_nodeTable); return m_nodeTable; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 78474886d74..57a3eb3f156 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -25,6 +25,7 @@ set(unittest_sources unittests/libp2p/capability.cpp unittests/libp2p/eip-8.cpp + unittests/libp2p/ENRTest.cpp unittests/libp2p/rlpx.cpp unittests/libweb3core/memorydb.cpp diff --git a/test/unittests/libp2p/ENRTest.cpp b/test/unittests/libp2p/ENRTest.cpp new file mode 100644 index 00000000000..c6f299ccc21 --- /dev/null +++ b/test/unittests/libp2p/ENRTest.cpp @@ -0,0 +1,168 @@ +// Aleth: Ethereum C++ client, tools and libraries. +// Copyright 2019 Aleth Authors. +// Licensed under the GNU General Public License, Version 3. + +#include +#include + +using namespace dev; +using namespace dev::p2p; + +namespace +{ +bytes dummySignFunction(bytesConstRef) +{ + return {}; +} +bool dummyVerifyFunction(std::map const&, bytesConstRef, bytesConstRef) +{ + return true; +}; +} // namespace + +TEST(enr, parse) +{ + // Test ENR from EIP-778 + bytes rlp = fromHex( + "f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f3" + "0813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235" + "366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f"); + ENR enr = parseV4ENR(RLP{rlp}); + + EXPECT_EQ(enr.signature(), + fromHex("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f3" + "0813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c")); + EXPECT_EQ(enr.sequenceNumber(), 1); + auto keyValuePairs = enr.keyValuePairs(); + EXPECT_EQ(RLP(keyValuePairs["id"]).toString(), "v4"); + EXPECT_EQ(RLP(keyValuePairs["ip"]).toBytes(), fromHex("7f000001")); + EXPECT_EQ(RLP(keyValuePairs["secp256k1"]).toBytes(), + fromHex("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138")); + EXPECT_EQ(RLP(keyValuePairs["udp"]).toInt(), 0x765f); +} + +TEST(enr, createAndParse) +{ + auto keyPair = KeyPair::create(); + + ENR enr1 = createV4ENR(keyPair.secret(), bi::address::from_string("127.0.0.1"), 3322, 5544); + + RLPStream s; + enr1.streamRLP(s); + bytes rlp = s.out(); + + ENR enr2 = parseV4ENR(RLP{rlp}); + + EXPECT_EQ(enr1.signature(), enr2.signature()); + EXPECT_EQ(enr1.sequenceNumber(), enr2.sequenceNumber()); + EXPECT_EQ(enr1.keyValuePairs(), enr2.keyValuePairs()); +} + +TEST(enr, parseTooBigRlp) +{ + std::map keyValuePairs = {{"key", rlp(bytes(300, 'a'))}}; + + ENR enr1{0, keyValuePairs, dummySignFunction}; + + RLPStream s; + enr1.streamRLP(s); + bytes rlp = s.out(); + + EXPECT_THROW(ENR(RLP(rlp), dummyVerifyFunction), ENRIsTooBig); +} + +TEST(enr, parseKeysNotSorted) +{ + std::vector> keyValuePairs = { + {"keyB", RLPNull}, {"keyA", RLPNull}}; + + RLPStream s((keyValuePairs.size() * 2 + 2)); + s << bytes{}; // signature + s << 0; // sequence number + for (auto const keyValue : keyValuePairs) + { + s << keyValue.first; + s.appendRaw(keyValue.second); + } + bytes rlp = s.out(); + + EXPECT_THROW(ENR(RLP(rlp), dummyVerifyFunction), ENRKeysAreNotUniqueSorted); +} + +TEST(enr, parseKeysNotUnique) +{ + std::vector> keyValuePairs = {{"key", RLPNull}, {"key", RLPNull}}; + + RLPStream s((keyValuePairs.size() * 2 + 2)); + s << bytes{}; // signature + s << 0; // sequence number + for (auto const keyValue : keyValuePairs) + { + s << keyValue.first; + s.appendRaw(keyValue.second); + } + bytes rlp = s.out(); + + EXPECT_THROW(ENR(RLP(rlp), dummyVerifyFunction), ENRKeysAreNotUniqueSorted); +} + +TEST(enr, parseInvalidSignature) +{ + auto keyPair = KeyPair::create(); + + ENR enr1 = createV4ENR(keyPair.secret(), bi::address::from_string("127.0.0.1"), 3322, 5544); + + RLPStream s; + enr1.streamRLP(s); + bytes rlp = s.out(); + + // change one byte of a signature + auto signatureOffset = RLP{rlp}[0].payload().data() - rlp.data(); + rlp[signatureOffset]++; + + EXPECT_THROW(parseV4ENR(RLP{rlp}), ENRSignatureIsInvalid); +} + +TEST(enr, parseV4WithInvalidID) +{ + std::map keyValuePairs = {{"id", rlp("v5")}}; + + ENR enr1{0, keyValuePairs, dummySignFunction}; + + RLPStream s; + enr1.streamRLP(s); + bytes rlp = s.out(); + + EXPECT_THROW(parseV4ENR(RLP{rlp}), ENRSignatureIsInvalid); +} + +TEST(enr, parseV4WithNoPublicKey) +{ + std::map keyValuePairs = {{"id", rlp("v4")}}; + + ENR enr1{0, keyValuePairs, dummySignFunction}; + + RLPStream s; + enr1.streamRLP(s); + bytes rlp = s.out(); + + EXPECT_THROW(parseV4ENR(RLP{rlp}), ENRSignatureIsInvalid); +} + +TEST(enr, createV4) +{ + auto keyPair = KeyPair::create(); + ENR enr = createV4ENR(keyPair.secret(), bi::address::from_string("127.0.0.1"), 3322, 5544); + + auto keyValuePairs = enr.keyValuePairs(); + + EXPECT_TRUE(contains(keyValuePairs, std::string("id"))); + EXPECT_EQ(keyValuePairs["id"], rlp("v4")); + EXPECT_TRUE(contains(keyValuePairs, std::string("secp256k1"))); + EXPECT_TRUE(contains(keyValuePairs, std::string("ip"))); + EXPECT_EQ(keyValuePairs["ip"], rlp(bytes{127, 0, 0, 1})); + EXPECT_TRUE(contains(keyValuePairs, std::string("tcp"))); + EXPECT_EQ(keyValuePairs["tcp"], rlp(3322)); + EXPECT_TRUE(contains(keyValuePairs, std::string("udp"))); + EXPECT_EQ(keyValuePairs["udp"], rlp(5544)); +} diff --git a/test/unittests/libp2p/eip-8.cpp b/test/unittests/libp2p/eip-8.cpp index 8a3c371d28b..8970764f32c 100644 --- a/test/unittests/libp2p/eip-8.cpp +++ b/test/unittests/libp2p/eip-8.cpp @@ -378,7 +378,8 @@ shared_ptr TestHandshake::runWithInput( }); // Spawn a client to execute the handshake. - auto host = make_shared("peer name", KeyPair(_hostAlias)); + auto host = make_shared( + "peer name", make_pair(_hostAlias, createV4ENR(_hostAlias, endpoint.address(), 0, 0))); auto client = make_shared(io); shared_ptr handshake; if (_remoteID == NodeID()) diff --git a/test/unittests/libp2p/peer.cpp b/test/unittests/libp2p/peer.cpp index 6a7a5752cae..2e232562de2 100644 --- a/test/unittests/libp2p/peer.cpp +++ b/test/unittests/libp2p/peer.cpp @@ -200,7 +200,12 @@ BOOST_AUTO_TEST_CASE(saveNodes) RLP r(firstHostNetwork); BOOST_REQUIRE(r.itemCount() == 3); BOOST_REQUIRE(r[0].toInt() == dev::p2p::c_protocolVersion); - BOOST_REQUIRE_EQUAL(r[1].toBytes().size(), 32); // secret + + BOOST_REQUIRE(r[1].isList()); + BOOST_REQUIRE(r[1].itemCount() == 2); + BOOST_REQUIRE_EQUAL(r[1][0].toBytes().size(), 32); // secret + BOOST_REQUIRE(r[1][1].isList()); // ENR + BOOST_REQUIRE(r[2].itemCount() >= c_nodes); for (auto i: r[2]) @@ -213,6 +218,23 @@ BOOST_AUTO_TEST_CASE(saveNodes) delete host; } +BOOST_AUTO_TEST_CASE(saveENR) +{ + NetworkConfig config("13.74.189.147", "", 30303, false); + Host host1("Test", config); + ENR enr1 = host1.enr(); + + bytes store(host1.saveNetwork()); + + Host host2("Test", config, bytesConstRef(&store)); + ENR enr2 = host2.enr(); + + BOOST_REQUIRE_EQUAL(enr1.sequenceNumber(), enr2.sequenceNumber()); + BOOST_REQUIRE(enr1.keyValuePairs() == enr2.keyValuePairs()); + BOOST_REQUIRE(enr1.signature() == enr2.signature()); +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(p2pPeer, TestOutputHelperFixture)