From 7f20d799457b155448e943bb583bfdf59b033188 Mon Sep 17 00:00:00 2001 From: Kim De Mey Date: Mon, 24 Jun 2024 14:57:39 +0200 Subject: [PATCH] Overhaul of ENR implementation - part I (#707) - Rework adding and updating of fields by having an insert call that gets used everywhere. Avoiding also duplicate keys. One side-effect of this is that ENR sequence number will always get updated on an update call, even if nothing changes. - Deprecate initRecord as it is only used in tests and is flawed - Assert when predefined keys go into the extra custom pairs. Any of the predefined keys are only to be passed now via specific parameters to make sure that the correct types are stored in ENR. - Clearify the Opt.none behaviour for Record.update - When setting ipv6, allow for tcp/udp port fields to be used default - General clean-up - Rework/clean-up completely the ENR tests. --- eth/p2p/discoveryv5/enr.nim | 259 ++++++++++--------- eth/p2p/discoveryv5/protocol.nim | 6 +- tests/p2p/test_discoveryv5.nim | 6 +- tests/p2p/test_enr.nim | 428 ++++++++++++++++++++----------- 4 files changed, 412 insertions(+), 287 deletions(-) diff --git a/eth/p2p/discoveryv5/enr.nim b/eth/p2p/discoveryv5/enr.nim index 9b23021..15c7c60 100644 --- a/eth/p2p/discoveryv5/enr.nim +++ b/eth/p2p/discoveryv5/enr.nim @@ -11,7 +11,7 @@ {.push raises: [].} import - std/[strutils, macros, algorithm, net], + std/[strutils, sequtils, macros, algorithm, net], nimcrypto/[keccak, utils], stew/base64, results, @@ -19,33 +19,17 @@ import ".."/../[rlp, keys], ../../net/utils -export options, results, keys +export results, rlp, keys const maxEnrSize = 300 ## Maximum size of an encoded node record, in bytes. minRlpListLen = 4 ## Minimum node record RLP list has: signature, seqId, ## "id" key and value. + PreDefinedKeys = ["id", "secp256k1", "ip", "ip6", "tcp", "tcp6", "udp", "udp6"] + ## Predefined keys in the ENR spec, these have specific constraints on the + ## type of the associated value. type - FieldPair* = (string, Field) - - Record* = object - seqNum*: uint64 - raw*: seq[byte] # RLP encoded record - pairs: seq[FieldPair] # sorted list of all key/value pairs - - EnrUri* = distinct string - - TypedRecord* = object - id*: string - secp256k1*: Opt[array[33, byte]] - ip*: Opt[array[4, byte]] - ip6*: Opt[array[16, byte]] - tcp*: Opt[int] - udp*: Opt[int] - tcp6*: Opt[int] - udp6*: Opt[int] - FieldKind = enum kString, kNum, @@ -64,6 +48,28 @@ type listRaw: seq[byte] ## Differently from the other kinds, this is is stored ## as raw (encoded) RLP data, and thus treated as such further on. + FieldPair* = (string, Field) + + Record* = object + seqNum*: uint64 ## ENR sequence number + pairs*: seq[FieldPair] ## List of all key:value pairs. List must have + ## at least the id k:v pair and the secp256k1 k:v pair. The list of pairs + ## must remain sorted and without duplicate keys. Use the insert func to + ## ensure this. + raw*: seq[byte] ## RLP encoded record + + EnrUri* = distinct string + + TypedRecord* = object + id*: string + secp256k1*: Opt[array[33, byte]] + ip*: Opt[array[4, byte]] + ip6*: Opt[array[16, byte]] + tcp*: Opt[int] + udp*: Opt[int] + tcp6*: Opt[int] + udp6*: Opt[int] + EnrResult*[T] = Result[T, cstring] template toField[T](v: T): Field = @@ -94,8 +100,45 @@ func `==`(a, b: Field): bool = else: false +template toFieldPair*(key: string, value: auto): FieldPair = + (key, toField(value)) + func cmp(a, b: FieldPair): int = cmp(a[0], b[0]) +func hasPredefinedKey(pair: FieldPair): bool = + PreDefinedKeys.contains(pair[0]) + +func hasPredefinedKey(pairs: openArray[FieldPair]): bool = + for pair in pairs: + if hasPredefinedKey(pair): + return true + false + +func find(pairs: openArray[FieldPair], key: string): Opt[int] = + ## Search for key in key:value pairs. + ## + ## Returns some(index of key) if key is found. Else returns none. + for i, (k, v) in pairs: + if k == key: + return Opt.some(i) + Opt.none(int) + +func insert(pairs: var seq[FieldPair], item: FieldPair) = + ## Insert item in key:value pairs. + ## + ## If a FieldPair with key is already present, the value is updated, otherwise + ## the pair is inserted in the correct position to keep the pairs sorted. + let index = find(pairs, item[0]) + if index.isSome(): + pairs[index.get()] = item + else: + pairs.insert(item, pairs.lowerBound(item, cmp)) + +func insert(pairs: var seq[FieldPair], b: openArray[FieldPair]) = + ## Insert all items in key:value pairs. + for item in b: + pairs.insert(item) + func makeEnrRaw( seqNum: uint64, pk: PrivateKey, pairs: openArray[FieldPair]): EnrResult[seq[byte]] = @@ -112,16 +155,18 @@ func makeEnrRaw( of kList: w.appendRawBytes(v.listRaw) # No encoding needs to happen w.finish() - let toSign = block: - var w = initRlpList(pairs.len * 2 + 1) - w.append(seqNum, pairs) + let content = + block: + var w = initRlpList(pairs.len * 2 + 1) + w.append(seqNum, pairs) - let sig = signNR(pk, toSign) + let signature = signNR(pk, content) - var raw = block: - var w = initRlpList(pairs.len * 2 + 2) - w.append(sig.toRaw()) - w.append(seqNum, pairs) + let raw = + block: + var w = initRlpList(pairs.len * 2 + 2) + w.append(signature.toRaw()) + w.append(seqNum, pairs) if raw.len > maxEnrSize: err("Record exceeds maximum size") @@ -129,7 +174,7 @@ func makeEnrRaw( ok(raw) func makeEnrAux( - seqNum: uint64, pk: PrivateKey, + seqNum: uint64, id: string, pk: PrivateKey, pairs: openArray[FieldPair]): EnrResult[Record] = var record: Record record.pairs = @pairs @@ -137,76 +182,70 @@ func makeEnrAux( let pubkey = pk.toPublicKey() - record.pairs.add(("id", Field(kind: kString, str: "v4"))) - record.pairs.add(("secp256k1", + record.pairs.insert(("id", Field(kind: kString, str: id))) + record.pairs.insert(("secp256k1", Field(kind: kBytes, bytes: @(pubkey.toRawCompressed())))) - # Sort by key - record.pairs.sort(cmp) - # TODO: Should deduplicate on keys here also. Should we error on that or just - # deal with it? - record.raw = ? makeEnrRaw(seqNum, pk, record.pairs) ok(record) macro initRecord*( seqNum: uint64, pk: PrivateKey, - pairs: untyped{nkTableConstr}): untyped = + pairs: untyped{nkTableConstr}): untyped {.deprecated: "Please use Record.init instead".} = ## Initialize a `Record` with given sequence number, private key and k:v ## pairs. ## ## Can fail in case the record exceeds the `maxEnrSize`. + # Note: Deprecated as it is flawed. It allows for any type to be stored in the + # predefined keys. It also allows for duplicate keys (which could be fixed) + # and no longer sorts the pairs. It can however be moved and used for testing + # purposes. + for c in pairs: c.expectKind(nnkExprColonExpr) c[1] = newCall(bindSym"toField", c[1]) result = quote do: - makeEnrAux(`seqNum`, `pk`, `pairs`) + makeEnrAux(`seqNum`, "v4", `pk`, `pairs`) -template toFieldPair*(key: string, value: auto): FieldPair = - (key, toField(value)) - -func addAddress( +func insertAddress( fields: var seq[FieldPair], ip: Opt[IpAddress], tcpPort, udpPort: Opt[Port]) = - ## Add address information in new fields. Incomplete address - ## information is allowed (example: Port but not IP) as that information - ## might be already in the ENR or added later. + ## Insert address data. + ## Incomplete address information is allowed (example: Port but not IP) as + ## that information might be already in the ENR or added later. if ip.isSome(): - let - ipExt = ip.get() - isV6 = ipExt.family == IPv6 + case ip.value.family + of IPv4: + fields.insert(("ip", ip.value.address_v4.toField)) + of IPv6: + fields.insert(("ip6", ip.value.address_v6.toField)) - fields.add(if isV6: ("ip6", ipExt.address_v6.toField) - else: ("ip", ipExt.address_v4.toField)) - if tcpPort.isSome(): - fields.add(((if isV6: "tcp6" else: "tcp"), tcpPort.get().uint16.toField)) - if udpPort.isSome(): - fields.add(((if isV6: "udp6" else: "udp"), udpPort.get().uint16.toField)) - else: - if tcpPort.isSome(): - fields.add(("tcp", tcpPort.get().uint16.toField)) - if udpPort.isSome(): - fields.add(("udp", udpPort.get().uint16.toField)) + if tcpPort.isSome(): + fields.insert(("tcp", tcpPort.get().uint16.toField)) + if udpPort.isSome(): + fields.insert(("udp", udpPort.get().uint16.toField)) func init*( T: type Record, seqNum: uint64, pk: PrivateKey, - ip: Opt[IpAddress], - tcpPort, udpPort: Opt[Port], + ip: Opt[IpAddress] = Opt.none(IpAddress), + tcpPort: Opt[Port] = Opt.none(Port), + udpPort: Opt[Port] = Opt.none(Port), extraFields: openArray[FieldPair] = []): EnrResult[T] = ## Initialize a `Record` with given sequence number, private key, optional ## ip address, tcp port, udp port, and optional custom k:v pairs. ## ## Can fail in case the record exceeds the `maxEnrSize`. + doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs") + var fields = newSeq[FieldPair]() - # TODO: Allow for initializing ENR with both ip4 and ipv6 address. - fields.addAddress(ip, tcpPort, udpPort) - fields.add extraFields - makeEnrAux(seqNum, pk, fields) + fields.insertAddress(ip, tcpPort, udpPort) + fields.insert extraFields + makeEnrAux(seqNum, "v4", pk, fields) func getField(r: Record, name: string, field: var Field): bool = # It might be more correct to do binary search, @@ -270,61 +309,10 @@ func get*(r: Record, T: type PublicKey): Opt[T] = return Opt.some(pk[]) Opt.none(T) -func find(r: Record, key: string): Opt[int] = - ## Search for key in record key:value pairs. - ## - ## Returns some(index of key) if key is found in record. Else return none. - for i, (k, v) in r.pairs: - if k == key: - return Opt.some(i) - Opt.none(int) - func update*( - record: var Record, pk: PrivateKey, - fieldPairs: openArray[FieldPair]): EnrResult[void] = - ## Update a `Record` k:v pairs. - ## - ## In case any of the k:v pairs is updated or added (new), the sequence number - ## of the `Record` will be incremented and a new signature will be applied. - ## - ## Can fail in case of wrong `PrivateKey`, if the size of the resulting record - ## exceeds `maxEnrSize` or if maximum sequence number is reached. The `Record` - ## will not be altered in these cases. - var r = record - - let pubkey = r.get(PublicKey) - if pubkey.isNone() or pubkey.get() != pk.toPublicKey(): - return err("Public key does not correspond with given private key") - - var updated = false - for fieldPair in fieldPairs: - let index = r.find(fieldPair[0]) - if(index.isSome()): - if r.pairs[index.get()][1] == fieldPair[1]: - # Exact k:v pair is already in record, nothing to do here. - continue - else: - # Need to update the value. - r.pairs[index.get()] = fieldPair - updated = true - else: - # Add new k:v pair. - r.pairs.insert(fieldPair, lowerBound(r.pairs, fieldPair, cmp)) - updated = true - - if updated: - if r.seqNum == high(type r.seqNum): # highly unlikely - return err("Maximum sequence number reached") - r.seqNum.inc() - r.raw = ? makeEnrRaw(r.seqNum, pk, r.pairs) - record = r - - ok() - -func update*( - r: var Record, + record: var Record, pk: PrivateKey, - ip: Opt[IpAddress], + ip: Opt[IpAddress] = Opt.none(IpAddress), tcpPort: Opt[Port] = Opt.none(Port), udpPort: Opt[Port] = Opt.none(Port), extraFields: openArray[FieldPair] = []): @@ -332,18 +320,35 @@ func update*( ## Update a `Record` with given ip address, tcp port, udp port and optional ## custom k:v pairs. ## - ## In case any of the k:v pairs is updated or added (new), the sequence number - ## of the `Record` will be incremented and a new signature will be applied. + ## If none of the k:v pairs are changed, the sequence number of the `Record` + ## will still be incremented and a new signature will be applied. + ## + ## Providing an `Opt.none` for `ip`, `tcpPort` or `udpPort` will leave the + ## corresponding field untouched. ## ## Can fail in case of wrong `PrivateKey`, if the size of the resulting record ## exceeds `maxEnrSize` or if maximum sequence number is reached. The `Record` ## will not be altered in these cases. - var fields = newSeq[FieldPair]() + # TODO: deprecate this call and have individual functions for updating? + doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs") - # TODO: Make updating of both ipv4 and ipv6 address in ENR more convenient. - fields.addAddress(ip, tcpPort, udpPort) - fields.add extraFields - r.update(pk, fields) + var r = record + + let pubkey = r.get(PublicKey) + if pubkey.isNone() or pubkey.get() != pk.toPublicKey(): + return err("Public key does not correspond with given private key") + + r.pairs.insertAddress(ip, tcpPort, udpPort) + r.pairs.insert extraFields + + if r.seqNum == high(type r.seqNum): # highly unlikely + return err("Maximum sequence number reached") + r.seqNum.inc() + + r.raw = ? makeEnrRaw(r.seqNum, pk, r.pairs) + record = r + + ok() func tryGet*(r: Record, key: string, T: type): Opt[T] = ## Get the value from the provided key. @@ -548,7 +553,7 @@ func read*( rlp: var Rlp, T: type Record): T {.raises: [RlpError, ValueError].} = var res: T - if not rlp.hasData() or not res.fromBytes(rlp.rawData): + if not rlp.hasData() or not res.fromBytes(rlp.rawData()): # TODO: This could also just be an invalid signature, would be cleaner to # split of RLP deserialisation errors from this. raise newException(ValueError, "Could not deserialize") diff --git a/eth/p2p/discoveryv5/protocol.nim b/eth/p2p/discoveryv5/protocol.nim index 43b0f08..6539fd9 100644 --- a/eth/p2p/discoveryv5/protocol.nim +++ b/eth/p2p/discoveryv5/protocol.nim @@ -253,7 +253,7 @@ func updateRecord*( d: Protocol, enrFields: openArray[(string, seq[byte])]): DiscResult[void] = ## Update the ENR of the local node with provided `enrFields` k:v pairs. let fields = mapIt(enrFields, toFieldPair(it[0], it[1])) - d.localNode.record.update(d.privateKey, fields) + d.localNode.record.update(d.privateKey, extraFields = fields) # TODO: Would it make sense to actively ping ("broadcast") to all the peers # we stored a handshake with in order to get that ENR updated? @@ -992,6 +992,8 @@ proc newProtocol*( var record: Record if previousRecord.isSome(): record = previousRecord.get() + # TODO: this is faulty in case the intent is to remove a field with + # opt.none record.update(privKey, enrIp, enrTcpPort, enrUdpPort, customEnrFields).expect("Record within size limits and correct key") else: @@ -1045,6 +1047,8 @@ proc newProtocol*( record = if previousRecord.isSome(): var res = previousRecord.get() + # TODO: this is faulty in case the intent is to remove a field with + # opt.none res.update(privKey, enrIp, enrTcpPort, enrUdpPort, customEnrFields).expect("Record within size limits and correct key") res diff --git a/tests/p2p/test_discoveryv5.nim b/tests/p2p/test_discoveryv5.nim index 051e3b4..cc63239 100644 --- a/tests/p2p/test_discoveryv5.nim +++ b/tests/p2p/test_discoveryv5.nim @@ -437,9 +437,9 @@ suite "Discovery v5 Tests": previousRecord = Opt.some(updatesNode.getRecord())) check: node.getRecord().seqNum == 1 - noUpdatesNode.getRecord().seqNum == 1 - updatesNode.getRecord().seqNum == 2 - moreUpdatesNode.getRecord().seqNum == 3 + noUpdatesNode.getRecord().seqNum == 2 + updatesNode.getRecord().seqNum == 3 + moreUpdatesNode.getRecord().seqNum == 4 # Defect (for now?) on incorrect key use expect ResultDefect: diff --git a/tests/p2p/test_enr.nim b/tests/p2p/test_enr.nim index 59896cf..3c34608 100644 --- a/tests/p2p/test_enr.nim +++ b/tests/p2p/test_enr.nim @@ -8,85 +8,156 @@ import std/[sequtils, net], + stew/byteutils, unittest2, ../../eth/p2p/discoveryv5/enr, ../../eth/[keys, rlp] let rng = newRng() -suite "ENR": - test "Serialization": - var pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[] - check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") - let uri = r.toURI() - var r2: Record - let sigValid = r2.fromURI(uri) - check(sigValid) - check($r2 == $r) - check(r2.raw == r.raw) +proc testRlpEncodingLoop*(r: enr.Record): bool = + let encoded = rlp.encode(r) + let decoded = rlp.decode(encoded, enr.Record) + decoded == r +suite "ENR test vector tests": + # Tests using the test vector from: + # https://github.com/ethereum/devp2p/blob/master/enr.md#test-vectors + const + uri = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8" + pk = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" + seqNum = 1 + id = "v4" + ip = "7f000001" + secp256k1 = "03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138" + udp = 0x765f + + test "Test vector full encode loop": + var r: Record + let valid = r.fromURI(uri) + check valid + let res = toTypedRecord(r) + check res.isOk() + let typedRecord = res.value + check: + r.seqNum == seqNum + typedRecord.id == id + typedRecord.ip.value() == array[4, byte].fromHex(ip) + typedRecord.secp256k1.value() == array[33, byte].fromHex(secp256k1) + typedRecord.udp.value() == udp + typedRecord.tcp.isNone() + + $r == """(1, id: "v4", ip: 127.0.0.1, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""" + + r.toURI() == uri + + test "Test vector Record.init": + let privKey = PrivateKey.fromHex( + pk).expect("valid private key") + + var r = Record.init(1, privKey, + Opt.some(IpAddress(family: IPv4, address_v4: array[4, byte].fromHex(ip))), + Opt.none(Port), Opt.some(Port(udp))) + + check: + r.isOk() + r.value.seqNum == seqNum + r.value.toURI() == uri + +suite "ENR encoding tests": test "RLP serialisation": - var pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[] - check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") - let encoded = rlp.encode(r) - let decoded = rlp.decode(encoded, enr.Record) - check($decoded == $r) - check(decoded.raw == r.raw) + let + keypair = KeyPair.random(rng[]) + ip = parseIpAddress("1.2.3.4") + port = Opt.some(Port(9000)) + enr = Record.init( + 100, keypair.seckey, Opt.some(ip), port, port) - test "RLP deserialisation without data": + check: + enr.isOk() + testRlpEncodingLoop(enr.value) + + test "Empty RLP": expect ValueError: - let decoded = rlp.decode([], enr.Record) + let _ = rlp.decode([], enr.Record) var r: Record check not fromBytes(r, []) - test "Base64 deserialisation without data": - var r: Record - let sigValid = r.fromURI("enr:") - check(not sigValid) + test "Invalid RLP": + expect RlpError: + let _ = rlp.decode([byte 0xf7], enr.Record) - test "Parsing": var r: Record - let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") - check(sigValid) - check($r == """(1, id: "v4", ip: 127.0.0.1, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""") + check not fromBytes(r, [byte 0xf7]) + + test "No RLP list": + expect ValueError: + let _ = rlp.decode([byte 0x7f], enr.Record) - test "Bad base64": var r: Record - let sigValid = r.fromURI("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") - check(not sigValid) + check not fromBytes(r, [byte 0x7f]) - test "Bad rlp": - var r: Record - let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOOnrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") - check(not sigValid) + test "ENR with RLP list value": + type + RlpTestList = object + number: uint16 + data: seq[byte] + text: string - test "Create from ENode address": let - keypair = KeyPair.random(rng[]) - ip = parseIpAddress("10.20.30.40") - port = Opt.some(Port(9000)) + rlpList = RlpTestList(number: 72, data: @[byte 0x0, 0x1, 0x2], text: "Hi there") + pk = PrivateKey.fromHex( + "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key") + ip = parseIpAddress("5.6.7.8") + port = Opt.some(Port(1234)) + customPairs = [toFieldPair("some_list", rlpList)] enr = Record.init( - 100, keypair.seckey, Opt.some(ip), port, port,@[])[] - typedEnr = get enr.toTypedRecord() + 123, pk, Opt.some(ip), Opt.none(Port), port, customPairs) check: - typedEnr.secp256k1.isSome() - typedEnr.secp256k1.get == keypair.pubkey.toRawCompressed() + enr.isOk() + $enr.value == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, some_list: (Raw RLP list) 0xCE4883000102884869207468657265, udp: 1234)""" + testRlpEncodingLoop(enr.value) - typedEnr.ip.isSome() - typedEnr.ip.get() == [byte 10, 20, 30, 40] + test "Base64 encode loop": + const encodedBase64 = "-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8" + var r: Record + check: + r.fromBase64(encodedBase64) + toBase64(r) == encodedBase64 - typedEnr.tcp.isSome() - typedEnr.tcp.get() == 9000 + test "Invalid base64": + var r: Record + let valid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") + check not valid - typedEnr.udp.isSome() - typedEnr.udp.get() == 9000 + test "URI encode loop": + let + keypair = KeyPair.random(rng[]) + ip = parseIpAddress("1.2.3.4") + port = Opt.some(Port(9000)) + res = Record.init( + 100, keypair.seckey, Opt.some(ip), port, port) + check res.isOk() + let enr = res.value() + let uri = enr.toURI() + var enr2: Record + let valid = enr2.fromURI(uri) + check(valid) + check(enr == enr2) - test "ENR without address": + test "Invalid URI: empty": + var r: Record + let valid = r.fromURI("") + check not valid + + test "Invalid URI: no payload": + var r: Record + let valid = r.fromURI("enr:") + check not valid + +suite "ENR init tests": + test "Record.init minimum fields": let keypair = KeyPair.random(rng[]) port = Opt.none(Port) @@ -95,6 +166,8 @@ suite "ENR": typedEnr = get enr.toTypedRecord() check: + testRlpEncodingLoop(enr) + typedEnr.secp256k1.isSome() typedEnr.secp256k1.get() == keypair.pubkey.toRawCompressed() @@ -106,17 +179,113 @@ suite "ENR": typedEnr.tcp6.isNone() typedEnr.udp6.isNone() - test "ENR init size too big": - let pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] + test "Record.init only ipv4": + let + keypair = KeyPair.random(rng[]) + ip = parseIpAddress("1.2.3.4") + port = Opt.some(Port(9000)) + enr = Record.init( + 100, keypair.seckey, Opt.some(ip), port, port)[] + typedEnr = get enr.toTypedRecord() + + check: + typedEnr.ip.isSome() + typedEnr.ip.get() == [byte 1, 2, 3, 4] + + typedEnr.tcp.isSome() + typedEnr.tcp.get() == 9000 + + typedEnr.udp.isSome() + typedEnr.udp.get() == 9000 + + test "Record.init only ipv6": + let + keypair = KeyPair.random(rng[]) + ip = parseIpAddress("::1") + port = Opt.some(Port(9000)) + enr = Record.init( + 100, keypair.seckey, Opt.some(ip), port, port)[] + typedEnr = get enr.toTypedRecord() + + check: + typedEnr.ip.isNone() + typedEnr.tcp.isSome() + typedEnr.tcp.value() == 9000 + typedEnr.udp.isSome() + typedEnr.udp.value() == 9000 + + typedEnr.ip6.isSome() + typedEnr.ip6.get() == [byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + + typedEnr.tcp6.isNone() + typedEnr.udp6.isNone() + + test "Record.init max ENR size": + let + pk = PrivateKey.fromHex( + "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key") block: # This gives ENR of 300 bytes encoded - let r = initRecord(1, pk, {"maxvalue": repeat(byte 2, 169),}) + let r = Record.init( + 1, pk, extraFields = [toFieldPair("maxvalue", repeat(byte 2, 169))] + ) check r.isOk() block: # This gives ENR of 301 bytes encoded - let r = initRecord(1, pk, {"maxplus1": repeat(byte 2, 170),}) + let r = Record.init( + 1, pk, extraFields = [toFieldPair("maxplus1", repeat(byte 2, 170))] + ) check r.isErr() + test "PreDefinedKeys in custom pairs": + let + keypair = KeyPair.random(rng[]) + customPairs = [toFieldPair("ip", @[byte 1, 1, 1, 1])] + + expect AssertionDefect: + let _ = Record.init( + 1, keypair.seckey, extraFields = customPairs) + + test "Duplicate key": + # With duplicate key, the last one should be used (insert) + let + keypair = KeyPair.random(rng[]) + customPairs = [ + toFieldPair("test1", @[byte 1, 1, 1, 1]), + toFieldPair("test2", "abc"), + toFieldPair("test1", "1.2.3.4") + ] + + let res = Record.init( + 1, keypair.seckey, extraFields = customPairs) + + check: res.isOk() + let + enr = res.value + test1Field = enr.get("test1", string) + test2Field = enr.get("test2", string) + check: + test1Field.isOk() + test2Field.isOk() + test1Field.value == "1.2.3.4" + test2Field.value == "abc" + + test "Record.init sorted": + let + pk = PrivateKey.fromHex( + "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key") + customPairs = [ + toFieldPair("abc", 1234'u), + toFieldPair("z", [byte 0]), + toFieldPair("123", "abc"), + toFieldPair("a12", 1'u) + ] + r = Record.init(123, pk, extraFields = customPairs) + + check: + r.isOk() + $r.value == """(123, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, z: 0x00)""" + +suite "ENR update tests": test "ENR update": let pk = PrivateKey.fromHex( @@ -125,67 +294,81 @@ suite "ENR": var r = Record.init(1, pk, Opt.none(IpAddress), Opt.none(Port), Opt.none(Port))[] block: # Insert new k:v pair, update of seqNum should occur. - let updated = r.update(pk, [newField]) + let updated = r.update(pk, extraFields = [newField]) check updated.isOk() check: r.get("test", uint).get() == 123 r.seqNum == 2 - block: # Insert same k:v pair, no update of seqNum should occur. - let updated = r.update(pk, [newField]) + block: # Insert same k:v pair, update of seqNum still occurs. + let updated = r.update(pk, extraFields = [newField]) check updated.isOk() check: r.get("test", uint).get() == 123 - r.seqNum == 2 + r.seqNum == 3 block: # Insert k:v pair with changed value, update of seqNum should occur. let updatedField = toFieldPair("test", 1234'u) - let updated = r.update(pk, [updatedField]) + let updated = r.update(pk, extraFields = [updatedField]) check updated.isOk() check: r.get("test", uint).get() == 1234 - r.seqNum == 3 + r.seqNum == 4 test "ENR update sorted": - let pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] - var r = initRecord(123, pk, {"abc": 1234'u, - "z": [byte 0], - "123": "abc", - "a12": 1'u})[] + let + pk = PrivateKey.fromHex( + "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key") + customPairs = [ + toFieldPair("abc", 1234'u), + toFieldPair("z", [byte 0]), + toFieldPair("123", "abc"), + toFieldPair("a12", 1'u) + ] + res = Record.init(123, pk, extraFields = customPairs) + + check res.isOk() + var r = res.value + check $r == """(123, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, z: 0x00)""" let newField = toFieldPair("test", 123'u) let newField2 = toFieldPair("zzz", 123'u) - let updated = r.update(pk, [newField, newField2]) + let updated = r.update(pk, extraFields = [newField, newField2]) check updated.isOk() check $r == """(124, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, test: 123, z: 0x00, zzz: 123)""" - test "ENR update size too big": - let pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] + test "ENR update too large": + let + pk = PrivateKey.fromHex( + "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key") + customPairs = [toFieldPair("maxvalue", repeat(byte 2, 169))] - var r = initRecord(1, pk, {"maxvalue": repeat(byte 2, 169),}) - check r.isOk() + res = Record.init(123, pk, extraFields = customPairs) + + check res.isOk() + var r = res.value let newField = toFieldPair("test", 123'u) - let updated = r[].update(pk, [newField]) + let updated = r.update(pk, extraFields = [newField]) check updated.isErr() - test "ENR update invalid key": - let pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] + test "ENR update with wrong private key": + let + pk = PrivateKey.fromHex( + "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key") - var r = initRecord(1, pk, {"abc": 1'u,}) - check r.isOk() + res = Record.init(123, pk) + check res.isOk() + var r = res.value let wrongPk = PrivateKey.random(rng[]) newField = toFieldPair("test", 123'u) - updated = r[].update(wrongPk, [newField]) + updated = r.update(wrongPk, extraFields = [newField]) check updated.isErr() - test "ENR update address": + test "ENR update addresses": let pk = PrivateKey.fromHex( "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] @@ -200,7 +383,7 @@ suite "ENR": r.tryGet("ip", uint).isNone() r.tryGet("tcp", uint).isSome() r.tryGet("udp", uint).isSome() - r.seqNum == 1 + r.seqNum == 2 block: let updated = r.update(pk, Opt.none(IpAddress), @@ -210,7 +393,7 @@ suite "ENR": r.tryGet("ip", uint).isNone() r.tryGet("tcp", uint).isSome() r.tryGet("udp", uint).isSome() - r.seqNum == 2 + r.seqNum == 3 block: let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")), @@ -229,10 +412,10 @@ suite "ENR": typedEnr.udp.isSome() typedEnr.udp.get() == 9000 - r.seqNum == 3 + r.seqNum == 4 block: - let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")), + let updated = r.update(pk, Opt.some(parseIpAddress("1.2.3.4")), Opt.some(Port(9001)), Opt.some(Port(9001))) check updated.isOk() @@ -240,7 +423,7 @@ suite "ENR": check: typedEnr.ip.isSome() - typedEnr.ip.get() == [byte 10, 20, 30, 40] + typedEnr.ip.get() == [byte 1, 2, 3, 4] typedEnr.tcp.isSome() typedEnr.tcp.get() == 9001 @@ -248,71 +431,4 @@ suite "ENR": typedEnr.udp.isSome() typedEnr.udp.get() == 9001 - r.seqNum == 4 - - test "ENR with RLP list value": - type - RlpTestList = object - number: uint16 - data: seq[byte] - text: string - - let rlpList = - RlpTestList(number: 72, data: @[byte 0x0, 0x1, 0x2], text: "Hi there") - - let pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8], - "some_list": rlpList})[] - - check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, some_list: (Raw RLP list) 0xCE4883000102884869207468657265, udp: 1234)""") - - let encoded = rlp.encode(r) - let decoded = rlp.decode(encoded, enr.Record) - check($decoded == $r) - check(decoded.raw == r.raw) - - test "ENR IP addresses ": - let pk = PrivateKey.fromHex( - "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] - block: # valid ipv4 - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[] - - check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") - - let encoded = rlp.encode(r) - let decoded = rlp.decode(encoded, enr.Record) - check($decoded == $r) - check(decoded.raw == r.raw) - - block: # invalid ipv4 - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7]})[] - - check($r == """(123, id: "v4", ip: (Invalid) 0x050607, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") - - let encoded = rlp.encode(r) - let decoded = rlp.decode(encoded, enr.Record) - check($decoded == $r) - check(decoded.raw == r.raw) - - block: # valid ipv4 + ipv6 - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8], - "ip6": [byte 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6]})[] - - check($r == """(123, id: "v4", ip: 5.6.7.8, ip6: 102::102:304:506, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") - - let encoded = rlp.encode(r) - let decoded = rlp.decode(encoded, enr.Record) - check($decoded == $r) - check(decoded.raw == r.raw) - - block: # invalid ipv4 + ipv6 - var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8, 9], - "ip6": [byte 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5]})[] - - check($r == """(123, id: "v4", ip: (Invalid) 0x0506070809, ip6: (Invalid) 0x010200000000000000000102030405, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") - - let encoded = rlp.encode(r) - let decoded = rlp.decode(encoded, enr.Record) - check($decoded == $r) - check(decoded.raw == r.raw) + r.seqNum == 5