diff --git a/tests/all_tests_common.nim b/tests/all_tests_common.nim index 56ad5ac01..e2e50a852 100644 --- a/tests/all_tests_common.nim +++ b/tests/all_tests_common.nim @@ -5,4 +5,5 @@ import ./common/test_envvar_serialization, ./common/test_confutils_envvar, ./common/test_protobuf_validation, + ./common/test_enr_builder, ./common/test_sqlite_migrations diff --git a/tests/common/test_enr_builder.nim b/tests/common/test_enr_builder.nim new file mode 100644 index 000000000..e9ef45520 --- /dev/null +++ b/tests/common/test_enr_builder.nim @@ -0,0 +1,133 @@ +{.used.} + +import + std/options , + stew/results, + stew/shims/net, + testutils/unittests +import + ../../waku/common/enr, + ../v2/testlib/waku2 + + +suite "nim-eth ENR - builder": + + test "Non-supported private key (ECDSA)": + ## Given + let privateKey = generateEcdsaKey() + + ## Then + expect Defect: + discard EnrBuilder.init(privateKey) + + test "Supported private key (Secp256k1)": + let + seqNum = 1u64 + privateKey = generateSecp256k1Key() + + let expectedPubKey = privateKey.getPublicKey().get().getRawBytes().get() + + ## When + var builder = EnrBuilder.init(privateKey, seqNum) + let enrRes = builder.build() + + ## Then + check enrRes.isOk() + + let record = enrRes.tryGet().toTypedRecord().get() + check: + @(record.secp256k1.get()) == expectedPubKey + + +suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports": + + test "EIP-778 test vector": + ## Given + # Test vector from EIP-778 + # See: https://eips.ethereum.org/EIPS/eip-778#test-vectors + let expectedEnr = "-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04j" & + "RzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJ" & + "c2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0x" & + "OIN1ZHCCdl8" + + let + seqNum = 1u64 + privateKey = ethSecp256k1Key("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + + enrIpAddr = ValidIpAddress.init("127.0.0.1") + enrUdpPort = Port(30303) + + ## When + var builder = EnrBuilder.init(privateKey, seqNum) + builder.withIpAddressAndPorts(ipAddr=some(enrIpAddr), udpPort=some(enrUdpPort)) + + let enrRes = builder.build() + + ## Then + check enrRes.isOk() + + let record = enrRes.tryGet().toBase64() + check: + record == expectedEnr + + test "IPv4 and TCP port": + let + seqNum = 1u64 + privateKey = generateSecp256k1Key() + + enrIpAddr = ValidIpAddress.init("127.0.0.1") + enrTcpPort = Port(30301) + + let expectedPubKey = privateKey.getPublicKey().get().getRawBytes().get() + + ## When + var builder = EnrBuilder.init(privateKey, seqNum) + builder.withIpAddressAndPorts( + ipAddr=some(enrIpAddr), + tcpPort=some(enrTcpPort), + ) + + let enrRes = builder.build() + + ## Then + check enrRes.isOk() + + let record = enrRes.tryGet().toTypedRecord().get() + check: + @(record.secp256k1.get()) == expectedPubKey + record.ip == some(enrIpAddr.address_v4) + record.tcp == some(enrTcpPort.int) + record.udp == none(int) + record.ip6 == none(array[0..15, byte]) + + test "IPv6 and UDP port": + let + seqNum = 1u64 + privateKey = generateSecp256k1Key() + + enrIpAddr = ValidIpAddress.init("::1") + enrUdpPort = Port(30301) + + let expectedPubKey = privateKey.getPublicKey().get().getRawBytes().get() + + ## When + var builder = EnrBuilder.init(privateKey, seqNum) + builder.withIpAddressAndPorts( + ipAddr=some(enrIpAddr), + udpPort=some(enrUdpPort), + ) + + let enrRes = builder.build() + + ## Then + check enrRes.isOk() + + let record = enrRes.tryGet().toTypedRecord().get() + check: + @(record.secp256k1.get()) == expectedPubKey + record.ip == none(array[0..3, byte]) + record.tcp == none(int) + record.udp == none(int) + record.ip6 == some(enrIpAddr.address_v6) + record.tcp6 == none(int) + record.udp6 == some(enrUdpPort.int) diff --git a/tests/v2/testlib/waku2.nim b/tests/v2/testlib/waku2.nim index c439bd827..e4e2fa980 100644 --- a/tests/v2/testlib/waku2.nim +++ b/tests/v2/testlib/waku2.nim @@ -2,7 +2,9 @@ import std/options, stew/byteutils, libp2p/switch, - libp2p/builders + libp2p/builders, + libp2p/crypto/crypto as libp2p_keys, + eth/keys as eth_keys import ../../../waku/v2/protocol/waku_message, ./common @@ -12,17 +14,20 @@ export switch # Switch -proc generateEcdsaKey*(): PrivateKey = - PrivateKey.random(ECDSA, rng[]).get() +proc generateEcdsaKey*(): libp2p_keys.PrivateKey = + libp2p_keys.PrivateKey.random(ECDSA, rng[]).get() -proc generateEcdsaKeyPair*(): KeyPair = - KeyPair.random(ECDSA, rng[]).get() +proc generateEcdsaKeyPair*(): libp2p_keys.KeyPair = + libp2p_keys.KeyPair.random(ECDSA, rng[]).get() -proc generateSecp256k1Key*(): PrivateKey = - PrivateKey.random(Secp256k1, rng[]).get() +proc generateSecp256k1Key*(): libp2p_keys.PrivateKey = + libp2p_keys.PrivateKey.random(Secp256k1, rng[]).get() + +proc ethSecp256k1Key*(hex: string): eth_keys.PrivateKey = + eth_keys.PrivateKey.fromHex(hex).get() -proc newTestSwitch*(key=none(PrivateKey), address=none(MultiAddress)): Switch = +proc newTestSwitch*(key=none(libp2p_keys.PrivateKey), address=none(MultiAddress)): Switch = let peerKey = key.get(generateSecp256k1Key()) let peerAddr = address.get(MultiAddress.init("/ip4/127.0.0.1/tcp/0").get()) return newStandardSwitch(some(peerKey), addrs=peerAddr) diff --git a/waku/common/enr.nim b/waku/common/enr.nim new file mode 100644 index 000000000..d2c9af103 --- /dev/null +++ b/waku/common/enr.nim @@ -0,0 +1,107 @@ +## An extension wrapper around nim-eth's ENR module + +when (NimMajor, NimMinor) < (1, 4): + {.push raises: [Defect].} +else: + {.push raises: [].} + + +import + std/options, + stew/results, + stew/shims/net, + eth/keys as eth_keys, + eth/p2p/discoveryv5/enr, + libp2p/crypto/crypto as libp2p_crypto + +export enr + + +## Builder + +type EnrBuilder* = object + seqNumber: uint64 + privateKey: eth_keys.PrivateKey + fields: seq[FieldPair] + + +proc init*(T: type EnrBuilder, key: eth_keys.PrivateKey, seqNum: uint64 = 1): EnrBuilder = + EnrBuilder( + seqNumber: seqNum, + privateKey: key, + fields: newSeq[FieldPair]() + ) + +proc init*(T: type EnrBuilder, key: libp2p_crypto.PrivateKey, seqNum: uint64 = 1): EnrBuilder = + # TODO: Inconvenient runtime assertion. Move this assertion to compile time + if key.scheme != PKScheme.Secp256k1: + raise newException(Defect, "invalid private key scheme") + + let + bytes = key.getRawBytes().expect("Private key is valid") + privateKey = eth_keys.PrivateKey.fromRaw(bytes).expect("Raw private key is of valid length") + + EnrBuilder.init(key=privateKey, seqNum=seqNum) + +proc addFieldPair*(builder: var EnrBuilder, pair: FieldPair) = + builder.fields.add(pair) + +proc addFieldPair*[V](builder: var EnrBuilder, key: string, value: V) = + builder.addFieldPair(toFieldPair(key, value)) + +proc build*(builder: EnrBuilder): EnrResult[enr.Record] = + # Note that nim-eth's `Record.init` does not deduplicate the field pairs. + # See: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L143-L144 + enr.Record.init( + seqNum = builder.seqNumber, + pk = builder.privateKey, + ip = none(ValidIpAddress), + tcpPort = none(Port), + udpPort = none(Port), + extraFields = builder.fields + ) + + +## Builder extension: IP address and TCP/UDP ports + +proc addAddressAndPorts(builder: var EnrBuilder, ip: ValidIpAddress, tcpPort, udpPort: Option[Port]) = + # Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166 + let isV6 = ip.family == IPv6 + + let ipField = if isV6: toFieldPair("ip6", ip.address_v6) + else: toFieldPair("ip", ip.address_v4) + builder.addFieldPair(ipField) + + if tcpPort.isSome(): + let + tcpPortFieldKey = if isV6: "tcp6" else: "tcp" + tcpPortFieldValue = tcpPort.get() + builder.addFieldPair(tcpPortFieldKey, tcpPortFieldValue.uint16) + + if udpPort.isSome(): + let + udpPortFieldKey = if isV6: "udp6" else: "udp" + udpPortFieldValue = udpPort.get() + builder.addFieldPair(udpPortFieldKey, udpPortFieldValue.uint16) + +proc addPorts(builder: var EnrBuilder, tcp, udp: Option[Port]) = + # Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166 + + if tcp.isSome(): + let tcpPort = tcp.get() + builder.addFieldPair("tcp", tcpPort.uint16) + + if udp.isSome(): + let udpPort = udp.get() + builder.addFieldPair("udp", udpPort.uint16) + + +proc withIpAddressAndPorts*(builder: var EnrBuilder, + ipAddr = none(ValidIpAddress), + tcpPort = none(Port), + udpPort = none(Port)) = + if ipAddr.isSome(): + addAddressAndPorts(builder, ipAddr.get(), tcpPort, udpPort) + else: + addPorts(builder, tcpPort, udpPort) + diff --git a/waku/v2/protocol/waku_enr.nim b/waku/v2/protocol/waku_enr.nim index 67e3b90c0..563349573 100644 --- a/waku/v2/protocol/waku_enr.nim +++ b/waku/v2/protocol/waku_enr.nim @@ -66,6 +66,7 @@ func toCapabilities*(bitfield: CapabilitiesBitfield): seq[Capabilities] = toSeq(Capabilities.low..Capabilities.high).filterIt(supportsCapability(bitfield, it)) +## TODO: Turn into an EnrBuilder extension func toFieldPair*(caps: CapabilitiesBitfield): FieldPair = toFieldPair(CapabilitiesEnrField, @[caps.uint8]) @@ -190,7 +191,8 @@ func init*(T: type enr.Record, enrTcpPort = none(Port), enrUdpPort = none(Port), wakuFlags = none(CapabilitiesBitfield), - multiaddrs: seq[MultiAddress] = @[]): T = + multiaddrs: seq[MultiAddress] = @[]): T {. + deprecated: "Use Waku commons EnrBuilder instead" .} = assert privateKey.scheme == PKScheme.Secp256k1