From 0edf3f4c0132ad7bc172055012ce13db899c06ce Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 9 Dec 2025 10:00:33 -0400 Subject: [PATCH] feat: add QUIC port --- eth/enr/enr.nim | 28 +++++++----- eth/p2p/discoveryv5/node.nim | 3 +- eth/p2p/discoveryv5/protocol.nim | 74 ++++++++++++++++++++++++++++---- tests/fuzzing/enr/generate.nim | 6 ++- tests/p2p/discv5_test_helper.nim | 6 ++- tests/p2p/test_discoveryv5.nim | 14 +++--- tests/test_enr.nim | 38 +++++++++++----- 7 files changed, 129 insertions(+), 40 deletions(-) diff --git a/eth/enr/enr.nim b/eth/enr/enr.nim index 4e2ffd59..cd6597c7 100644 --- a/eth/enr/enr.nim +++ b/eth/enr/enr.nim @@ -75,6 +75,8 @@ type udp*: Opt[int] tcp6*: Opt[int] udp6*: Opt[int] + quic*: Opt[int] + quic6*: Opt[int] EnrResult*[T] = Result[T, cstring] @@ -218,7 +220,7 @@ macro initRecord*( func insertAddress( fields: var seq[FieldPair], ip: Opt[IpAddress], - tcpPort, udpPort: Opt[Port]) = + tcpPort, udpPort, quicPort: Opt[Port]) = ## 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. @@ -233,6 +235,8 @@ func insertAddress( fields.insert(("tcp", tcpPort.get().uint16.toField)) if udpPort.isSome(): fields.insert(("udp", udpPort.get().uint16.toField)) + if quicPort.isSome(): + fields.insert(("quic", quicPort.get().uint16.toField)) func init*( T: type Record, @@ -240,17 +244,18 @@ func init*( ip: Opt[IpAddress] = Opt.none(IpAddress), tcpPort: Opt[Port] = Opt.none(Port), udpPort: Opt[Port] = Opt.none(Port), + quicPort: 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. + ## ip address, tcp port, udp port, quic 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]() - fields.insertAddress(ip, tcpPort, udpPort) + fields.insertAddress(ip, tcpPort, udpPort, quicPort) fields.insert extraFields makeEnrAux(seqNum, "v4", pk, fields) @@ -326,16 +331,17 @@ func update*( ip: Opt[IpAddress] = Opt.none(IpAddress), tcpPort: Opt[Port] = Opt.none(Port), udpPort: Opt[Port] = Opt.none(Port), + quicPort: Opt[Port] = Opt.none(Port), extraFields: openArray[FieldPair] = []): EnrResult[void] = - ## Update a `Record` with given ip address, tcp port, udp port and optional - ## custom k:v pairs. + ## Update a `Record` with given ip address, tcp port, udp port, quic port + ## and optional custom k:v pairs. ## ## 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. + ## Providing an `Opt.none` for `ip`, `tcpPort`, `udpPort` or `quicPort` 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` @@ -349,7 +355,7 @@ func update*( 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.insertAddress(ip, tcpPort, udpPort, quicPort) r.pairs.insert extraFields if r.seqNum == high(type r.seqNum): # highly unlikely @@ -376,7 +382,9 @@ func fromRecord*(T: type TypedRecord, r: Record): T = tcp: r.tryGet("tcp", int), tcp6: r.tryGet("tcp6", int), udp: r.tryGet("udp", int), - udp6: r.tryGet("udp6", int) + udp6: r.tryGet("udp6", int), + quic: r.tryGet("quic", int), + quic6: r.tryGet("quic6", int) ) func toTypedRecord*(r: Record): EnrResult[TypedRecord] {.deprecated: "Please use TypedRecord.fromRecord instead".} = @@ -462,7 +470,7 @@ func fromBytesAux(T: type Record, s: openArray[byte]): EnrResult[T] = of "secp256k1": pkRaw = Opt.some rlpResult rlp.read(seq[byte]) pairs.add((k, Field(kind: kBytes, bytes: pkRaw.value()))) - of "tcp", "udp", "tcp6", "udp6": + of "tcp", "udp", "tcp6", "udp6", "quic", "quic6": let v = rlpResult rlp.read(uint16) pairs.add((k, Field(kind: kNum, num: v))) else: diff --git a/eth/p2p/discoveryv5/node.nim b/eth/p2p/discoveryv5/node.nim index 9136fbdb..668a311e 100644 --- a/eth/p2p/discoveryv5/node.nim +++ b/eth/p2p/discoveryv5/node.nim @@ -56,8 +56,9 @@ func newNode*(r: Record): Result[Node, cstring] {.deprecated: "Use TypedRecord.f func update*(n: Node, pk: PrivateKey, ip: Opt[IpAddress], tcpPort: Opt[Port] = Opt.none(Port), udpPort: Opt[Port] = Opt.none(Port), + quicPort: Opt[Port] = Opt.none(Port), extraFields: openArray[FieldPair] = []): Result[void, cstring] = - ? n.record.update(pk, ip, tcpPort, udpPort, extraFields) + ? n.record.update(pk, ip, tcpPort, udpPort, quicPort, extraFields) if ip.isSome(): if udpPort.isSome(): diff --git a/eth/p2p/discoveryv5/protocol.nim b/eth/p2p/discoveryv5/protocol.nim index bd538608..47a94bfb 100644 --- a/eth/p2p/discoveryv5/protocol.nim +++ b/eth/p2p/discoveryv5/protocol.nim @@ -1059,7 +1059,7 @@ func init*( proc newProtocol*( privKey: PrivateKey, enrIp: Opt[IpAddress], - enrTcpPort, enrUdpPort: Opt[Port], + enrTcpPort, enrUdpPort, enrQuicPort: Opt[Port], localEnrFields: openArray[FieldPair] = [], bootstrapRecords: openArray[Record] = [], previousRecord = Opt.none(enr.Record), @@ -1083,14 +1083,14 @@ proc newProtocol*( 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, + record.update(privKey, enrIp, enrTcpPort, enrUdpPort, enrQuicPort, localEnrFields).expect("Record within size limits and correct key") else: - record = enr.Record.init(1, privKey, enrIp, enrTcpPort, enrUdpPort, + record = enr.Record.init(1, privKey, enrIp, enrTcpPort, enrUdpPort, enrQuicPort, localEnrFields).expect("Record within size limits") info "Discovery ENR initialized", enrAutoUpdate, seqNum = record.seqNum, - ip = enrIp, tcpPort = enrTcpPort, udpPort = enrUdpPort, + ip = enrIp, tcpPort = enrTcpPort, udpPort = enrUdpPort, quicPort = enrQuicPort, localEnrFields, uri = toURI(record) if enrIp.isNone(): if enrAutoUpdate: @@ -1124,6 +1124,26 @@ proc newProtocol*( privKey: PrivateKey, enrIp: Opt[IpAddress], enrTcpPort, enrUdpPort: Opt[Port], + localEnrFields: openArray[FieldPair] = [], + bootstrapRecords: openArray[Record] = [], + previousRecord = Opt.none(enr.Record), + bindPort: Port, + bindIp = IPv4_any(), + enrAutoUpdate = false, + banNodes = false, + config = defaultDiscoveryConfig, + rng = newRng()): + Protocol = + newProtocol( + privKey, enrIp, enrTcpPort, enrUdpPort, Opt.none(Port), localEnrFields, + bootstrapRecords, previousRecord, bindPort, bindIp, enrAutoUpdate, + banNodes, config, rng + ) + +proc newProtocol*( + privKey: PrivateKey, + enrIp: Opt[IpAddress], + enrTcpPort, enrUdpPort, enrQuicPort: Opt[Port], localEnrFields: openArray[(string, seq[byte])] = [], bootstrapRecords: openArray[Record] = [], previousRecord = Opt.none(enr.Record), @@ -1136,7 +1156,7 @@ proc newProtocol*( ): Protocol = let customEnrFields = mapIt(localEnrFields, toFieldPair(it[0], it[1])) newProtocol( - privKey, enrIp, enrTcpPort, enrUdpPort, customEnrFields, bootstrapRecords, + privKey, enrIp, enrTcpPort, enrUdpPort, enrQuicPort, customEnrFields, bootstrapRecords, previousRecord, bindPort, bindIp, enrAutoUpdate, banNodes, config, rng, ) @@ -1148,6 +1168,26 @@ proc newProtocol*( bootstrapRecords: openArray[Record] = [], previousRecord = Opt.none(enr.Record), bindPort: Port, + bindIp = IPv4_any(), + enrAutoUpdate = false, + banNodes = false, + config = defaultDiscoveryConfig, + rng = newRng(), +): Protocol = + newProtocol( + privKey, enrIp, enrTcpPort, enrUdpPort, Opt.none(Port), localEnrFields, + bootstrapRecords, previousRecord, bindPort, bindIp, enrAutoUpdate, banNodes, + config, rng + ) + +proc newProtocol*( + privKey: PrivateKey, + enrIp: Opt[IpAddress], + enrTcpPort, enrUdpPort, enrQuicPort: Opt[Port], + localEnrFields: openArray[(string, seq[byte])] = [], + bootstrapRecords: openArray[Record] = [], + previousRecord = Opt.none(enr.Record), + bindPort: Port, bindIp: Opt[IpAddress], enrAutoUpdate = false, config = defaultDiscoveryConfig, @@ -1159,15 +1199,15 @@ proc newProtocol*( 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, + res.update(privKey, enrIp, enrTcpPort, enrUdpPort, enrQuicPort, customEnrFields).expect("Record within size limits and correct key") res else: - enr.Record.init(1, privKey, enrIp, enrTcpPort, enrUdpPort, + enr.Record.init(1, privKey, enrIp, enrTcpPort, enrUdpPort, enrQuicPort, customEnrFields).expect("Record within size limits") info "Discovery ENR initialized", enrAutoUpdate, seqNum = record.seqNum, - ip = enrIp, tcpPort = enrTcpPort, udpPort = enrUdpPort, + ip = enrIp, tcpPort = enrTcpPort, udpPort = enrUdpPort, quicPort = enrQuicPort, customEnrFields, uri = toURI(record) if enrIp.isNone(): @@ -1198,6 +1238,24 @@ proc newProtocol*( responseTimeout: config.responseTimeout, rng: rng) +proc newProtocol*( + privKey: PrivateKey, + enrIp: Opt[IpAddress], + enrTcpPort, enrUdpPort: Opt[Port], + localEnrFields: openArray[(string, seq[byte])] = [], + bootstrapRecords: openArray[Record] = [], + previousRecord = Opt.none(enr.Record), + bindPort: Port, + bindIp: Opt[IpAddress], + enrAutoUpdate = false, + config = defaultDiscoveryConfig, + rng = newRng()): Protocol = + newProtocol( + privKey, enrIp, enrTcpPort, enrUdpPort, Opt.none(Port), localEnrFields, + bootstrapRecords, previousRecord, bindPort, bindIp, enrAutoUpdate, + config, rng + ) + proc `$`*(a: OptAddress): string = if a.ip.isNone(): "*:" & $a.port diff --git a/tests/fuzzing/enr/generate.nim b/tests/fuzzing/enr/generate.nim index b7c7d0a0..fa8baea0 100644 --- a/tests/fuzzing/enr/generate.nim +++ b/tests/fuzzing/enr/generate.nim @@ -14,10 +14,12 @@ proc generate() = port = Opt.some(Port(20301)) block: - let record = enr.Record.init(1, privKey, ip, port, port)[] + let record = enr.Record.init(1, privKey, ip, port, port, port)[] record.raw.toFile(inputsDir / "enr1") block: - let record = enr.Record.init(1, privKey, ip, port, port, [toFieldPair("test", 1'u)])[] + let record = enr.Record.init( + 1, privKey, ip, port, port, port, [toFieldPair("test", 1'u)] + )[] record.raw.toFile(inputsDir / "enr2") discard existsOrCreateDir(inputsDir) diff --git a/tests/p2p/discv5_test_helper.nim b/tests/p2p/discv5_test_helper.nim index 6865cc63..61ee4753 100644 --- a/tests/p2p/discv5_test_helper.nim +++ b/tests/p2p/discv5_test_helper.nim @@ -32,7 +32,9 @@ proc initDiscoveryNode*( let protocol = newProtocol( privKey, Opt.some(address.ip), - Opt.some(address.port), Opt.some(address.port), + Opt.some(address.port), + Opt.some(address.port), + Opt.some(address.port), bindPort = address.port, bootstrapRecords = bootstrapRecords, localEnrFields = localEnrFields, @@ -54,7 +56,7 @@ func generateNode*(privKey: PrivateKey, port: int = 20302, localEnrFields: openArray[FieldPair] = []): Node = let port = Port(port) let enr = enr.Record.init(1, privKey, Opt.some(ip), - Opt.some(port), Opt.some(port), localEnrFields).expect("Properly initialized private key") + Opt.some(port), Opt.some(port), Opt.some(port), localEnrFields).expect("Properly initialized private key") result = Node.fromRecord(enr) proc generateNRandomNodes*(rng: var HmacDrbgContext, n: int): seq[Node] = diff --git a/tests/p2p/test_discoveryv5.nim b/tests/p2p/test_discoveryv5.nim index 936b5ffa..f58c61d6 100644 --- a/tests/p2p/test_discoveryv5.nim +++ b/tests/p2p/test_discoveryv5.nim @@ -428,15 +428,17 @@ suite "Discovery v5.1 Tests": privKey = PrivateKey.random(rng[]) ip = Opt.some(parseIpAddress("127.0.0.1")) port = Port(20301) - node = newProtocol(privKey, ip, Opt.some(port), Opt.some(port), bindPort = port, - rng = rng) + node = newProtocol(privKey, ip, Opt.some(port), Opt.some(port), + Opt.some(port), bindPort = port, rng = rng) noUpdatesNode = newProtocol(privKey, ip, Opt.some(port), Opt.some(port), - bindPort = port, rng = rng, previousRecord = Opt.some(node.getRecord())) + Opt.some(port), bindPort = port, rng = rng, + previousRecord = Opt.some(node.getRecord())) updatesNode = newProtocol(privKey, ip, Opt.some(port), Opt.some(Port(20302)), - bindPort = port, rng = rng, + Opt.some(port), bindPort = port, rng = rng, previousRecord = Opt.some(noUpdatesNode.getRecord())) moreUpdatesNode = newProtocol(privKey, ip, Opt.some(port), Opt.some(port), - bindPort = port, rng = rng, localEnrFields = {"addfield": @[byte 0]}, + Opt.some(port), bindPort = port, rng = rng, + localEnrFields = {"addfield": @[byte 0]}, previousRecord = Opt.some(updatesNode.getRecord())) check: node.getRecord().seqNum == 1 @@ -447,7 +449,7 @@ suite "Discovery v5.1 Tests": # Defect (for now?) on incorrect key use expect ResultDefect: discard newProtocol(PrivateKey.random(rng[]), - ip, Opt.some(port), Opt.some(port), bindPort = port, rng = rng, + ip, Opt.some(port), Opt.some(port), Opt.some(port), bindPort = port, rng = rng, previousRecord = Opt.some(updatesNode.getRecord())) asyncTest "Update node record with revalidate": diff --git a/tests/test_enr.nim b/tests/test_enr.nim index fb6704c9..0197e080 100644 --- a/tests/test_enr.nim +++ b/tests/test_enr.nim @@ -43,6 +43,7 @@ suite "ENR test vector tests": typedRecord.secp256k1.value() == array[33, byte].fromHex(secp256k1) typedRecord.udp.value() == udp typedRecord.tcp.isNone() + typedRecord.quic.isNone() $r == """(1, id: "v4", ip: 127.0.0.1, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""" @@ -107,7 +108,7 @@ suite "ENR encoding tests": port = Opt.some(Port(1234)) customPairs = [toFieldPair("some_list", rlpList)] enr = Record.init( - 123, pk, Opt.some(ip), Opt.none(Port), port, customPairs) + 123, pk, Opt.some(ip), Opt.none(Port), port, Opt.none(Port), customPairs) check: enr.isOk() @@ -131,7 +132,7 @@ suite "ENR encoding tests": ip = parseIpAddress("1.2.3.4") port = Opt.some(Port(9000)) res = Record.init( - 100, keypair.seckey, Opt.some(ip), port, port) + 100, keypair.seckey, Opt.some(ip), port, port, port) check res.isOk() let enr = res.value() let uri = enr.toURI() @@ -152,7 +153,7 @@ suite "ENR init tests": keypair = KeyPair.random(rng[]) port = Opt.none(Port) enr = Record.init( - 100, keypair.seckey, Opt.none(IpAddress), port, port)[] + 100, keypair.seckey, Opt.none(IpAddress), port, port, port)[] typedEnr = TypedRecord.fromRecord(enr) check: @@ -164,10 +165,12 @@ suite "ENR init tests": typedEnr.ip.isNone() typedEnr.tcp.isNone() typedEnr.udp.isNone() + typedEnr.quic.isNone() typedEnr.ip6.isNone() typedEnr.tcp6.isNone() typedEnr.udp6.isNone() + typedEnr.quic6.isNone() test "Record.init only ipv4": let @@ -175,7 +178,7 @@ suite "ENR init tests": ip = parseIpAddress("1.2.3.4") port = Opt.some(Port(9000)) enr = Record.init( - 100, keypair.seckey, Opt.some(ip), port, port)[] + 100, keypair.seckey, Opt.some(ip), port, port, port)[] typedEnr = TypedRecord.fromRecord(enr) check: @@ -188,13 +191,16 @@ suite "ENR init tests": typedEnr.udp.isSome() typedEnr.udp.get() == 9000 + typedEnr.quic.isSome() + typedEnr.quic.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)[] + 100, keypair.seckey, Opt.some(ip), port, port, port)[] typedEnr = TypedRecord.fromRecord(enr) check: @@ -203,12 +209,14 @@ suite "ENR init tests": typedEnr.tcp.value() == 9000 typedEnr.udp.isSome() typedEnr.udp.value() == 9000 - + typedEnr.quic.isSome() + typedEnr.quic.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() + typedEnr.quic6.isNone() test "Record.init max ENR size": let @@ -281,7 +289,7 @@ suite "ENR update tests": pk = PrivateKey.fromHex( "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] newField = toFieldPair("test", 123'u) - var r = Record.init(1, pk, Opt.none(IpAddress), Opt.none(Port), Opt.none(Port))[] + var r = Record.init(1, pk, Opt.none(IpAddress), Opt.none(Port), Opt.none(Port), Opt.none(Port))[] block: # Insert new k:v pair, update of seqNum should occur. let updated = r.update(pk, extraFields = [newField]) @@ -363,7 +371,7 @@ suite "ENR update tests": pk = PrivateKey.fromHex( "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] var r = Record.init(1, pk, Opt.none(IpAddress), - Opt.some(Port(9000)), Opt.some(Port(9000)))[] + Opt.some(Port(9000)), Opt.some(Port(9000)), Opt.some(Port(9000)))[] block: let updated = r.update(pk, Opt.none(IpAddress), @@ -373,21 +381,23 @@ suite "ENR update tests": r.tryGet("ip", uint).isNone() r.tryGet("tcp", uint).isSome() r.tryGet("udp", uint).isSome() + r.tryGet("quic", uint).isSome() r.seqNum == 2 block: let updated = r.update(pk, Opt.none(IpAddress), - Opt.some(Port(9001)), Opt.some(Port(9002))) + Opt.some(Port(9001)), Opt.some(Port(9002)), Opt.some(Port(9002))) check updated.isOk() check: r.tryGet("ip", uint).isNone() r.tryGet("tcp", uint).isSome() r.tryGet("udp", uint).isSome() + r.tryGet("quic", uint).isSome() r.seqNum == 3 block: let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")), - Opt.some(Port(9000)), Opt.some(Port(9000))) + Opt.some(Port(9000)), Opt.some(Port(9000)), Opt.some(Port(9000))) check updated.isOk() let typedEnr = TypedRecord.fromRecord(r) @@ -402,11 +412,14 @@ suite "ENR update tests": typedEnr.udp.isSome() typedEnr.udp.get() == 9000 + typedEnr.quic.isSome() + typedEnr.quic.get() == 9000 + r.seqNum == 4 block: let updated = r.update(pk, Opt.some(parseIpAddress("1.2.3.4")), - Opt.some(Port(9001)), Opt.some(Port(9001))) + Opt.some(Port(9001)), Opt.some(Port(9001)), Opt.some(Port(9001))) check updated.isOk() let typedEnr = TypedRecord.fromRecord(r) @@ -421,4 +434,7 @@ suite "ENR update tests": typedEnr.udp.isSome() typedEnr.udp.get() == 9001 + typedEnr.quic.isSome() + typedEnr.quic.get() == 9001 + r.seqNum == 5