From a1a6862c7c9af4dda2b9034d92430e5469579d33 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 11 Feb 2020 18:25:31 +0200 Subject: [PATCH] More spec compliant ENR * Don't use signed integers in RLP * Don't store IP addresses as var-sized ints (use fixed-sized blobs instead) * Allow constructing ENR from ENode.Address --- eth/p2p/discoveryv5/encoding.nim | 2 +- eth/p2p/discoveryv5/enr.nim | 58 ++++++++++++++++++++------------ eth/p2p/discoveryv5/protocol.nim | 4 +-- tests/p2p/test_enr.nim | 34 ++++++++++++++++--- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/eth/p2p/discoveryv5/encoding.nim b/eth/p2p/discoveryv5/encoding.nim index b5d5476..e1207ce 100644 --- a/eth/p2p/discoveryv5/encoding.nim +++ b/eth/p2p/discoveryv5/encoding.nim @@ -81,7 +81,7 @@ proc makeAuthHeader(c: Codec, toNode: Node, nonce: array[gcmNonceSize, byte], var resp = AuthResponse(version: 5) let ln = c.localNode - if challenge.recordSeq < ln.record.sequenceNumber: + if challenge.recordSeq < ln.record.seqNum: resp.record = ln.record var remotePubkey: PublicKey diff --git a/eth/p2p/discoveryv5/enr.nim b/eth/p2p/discoveryv5/enr.nim index d3121e0..7a9fc83 100644 --- a/eth/p2p/discoveryv5/enr.nim +++ b/eth/p2p/discoveryv5/enr.nim @@ -1,8 +1,10 @@ # ENR implemetation according to spec: # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-778.md -import strutils, macros, algorithm, options -import eth/[rlp, keys], nimcrypto, stew/base64 +import + net, strutils, macros, algorithm, options, + nimcrypto, stew/base64, + eth/[rlp, keys], ../enode const maxEnrSize = 300 @@ -10,7 +12,7 @@ const type Record* = object - sequenceNumber*: uint64 + seqNum*: uint64 # signature: seq[byte] raw*: seq[byte] # RLP encoded record pairs: seq[(string, Field)] # sorted list of all key/value pairs @@ -37,23 +39,25 @@ type of kString: str: string of kNum: - num: int + num: BiggestUInt of kBytes: bytes: seq[byte] template toField[T](v: T): Field = when T is string: Field(kind: kString, str: v) + elif T is array: + Field(kind: kBytes, bytes: @v) elif T is seq[byte]: Field(kind: kBytes, bytes: v) - elif T is SomeInteger: - Field(kind: kNum, num: v.int) + elif T is SomeUnsignedInt: + Field(kind: kNum, num: BiggestUInt(v)) else: {.error: "Unsupported field type".} -proc makeEnrAux(sequenceNumber: uint64, pk: PrivateKey, pairs: openarray[(string, Field)]): Record = +proc makeEnrAux(seqNum: uint64, pk: PrivateKey, pairs: openarray[(string, Field)]): Record = result.pairs = @pairs - result.sequenceNumber = sequenceNumber + result.seqNum = seqNum let pubkey = pk.getPublicKey() @@ -64,8 +68,8 @@ proc makeEnrAux(sequenceNumber: uint64, pk: PrivateKey, pairs: openarray[(string result.pairs.sort() do(a, b: (string, Field)) -> int: cmp(a[0], b[0]) - proc append(w: var RlpWriter, sequenceNumber: uint64, pairs: openarray[(string, Field)]): seq[byte] = - w.append(sequenceNumber) + proc append(w: var RlpWriter, seqNum: uint64, pairs: openarray[(string, Field)]): seq[byte] = + w.append(seqNum) for (k, v) in pairs: w.append(k) case v.kind @@ -76,7 +80,7 @@ proc makeEnrAux(sequenceNumber: uint64, pk: PrivateKey, pairs: openarray[(string let toSign = block: var w = initRlpList(result.pairs.len * 2 + 1) - w.append(sequenceNumber, result.pairs) + w.append(seqNum, result.pairs) var sig: SignatureNR if signRawMessage(keccak256.digest(toSign).data, pk, sig) != EthKeysStatus.Success: @@ -85,15 +89,27 @@ proc makeEnrAux(sequenceNumber: uint64, pk: PrivateKey, pairs: openarray[(string result.raw = block: var w = initRlpList(result.pairs.len * 2 + 2) w.append(sig.getRaw()) - w.append(sequenceNumber, result.pairs) + w.append(seqNum, result.pairs) -macro initRecord*(sequenceNumber: uint64, pk: PrivateKey, pairs: untyped{nkTableConstr}): untyped = +macro initRecord*(seqNum: uint64, pk: PrivateKey, pairs: untyped{nkTableConstr}): untyped = for c in pairs: c.expectKind(nnkExprColonExpr) c[1] = newCall(bindSym"toField", c[1]) result = quote do: - makeEnrAux(`sequenceNumber`, `pk`, `pairs`) + makeEnrAux(`seqNum`, `pk`, `pairs`) + +proc init*(T: type Record, seqNum: uint64, + pk: PrivateKey, + address: enode.Address): T = + let + isV6 = address.ip.family == IPv6 + ipField = if isV6: ("ip6", address.ip.address_v6.toField) + else: ("ip", address.ip.address_v4.toField) + tcpField = ((if isV6: "tcp6" else: "tcp"), address.udpPort.uint16.toField) + udpField = ((if isV6: "udp6" else: "udp"), address.tcpPort.uint16.toField) + + makeEnrAux(seqNum, pk, [ipField, tcpField, udpField]) proc getField(r: Record, name: string, field: var Field): bool = # It might be more correct to do binary search, @@ -176,7 +192,7 @@ proc verifySignature(r: Record): bool = var rlp = rlpFromBytes(r.raw.toRange) let sz = rlp.listLen rlp.enterList() - let sigData = rlp.read(seq[byte]) + let sigData = rlp.read(Bytes) let content = block: var writer = initRlpList(sz - 1) var reader = rlp @@ -195,7 +211,8 @@ proc verifySignature(r: Record): bool = discard proc fromBytesAux(r: var Record): bool = - if r.raw.len > maxEnrSize: return false + if r.raw.len > maxEnrSize: + return false var rlp = rlpFromBytes(r.raw.toRange) let sz = rlp.listLen @@ -206,7 +223,7 @@ proc fromBytesAux(r: var Record): bool = rlp.enterList() rlp.skipElem() # Skip signature - r.sequenceNumber = rlp.read(uint64) + r.seqNum = rlp.read(uint64) let numPairs = (sz - 2) div 2 @@ -219,12 +236,11 @@ proc fromBytesAux(r: var Record): bool = of "secp256k1": let pubkeyData = rlp.read(seq[byte]) r.pairs.add((k, Field(kind: kBytes, bytes: pubkeyData))) - of "tcp", "udp", "tcp6", "udp6", "ip": - let v = rlp.read(int) + of "tcp", "udp", "tcp6", "udp6": + let v = rlp.read(uint16) r.pairs.add((k, Field(kind: kNum, num: v))) else: - r.pairs.add((k, Field(kind: kBytes, bytes: rlp.rawData.toSeq))) - rlp.skipElem + r.pairs.add((k, Field(kind: kBytes, bytes: rlp.read(Bytes)))) verifySignature(r) diff --git a/eth/p2p/discoveryv5/protocol.nim b/eth/p2p/discoveryv5/protocol.nim index 811b78f..c678b64 100644 --- a/eth/p2p/discoveryv5/protocol.nim +++ b/eth/p2p/discoveryv5/protocol.nim @@ -297,7 +297,7 @@ proc processClient(transp: DatagramTransport, proc revalidateNode(p: Protocol, n: Node) {.async.} = let reqId = newRequestId() var ping: PingPacket - ping.enrSeq = p.localNode.record.sequenceNumber + ping.enrSeq = p.localNode.record.seqNum let (data, nonce) = p.codec.encodeEncrypted(n, encodePacket(ping, reqId), challenge = nil) p.pendingRequests[nonce] = PendingRequest(node: n, packet: data) p.send(n, data) @@ -305,7 +305,7 @@ proc revalidateNode(p: Protocol, n: Node) {.async.} = let resp = await p.waitPacket(n, reqId) if resp.isSome and resp.get.kind == pong: let pong = resp.get.pong - if pong.enrSeq > n.record.sequenceNumber: + if pong.enrSeq > n.record.seqNum: # TODO: Request new ENR discard diff --git a/tests/p2p/test_enr.nim b/tests/p2p/test_enr.nim index 8cd4d29..92d2dfb 100644 --- a/tests/p2p/test_enr.nim +++ b/tests/p2p/test_enr.nim @@ -1,11 +1,13 @@ -import unittest -import eth/p2p/discoveryv5/enr, eth/keys +import + net, unittest, options, + nimcrypto/utils, + eth/p2p/enode, eth/p2p/discoveryv5/enr, eth/keys, eth/rlp suite "ENR": test "Serialization": var pk = initPrivateKey("5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d") - var r = initRecord(123, pk, {"udp": 1234, "ip": 12345}) - doAssert($r == """(id: "v4", ip: 12345, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") + var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]}) + doAssert($r == """(id: "v4", ip: 0x05060708, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") let uri = r.toURI() var r2: Record let sigValid = r2.fromURI(uri) @@ -16,7 +18,7 @@ suite "ENR": var r: Record let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") doAssert(sigValid) - doAssert($r == """(id: "v4", ip: 2130706433, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""") + doAssert($r == """(id: "v4", ip: 0x7F000001, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""") test "Bad base64": var r: Record @@ -27,3 +29,25 @@ suite "ENR": var r: Record let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOOnrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") doAssert(not sigValid) + + test "Create from ENode address": + let + keys = newKeyPair() + ip = parseIpAddress("10.20.30.40") + enodeAddress = Address(ip: ip, tcpPort: Port 9000, udpPort: Port 9000) + enr = Record.init(100, keys.seckey, enodeAddress) + typedEnr = get enr.toTypedRecord + + check: + typedEnr.secp256k1.isSome + typedEnr.secp256k1.get == keys.pubkey.getRawCompressed + + typedEnr.ip.isSome + typedEnr.ip.get == [byte 10, 20, 30, 40] + + typedEnr.tcp.isSome + typedEnr.tcp.get == 9000 + + typedEnr.udp.isSome + typedEnr.udp.get == 9000 +