diff --git a/tests/common/test_enr_builder.nim b/tests/common/test_enr_builder.nim index d6aa4a234..e85b09c9a 100644 --- a/tests/common/test_enr_builder.nim +++ b/tests/common/test_enr_builder.nim @@ -10,7 +10,7 @@ import ../v2/testlib/waku2 -suite "nim-eth ENR - builder": +suite "nim-eth ENR - builder and typed record": test "Non-supported private key (ECDSA)": ## Given @@ -34,12 +34,19 @@ suite "nim-eth ENR - builder": ## Then check enrRes.isOk() - let record = enrRes.tryGet().toTypedRecord().get() + let record = enrRes.tryGet().toTyped().get() + + let id = record.id check: - @(record.secp256k1.get()) == expectedPubKey + id == some(RecordId.V4) + + let publicKey = record.secp256k1 + check: + publicKey.isSome() + @(publicKey.get()) == expectedPubKey -suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports": +suite "nim-eth ENR - Ext: IP address and TCP/UDP ports": test "EIP-778 test vector": ## Given @@ -92,13 +99,13 @@ suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports": ## Then check enrRes.isOk() - let record = enrRes.tryGet().toTypedRecord().get() + let record = enrRes.tryGet().toTyped().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]) + record.tcp == some(enrTcpPort.uint16) + record.udp == none(uint16) + record.ip6 == none(array[16, byte]) test "IPv6 and UDP port": let @@ -122,12 +129,12 @@ suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports": ## Then check enrRes.isOk() - let record = enrRes.tryGet().toTypedRecord().get() + let record = enrRes.tryGet().toTyped().get() check: @(record.secp256k1.get()) == expectedPubKey - record.ip == none(array[0..3, byte]) - record.tcp == none(int) - record.udp == none(int) + record.ip == none(array[4, byte]) + record.tcp == none(uint16) + record.udp == none(uint16) record.ip6 == some(enrIpAddr.address_v6) - record.tcp6 == none(int) - record.udp6 == some(enrUdpPort.int) + record.tcp6 == none(uint16) + record.udp6 == some(enrUdpPort.uint16) diff --git a/tests/v2/test_waku_enr.nim b/tests/v2/test_waku_enr.nim index 5c2feda5f..e693219c9 100644 --- a/tests/v2/test_waku_enr.nim +++ b/tests/v2/test_waku_enr.nim @@ -179,7 +179,7 @@ suite "Waku ENR - Multiaddresses": some(enrTcpPort), some(enrUdpPort), none(CapabilitiesBitfield), multiaddrs) - typedRecord = record.toTypedRecord.get() + typedRecord = record.toTyped().get() # Check EIP-778 ENR fields check: @@ -218,8 +218,8 @@ suite "Waku ENR - Multiaddresses": let # Known values correspond to shared test vectors with other Waku implementations knownIp = ValidIpAddress.init("18.223.219.100") - knownUdpPort = some(9000.int) - knownTcpPort = none(int) + knownUdpPort = some(9000.uint16) + knownTcpPort = none(uint16) knownMultiaddrs = @[MultiAddress.init("/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss")[], MultiAddress.init("/dns6/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss")[]] knownEnr = "enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSH" & @@ -233,7 +233,7 @@ suite "Waku ENR - Multiaddresses": check: enrRecord.fromURI(knownEnr) - let typedRecord = enrRecord.toTypedRecord.get() + let typedRecord = enrRecord.toTyped().get() # Check EIP-778 ENR fields check: diff --git a/waku/common/enr.nim b/waku/common/enr.nim index d2c9af103..5fa864cc7 100644 --- a/waku/common/enr.nim +++ b/waku/common/enr.nim @@ -1,107 +1,13 @@ ## An extension wrapper around nim-eth's ENR module -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - - +import eth/p2p/discoveryv5/enr 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) + ./enr/builder, + ./enr/typed_record +export + enr.Record, enr.EnrResult, enr.get, enr.tryGet, + enr.fromBase64, enr.toBase64, enr.fromURI, enr.toURI, + enr.FieldPair, enr.toFieldPair, enr.init, # TODO: Delete after removing the deprecated procs + builder, + typed_record diff --git a/waku/common/enr/builder.nim b/waku/common/enr/builder.nim new file mode 100644 index 000000000..30fd6d9d4 --- /dev/null +++ b/waku/common/enr/builder.nim @@ -0,0 +1,103 @@ +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 + + +## 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): T = + EnrBuilder( + seqNumber: seqNum, + privateKey: key, + fields: newSeq[FieldPair]() + ) + +proc init*(T: type EnrBuilder, key: libp2p_crypto.PrivateKey, seqNum: uint64 = 1): T = + # 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/common/enr/typed_record.nim b/waku/common/enr/typed_record.nim new file mode 100644 index 000000000..f349b1d69 --- /dev/null +++ b/waku/common/enr/typed_record.nim @@ -0,0 +1,91 @@ +when (NimMajor, NimMinor) < (1, 4): + {.push raises: [Defect].} +else: + {.push raises: [].} + + +import + std/options, + stew/results, + eth/keys as eth_keys, + libp2p/crypto/crypto as libp2p_crypto + +import eth/p2p/discoveryv5/enr except TypedRecord, toTypedRecord + + +## ENR typed record + +# Record identity scheme + +type RecordId* {.pure.} = enum + V4 + +func toRecordId(id: string): EnrResult[RecordId] = + case id: + of "v4": + ok(RecordId.V4) + else: + err("unknown identity scheme") + +func `$`*(id: RecordId): string = + case id: + of RecordId.V4: "v4" + + +# Typed record + +type TypedRecord* = object + raw: Record + +proc init(T: type TypedRecord, record: Record): T = + TypedRecord(raw: record) + +proc tryGet*(record: TypedRecord, field: string, T: type): Option[T] = + record.raw.tryGet(field, T) + +func toTyped*(record: Record): EnrResult[TypedRecord] = + let tr = TypedRecord.init(record) + + # Validate record's identity scheme + let idOpt = tr.tryGet("id", string) + if idOpt.isNone(): + return err("missing id scheme field") + + discard ? toRecordId(idOpt.get()) + + ok(tr) + + +# Typed record field accessors + +func id*(record: TypedRecord): Option[RecordId] = + let fieldOpt = record.tryGet("id", string) + if fieldOpt.isNone(): + return none(RecordId) + + let fieldRes = toRecordId(fieldOpt.get()) + if fieldRes.isErr(): + return none(RecordId) + + some(fieldRes.value) + +func secp256k1*(record: TypedRecord): Option[array[33, byte]] = + record.tryGet("secp256k1", array[33, byte]) + +func ip*(record: TypedRecord): Option[array[4, byte]] = + record.tryGet("ip", array[4, byte]) + +func ip6*(record: TypedRecord): Option[array[16, byte]] = + record.tryGet("ip6", array[16, byte]) + +func tcp*(record: TypedRecord): Option[uint16] = + record.tryGet("tcp", uint16) + +func tcp6*(record: TypedRecord): Option[uint16] = + record.tryGet("tcp6", uint16) + +func udp*(record: TypedRecord): Option[uint16] = + record.tryGet("udp", uint16) + +func udp6*(record: TypedRecord): Option[uint16] = + record.tryGet("udp6", uint16) diff --git a/waku/v2/protocol/waku_enr.nim b/waku/v2/protocol/waku_enr.nim index ee6b01870..0826ebe00 100644 --- a/waku/v2/protocol/waku_enr.nim +++ b/waku/v2/protocol/waku_enr.nim @@ -8,7 +8,7 @@ else: {.push raises: [].} import - std/[bitops, sequtils], + std/[options, bitops, sequtils], stew/[endians2, results], stew/shims/net, eth/keys,