From 9a01791e117313d2548ac490f6ac90d34c24723c Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Mon, 7 Mar 2022 01:19:49 +0100 Subject: [PATCH] feat: Swap ENR to libp2p SignedPeerRecords Swap all instances of Record with SignedPeerRecord. Allow for `SignedPeerRecord`s to be updated by updating the first multiaddress in the `PeerRecord`. This also increments the `seqNo` in the `PeerRecord` only if the address was actually updated. --- libp2pdht.nimble | 2 +- libp2pdht/discv5.nim | 4 +- libp2pdht/discv5/enr.nim | 4 - libp2pdht/discv5/spr.nim | 4 + .../private/eth/p2p/discoveryv5/encoding.nim | 62 +- libp2pdht/private/eth/p2p/discoveryv5/enr.nim | 528 ------------------ .../private/eth/p2p/discoveryv5/messages.nim | 8 +- .../eth/p2p/discoveryv5/messages_encoding.nim | 2 +- .../private/eth/p2p/discoveryv5/node.nim | 25 +- .../p2p/discoveryv5/nodes_verification.nim | 26 +- .../private/eth/p2p/discoveryv5/protocol.nim | 125 +++-- .../eth/p2p/discoveryv5/routing_table.nim | 14 +- libp2pdht/private/eth/p2p/discoveryv5/spr.nim | 360 ++++++++++++ .../private/eth/p2p/discoveryv5/transport.nim | 8 +- tests/dht/test_helper.nim | 73 ++- tests/dht/test_providers.nim | 15 +- tests/discv5/discv5_test_helper.nim | 87 --- tests/discv5/test_discoveryv5.nim | 183 +++--- tests/discv5/test_discoveryv5_encoding.nim | 156 +++--- 19 files changed, 744 insertions(+), 942 deletions(-) delete mode 100644 libp2pdht/discv5/enr.nim create mode 100644 libp2pdht/discv5/spr.nim delete mode 100644 libp2pdht/private/eth/p2p/discoveryv5/enr.nim create mode 100644 libp2pdht/private/eth/p2p/discoveryv5/spr.nim delete mode 100644 tests/discv5/discv5_test_helper.nim diff --git a/libp2pdht.nimble b/libp2pdht.nimble index 8fe958f..80f43ca 100644 --- a/libp2pdht.nimble +++ b/libp2pdht.nimble @@ -14,7 +14,7 @@ requires "nim >= 1.2.0", "chronicles >= 0.10.2 & < 0.11.0", "chronos >= 3.0.11 & < 3.1.0", "eth >= 1.0.0 & < 1.1.0", # to be removed in https://github.com/status-im/nim-libp2p-dht/issues/2 - "libp2p#22fe39819ae8b3118a59e3962ea42087f878c5b6", + "libp2p#c7504d2446717a48a79c8b15e0f21bbfc84957ba", "metrics", "protobufserialization >= 0.2.0 & < 0.3.0", "secp256k1 >= 0.5.2 & < 0.6.0", diff --git a/libp2pdht/discv5.nim b/libp2pdht/discv5.nim index 61e011e..a7f0f50 100644 --- a/libp2pdht/discv5.nim +++ b/libp2pdht/discv5.nim @@ -1,4 +1,4 @@ import - ./discv5/[enr, encoding, messages, messages_encoding, node, nodes_verification, protocol, routing_table, sessions, transport] + ./discv5/[spr, encoding, messages, messages_encoding, node, nodes_verification, protocol, routing_table, sessions, transport] -export enr, encoding, messages, messages_encoding, node, nodes_verification, protocol, routing_table, sessions, transport \ No newline at end of file +export spr, encoding, messages, messages_encoding, node, nodes_verification, protocol, routing_table, sessions, transport \ No newline at end of file diff --git a/libp2pdht/discv5/enr.nim b/libp2pdht/discv5/enr.nim deleted file mode 100644 index 594395a..0000000 --- a/libp2pdht/discv5/enr.nim +++ /dev/null @@ -1,4 +0,0 @@ -import - ../private/eth/p2p/discoveryv5/enr - -export enr \ No newline at end of file diff --git a/libp2pdht/discv5/spr.nim b/libp2pdht/discv5/spr.nim new file mode 100644 index 0000000..1a7b044 --- /dev/null +++ b/libp2pdht/discv5/spr.nim @@ -0,0 +1,4 @@ +import + ../private/eth/p2p/discoveryv5/spr + +export spr \ No newline at end of file diff --git a/libp2pdht/private/eth/p2p/discoveryv5/encoding.nim b/libp2pdht/private/eth/p2p/discoveryv5/encoding.nim index 7115778..06807c3 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/encoding.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/encoding.nim @@ -17,7 +17,8 @@ import std/[tables, options, hashes, net], nimcrypto, stint, chronicles, bearssl, stew/[results, byteutils], metrics, eth/[rlp, keys], - "."/[messages, messages_encoding, node, enr, hkdf, sessions] + libp2p/signed_envelope, + "."/[messages, messages_encoding, node, spr, hkdf, sessions] from stew/objects import checkedEnumAssign @@ -56,7 +57,7 @@ type Challenge* = object whoareyouData*: WhoareyouData - pubkey*: Option[PublicKey] + pubkey*: Option[keys.PublicKey] StaticHeader* = object flag: Flag @@ -92,7 +93,7 @@ type Codec* = object localNode*: Node - privKey*: PrivateKey + privKey*: keys.PrivateKey handshakes*: Table[HandshakeKey, Challenge] sessions*: Sessions @@ -116,16 +117,16 @@ proc idHash(challengeData, ephkey: openArray[byte], nodeId: NodeId): result = ctx.finish() ctx.clear() -proc createIdSignature*(privKey: PrivateKey, challengeData, +proc createIdSignature*(privKey: keys.PrivateKey, challengeData, ephKey: openArray[byte], nodeId: NodeId): SignatureNR = signNR(privKey, SkMessage(idHash(challengeData, ephKey, nodeId).data)) proc verifyIdSignature*(sig: SignatureNR, challengeData, ephKey: openArray[byte], - nodeId: NodeId, pubkey: PublicKey): bool = + nodeId: NodeId, pubkey: keys.PublicKey): bool = let h = idHash(challengeData, ephKey, nodeId) verify(sig, SkMessage(h.data), pubkey) -proc deriveKeys*(n1, n2: NodeId, priv: PrivateKey, pub: PublicKey, +proc deriveKeys*(n1, n2: NodeId, priv: keys.PrivateKey, pub: keys.PublicKey, challengeData: openArray[byte]): HandshakeSecrets = let eph = ecdhRawFull(priv, pub) @@ -235,7 +236,7 @@ proc encodeMessagePacket*(rng: var BrHmacDrbgContext, c: var Codec, proc encodeWhoareyouPacket*(rng: var BrHmacDrbgContext, c: var Codec, toId: NodeId, toAddr: Address, requestNonce: AESGCMNonce, recordSeq: uint64, - pubkey: Option[PublicKey]): seq[byte] = + pubkey: Option[keys.PublicKey]): seq[byte] = var idNonce: IdNonce brHmacDrbgGenerate(rng, idNonce) @@ -277,7 +278,7 @@ proc encodeWhoareyouPacket*(rng: var BrHmacDrbgContext, c: var Codec, proc encodeHandshakePacket*(rng: var BrHmacDrbgContext, c: var Codec, toId: NodeId, toAddr: Address, message: openArray[byte], - whoareyouData: WhoareyouData, pubkey: PublicKey): seq[byte] = + whoareyouData: WhoareyouData, pubkey: keys.PublicKey): seq[byte] = var header: seq[byte] var nonce: AESGCMNonce brHmacDrbgGenerate(rng, nonce) @@ -292,7 +293,7 @@ proc encodeHandshakePacket*(rng: var BrHmacDrbgContext, c: var Codec, authdataHead.add(33'u8) # eph-key-size: 33 authdata.add(authdataHead) - let ephKeys = KeyPair.random(rng) + let ephKeys = keys.KeyPair.random(rng) let signature = createIdSignature(c.privKey, whoareyouData.challengeData, ephKeys.pubkey.toRawCompressed(), toId) @@ -300,9 +301,15 @@ proc encodeHandshakePacket*(rng: var BrHmacDrbgContext, c: var Codec, # compressed pub key format (33 bytes) authdata.add(ephKeys.pubkey.toRawCompressed()) - # Add ENR of sequence number is newer + # Add SPR of sequence number is newer if whoareyouData.recordSeq < c.localNode.record.seqNum: - authdata.add(encode(c.localNode.record)) + let encoded = c.localNode.record.encode + if encoded.isOk: + trace "Encoded local node's SignedPeerRecord", bytes = encoded.get + authdata.add(encoded.get) + else: + error "Failed to encode local node's SignedPeerRecord", error = encoded.error + authdata.add(@[]) let secrets = deriveKeys(c.localNode.id, toId, ephKeys.seckey, pubkey, whoareyouData.challengeData) @@ -312,6 +319,7 @@ proc encodeHandshakePacket*(rng: var BrHmacDrbgContext, c: var Codec, authdata.len()) header.add(staticHeader) + trace "Handshake packet's authdata", authdata header.add(authdata) c.sessions.store(toId, toAddr, secrets.recipientKey, secrets.initiatorKey) @@ -446,7 +454,7 @@ proc decodeHandshakePacket(c: var Codec, fromAddr: Address, nonce: AESGCMNonce, sigSize = uint8(authdata[32]) ephKeySize = uint8(authdata[33]) - # If smaller, as it can be equal and bigger (in case it holds an enr) + # If smaller, as it can be equal and bigger (in case it holds an spr) if header.len < staticHeaderSize + authdataHeadSize + int(sigSize) + int(ephKeySize): return err("Invalid header for handshake message packet") @@ -461,40 +469,44 @@ proc decodeHandshakePacket(c: var Codec, fromAddr: Address, nonce: AESGCMNonce, let ephKeyPos = authdataHeadSize + int(sigSize) ephKeyRaw = authdata[ephKeyPos.. recordPos: - # There is possibly an ENR still + # There is possibly an SPR still try: + trace "Decoding handshake packet's authdata", authdata, recordPos, decodeBytes = authdata.toOpenArray(recordPos, authdata.high) # Signature check of record happens in decode. - record = some(rlp.decode(authdata.toOpenArray(recordPos, authdata.high), - enr.Record)) + let + prBytes = @(authdata.toOpenArray(recordPos, authdata.high)) + decoded = SignedPeerRecord.decode(prBytes) + .expect("Should be valid bytes for SignedPeerRecord") + record = some(decoded) except RlpError, ValueError: - return err("Invalid encoded ENR") + return err("Invalid encoded SPR") - var pubkey: PublicKey + var pubkey: keys.PublicKey var newNode: Option[Node] - # TODO: Shall we return Node or Record? Record makes more sense, but we do - # need the pubkey and the nodeid + # TODO: Shall we return Node or SignedPeerRecord? SignedPeerRecord makes + # more sense, but we do need the pubkey and the nodeid if record.isSome(): # Node returned might not have an address or not a valid address. let node = ? newNode(record.get()) if node.id != srcId: - return err("Invalid node id: does not match node id of ENR") + return err("Invalid node id: does not match node id of SPR") # Note: Not checking if the record seqNum is higher than the one we might # have stored as it comes from this node directly. pubkey = node.pubkey newNode = some(node) else: - # TODO: Hmm, should we still verify node id of the ENR of this node? + # TODO: Hmm, should we still verify node id of the SPR of this node? if challenge.pubkey.isSome(): pubkey = challenge.pubkey.get() else: - # We should have received a Record in this case. - return err("Missing ENR in handshake packet") + # We should have received a SignedPeerRecord in this case. + return err("Missing SPR in handshake packet") # Verify the id-signature let sig = ? SignatureNR.fromRaw( diff --git a/libp2pdht/private/eth/p2p/discoveryv5/enr.nim b/libp2pdht/private/eth/p2p/discoveryv5/enr.nim deleted file mode 100644 index a774c6f..0000000 --- a/libp2pdht/private/eth/p2p/discoveryv5/enr.nim +++ /dev/null @@ -1,528 +0,0 @@ -# nim-eth - Node Discovery Protocol v5 -# Copyright (c) 2020-2021 Status Research & Development GmbH -# Licensed and distributed under either of -# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). -# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). -# at your option. This file may not be copied, modified, or distributed except according to those terms. -# -## ENR implementation according to specification in EIP-778: -## https://github.com/ethereum/EIPs/blob/master/EIPS/eip-778.md - -{.push raises: [Defect].} - -import - std/[strutils, macros, algorithm, options], - stew/shims/net, stew/[base64, results], nimcrypto, - eth/[rlp, keys] - -export options, results - -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. - -type - FieldPair* = (string, Field) - - Record* = object - seqNum*: uint64 - # signature: seq[byte] - raw*: seq[byte] # RLP encoded record - pairs: seq[FieldPair] # sorted list of all key/value pairs - - EnrUri* = distinct string - - TypedRecord* = object - id*: string - secp256k1*: Option[array[33, byte]] - ip*: Option[array[4, byte]] - ip6*: Option[array[16, byte]] - tcp*: Option[int] - udp*: Option[int] - tcp6*: Option[int] - udp6*: Option[int] - - FieldKind = enum - kString, - kNum, - kBytes, - kList - - Field = object - case kind: FieldKind - of kString: - str: string - of kNum: - num: BiggestUInt - of kBytes: - bytes: seq[byte] - of kList: - listRaw: seq[byte] ## Differently from the other kinds, this is is stored - ## as raw (encoded) RLP data, and thus treated as such further on. - - EnrResult*[T] = Result[T, cstring] - -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 SomeUnsignedInt: - Field(kind: kNum, num: BiggestUInt(v)) - elif T is object|tuple: - Field(kind: kList, listRaw: rlp.encode(v)) - else: - {.error: "Unsupported field type".} - -proc `==`(a, b: Field): bool = - if a.kind == b.kind: - case a.kind - of kString: - return a.str == b.str - of kNum: - return a.num == b.num - of kBytes: - return a.bytes == b.bytes - of kList: - return a.listRaw == b.listRaw - else: - return false - -proc cmp(a, b: FieldPair): int = cmp(a[0], b[0]) - -proc makeEnrRaw(seqNum: uint64, pk: PrivateKey, - pairs: openArray[FieldPair]): EnrResult[seq[byte]] = - proc append(w: var RlpWriter, seqNum: uint64, - pairs: openArray[FieldPair]): seq[byte] = - w.append(seqNum) - for (k, v) in pairs: - w.append(k) - case v.kind - of kString: w.append(v.str) - of kNum: w.append(v.num) - of kBytes: w.append(v.bytes) - 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 sig = signNR(pk, toSign) - - var raw = block: - var w = initRlpList(pairs.len * 2 + 2) - w.append(sig.toRaw()) - w.append(seqNum, pairs) - - if raw.len > maxEnrSize: - err("Record exceeds maximum size") - else: - ok(raw) - -proc makeEnrAux(seqNum: uint64, pk: PrivateKey, - pairs: openArray[FieldPair]): EnrResult[Record] = - var record: Record - record.pairs = @pairs - record.seqNum = seqNum - - let pubkey = pk.toPublicKey() - - record.pairs.add(("id", Field(kind: kString, str: "v4"))) - record.pairs.add(("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 = - ## Initialize a `Record` with given sequence number, private key and k:v - ## pairs. - ## - ## Can fail in case the record exceeds the `maxEnrSize`. - for c in pairs: - c.expectKind(nnkExprColonExpr) - c[1] = newCall(bindSym"toField", c[1]) - - result = quote do: - makeEnrAux(`seqNum`, `pk`, `pairs`) - -template toFieldPair*(key: string, value: auto): FieldPair = - (key, toField(value)) - -proc addAddress(fields: var seq[FieldPair], ip: Option[ValidIpAddress], - tcpPort, udpPort: Option[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. - if ip.isSome(): - let - ipExt = ip.get() - isV6 = ipExt.family == IPv6 - - 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)) - -proc init*(T: type Record, seqNum: uint64, - pk: PrivateKey, - ip: Option[ValidIpAddress], - tcpPort, udpPort: Option[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`. - 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) - -proc getField(r: Record, name: string, field: var Field): bool = - # It might be more correct to do binary search, - # as the fields are sorted, but it's unlikely to - # make any difference in reality. - for (k, v) in r.pairs: - if k == name: - field = v - return true - -proc requireKind(f: Field, kind: FieldKind): EnrResult[void] = - if f.kind != kind: - err("Wrong field kind") - else: - ok() - -proc get*(r: Record, key: string, T: type): EnrResult[T] = - ## Get the value from the provided key. - var f: Field - if r.getField(key, f): - when T is SomeInteger: - ? requireKind(f, kNum) - ok(T(f.num)) - elif T is seq[byte]: - ? requireKind(f, kBytes) - ok(f.bytes) - elif T is string: - ? requireKind(f, kString) - ok(f.str) - elif T is PublicKey: - ? requireKind(f, kBytes) - let pk = PublicKey.fromRaw(f.bytes) - if pk.isErr: - err("Invalid public key") - else: - ok(pk[]) - elif T is array: - when type(default(T)[low(T)]) is byte: - ? requireKind(f, kBytes) - if f.bytes.len != T.len: - err("Invalid byte blob length") - else: - var res: T - copyMem(addr res[0], addr f.bytes[0], res.len) - ok(res) - else: - {.fatal: "Unsupported output type in enr.get".} - else: - {.fatal: "Unsupported output type in enr.get".} - else: - err("Key not found in ENR") - -proc get*(r: Record, T: type PublicKey): Option[T] = - ## Get the `PublicKey` from provided `Record`. Return `none` when there is - ## no `PublicKey` in the record. - var pubkeyField: Field - if r.getField("secp256k1", pubkeyField) and pubkeyField.kind == kBytes: - let pk = PublicKey.fromRaw(pubkeyField.bytes) - if pk.isOk: - return some pk[] - -proc find(r: Record, key: string): Option[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 some(i) - -proc 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(r.seqNum): # highly unlikely - return err("Maximum sequence number reached") - r.seqNum.inc() - r.raw = ? makeEnrRaw(r.seqNum, pk, r.pairs) - record = r - - ok() - -proc update*(r: var Record, pk: PrivateKey, - ip: Option[ValidIpAddress], - tcpPort, udpPort: Option[Port] = none[Port](), - extraFields: openArray[FieldPair] = []): - EnrResult[void] = - ## 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. - ## - ## 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: Make updating of both ipv4 and ipv6 address in ENR more convenient. - fields.addAddress(ip, tcpPort, udpPort) - fields.add extraFields - r.update(pk, fields) - -proc tryGet*(r: Record, key: string, T: type): Option[T] = - ## Get the value from the provided key. - ## Return `none` if the key does not exist or if the value is invalid - ## according to type `T`. - let val = get(r, key, T) - if val.isOk(): - some(val.get()) - else: - none(T) - -proc toTypedRecord*(r: Record): EnrResult[TypedRecord] = - let id = r.tryGet("id", string) - if id.isSome: - var tr: TypedRecord - tr.id = id.get - - template readField(fieldName: untyped) {.dirty.} = - tr.fieldName = tryGet(r, astToStr(fieldName), type(tr.fieldName.get)) - - readField secp256k1 - readField ip - readField ip6 - readField tcp - readField tcp6 - readField udp - readField udp6 - - ok(tr) - else: - err("Record without id field") - -proc contains*(r: Record, fp: (string, seq[byte])): bool = - # TODO: use FieldPair for this, but that is a bit cumbersome. Perhaps the - # `get` call can be improved to make this easier. - let field = r.tryGet(fp[0], seq[byte]) - if field.isSome(): - if field.get() == fp[1]: - return true - -proc verifySignatureV4(r: Record, sigData: openArray[byte], content: seq[byte]): - bool = - let publicKey = r.get(PublicKey) - if publicKey.isSome: - let sig = SignatureNR.fromRaw(sigData) - if sig.isOk: - var h = keccak256.digest(content) - return verify(sig[], SkMessage(h.data), publicKey.get) - -proc verifySignature(r: Record): bool {.raises: [RlpError, Defect].} = - var rlp = rlpFromBytes(r.raw) - let sz = rlp.listLen - if not rlp.enterList: - return false - let sigData = rlp.read(seq[byte]) - let content = block: - var writer = initRlpList(sz - 1) - var reader = rlp - for i in 1 ..< sz: - writer.appendRawBytes(reader.rawData) - reader.skipElem - writer.finish() - - var id: Field - if r.getField("id", id) and id.kind == kString: - case id.str - of "v4": - result = verifySignatureV4(r, sigData, content) - else: - # Unknown Identity Scheme - discard - -proc fromBytesAux(r: var Record): bool {.raises: [RlpError, Defect].} = - if r.raw.len > maxEnrSize: - return false - - var rlp = rlpFromBytes(r.raw) - if not rlp.isList: - return false - - let sz = rlp.listLen - if sz < minRlpListLen or sz mod 2 != 0: - # Wrong rlp object - return false - - # We already know we are working with a list - doAssert rlp.enterList() - rlp.skipElem() # Skip signature - - r.seqNum = rlp.read(uint64) - - let numPairs = (sz - 2) div 2 - - for i in 0 ..< numPairs: - let k = rlp.read(string) - case k - of "id": - let id = rlp.read(string) - r.pairs.add((k, Field(kind: kString, str: id))) - of "secp256k1": - let pubkeyData = rlp.read(seq[byte]) - r.pairs.add((k, Field(kind: kBytes, bytes: pubkeyData))) - of "tcp", "udp", "tcp6", "udp6": - let v = rlp.read(uint16) - r.pairs.add((k, Field(kind: kNum, num: v))) - else: - # Don't know really what this is supposed to represent so drop it in - # `kBytes` field pair when a single byte or blob. - if rlp.isSingleByte() or rlp.isBlob(): - r.pairs.add((k, Field(kind: kBytes, bytes: rlp.read(seq[byte])))) - elif rlp.isList(): - # Not supporting decoding lists as value (especially unknown ones), - # just drop the raw RLP value in there. - r.pairs.add((k, Field(kind: kList, listRaw: @(rlp.rawData())))) - # Need to skip the element still. - rlp.skipElem() - - verifySignature(r) - -proc fromBytes*(r: var Record, s: openArray[byte]): bool = - ## Loads ENR from rlp-encoded bytes, and validates the signature. - r.raw = @s - try: - result = fromBytesAux(r) - except RlpError: - discard - -proc fromBase64*(r: var Record, s: string): bool = - ## Loads ENR from base64-encoded rlp-encoded bytes, and validates the - ## signature. - try: - r.raw = Base64Url.decode(s) - result = fromBytesAux(r) - except RlpError, Base64Error: - discard - -proc fromURI*(r: var Record, s: string): bool = - ## Loads ENR from its text encoding: base64-encoded rlp-encoded bytes, - ## prefixed with "enr:". Validates the signature. - const prefix = "enr:" - if s.startsWith(prefix): - result = r.fromBase64(s[prefix.len .. ^1]) - -template fromURI*(r: var Record, url: EnrUri): bool = - fromURI(r, string(url)) - -proc toBase64*(r: Record): string = - result = Base64Url.encode(r.raw) - -proc toURI*(r: Record): string = "enr:" & r.toBase64 - -proc `$`(f: Field): string = - case f.kind - of kNum: - $f.num - of kBytes: - "0x" & f.bytes.toHex - of kString: - "\"" & f.str & "\"" - of kList: - "(Raw RLP list) " & "0x" & f.listRaw.toHex - -proc `$`*(r: Record): string = - result = "(" - result &= $r.seqNum - for (k, v) in r.pairs: - result &= ", " - result &= k - result &= ": " - # For IP addresses we print something prettier than the default kinds - # Note: Could disallow for invalid IPs in ENR also. - if k == "ip": - let ip = r.tryGet("ip", array[4, byte]) - if ip.isSome(): - result &= $ipv4(ip.get()) - else: - result &= "(Invalid) " & $v - elif k == "ip6": - let ip = r.tryGet("ip6", array[16, byte]) - if ip.isSome(): - result &= $ipv6(ip.get()) - else: - result &= "(Invalid) " & $v - else: - result &= $v - result &= ')' - -proc `==`*(a, b: Record): bool = a.raw == b.raw - -proc read*(rlp: var Rlp, T: typedesc[Record]): - T {.raises: [RlpError, ValueError, Defect].} = - if not rlp.hasData() or not result.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") - rlp.skipElem() - -proc append*(rlpWriter: var RlpWriter, value: Record) = - rlpWriter.appendRawBytes(value.raw) diff --git a/libp2pdht/private/eth/p2p/discoveryv5/messages.nim b/libp2pdht/private/eth/p2p/discoveryv5/messages.nim index 8ce4d66..7198006 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/messages.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/messages.nim @@ -15,7 +15,7 @@ import std/[hashes, net], eth/[keys], - ./enr, + ./spr, ../../../../dht/providers_messages export providers_messages @@ -45,10 +45,10 @@ type id*: seq[byte] PingMessage* = object - enrSeq*: uint64 + sprSeq*: uint64 PongMessage* = object - enrSeq*: uint64 + sprSeq*: uint64 ip*: IpAddress port*: uint16 @@ -57,7 +57,7 @@ type NodesMessage* = object total*: uint32 - enrs*: seq[Record] + sprs*: seq[SignedPeerRecord] TalkReqMessage* = object protocol*: seq[byte] diff --git a/libp2pdht/private/eth/p2p/discoveryv5/messages_encoding.nim b/libp2pdht/private/eth/p2p/discoveryv5/messages_encoding.nim index ecb462b..882ad80 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/messages_encoding.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/messages_encoding.nim @@ -15,7 +15,7 @@ import chronicles, libp2p/routing_record, libp2p/signed_envelope, - "."/[messages, enr], + "."/[messages, spr], ../../../../dht/providers_encoding from stew/objects import checkedEnumAssign diff --git a/libp2pdht/private/eth/p2p/discoveryv5/node.nim b/libp2pdht/private/eth/p2p/discoveryv5/node.nim index 4a9bbdb..cf61e47 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/node.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/node.nim @@ -11,7 +11,7 @@ import std/hashes, nimcrypto, stint, chronos, stew/shims/net, chronicles, eth/keys, eth/net/utils, - ./enr + ./spr export stint @@ -24,27 +24,27 @@ type Node* = ref object id*: NodeId - pubkey*: PublicKey + pubkey*: keys.PublicKey address*: Option[Address] - record*: Record + record*: SignedPeerRecord seen*: bool ## Indicates if there was at least one successful ## request-response with this node. -func toNodeId*(pk: PublicKey): NodeId = +func toNodeId*(pk: keys.PublicKey): NodeId = ## Convert public key to a node identifier. - # Keccak256 hash is used as defined in ENR spec for scheme v4: + # Keccak256 hash is used as defined in SPR spec for scheme v4: # https://github.com/ethereum/devp2p/blob/master/enr.md#v4-identity-scheme readUintBE[256](keccak256.digest(pk.toRaw()).data) -func newNode*(r: Record): Result[Node, cstring] = - ## Create a new `Node` from a `Record`. +func newNode*(r: SignedPeerRecord): Result[Node, cstring] = + ## Create a new `Node` from a `SignedPeerRecord`. # TODO: Handle IPv6 - let pk = r.get(PublicKey) + let pk = r.get(keys.PublicKey) # This check is redundant for a properly created record as the deserialization # of a record will fail at `verifySignature` if there is no public key. if pk.isNone(): - return err("Could not recover public key from ENR") + return err("Could not recover public key from SPR") # Also this can not fail for a properly created record as id is checked upon # deserialization. @@ -58,10 +58,9 @@ func newNode*(r: Record): Result[Node, cstring] = ok(Node(id: pk.get().toNodeId(), pubkey: pk.get(), record: r, address: none(Address))) -func update*(n: Node, pk: PrivateKey, ip: Option[ValidIpAddress], - tcpPort, udpPort: Option[Port] = none[Port](), - extraFields: openArray[FieldPair] = []): Result[void, cstring] = - ? n.record.update(pk, ip, tcpPort, udpPort, extraFields) +proc update*(n: Node, pk: keys.PrivateKey, ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port] = none[Port]()): Result[void, cstring] = + ? n.record.update(pk, ip, tcpPort, udpPort) if ip.isSome(): if udpPort.isSome(): diff --git a/libp2pdht/private/eth/p2p/discoveryv5/nodes_verification.nim b/libp2pdht/private/eth/p2p/discoveryv5/nodes_verification.nim index 45fd89f..d3cecf7 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/nodes_verification.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/nodes_verification.nim @@ -3,7 +3,7 @@ import std/[sets, options], stew/results, stew/shims/net, chronicles, chronos, - "."/[node, enr, routing_table] + "."/[node, spr, routing_table] logScope: topics = "nodes-verification" @@ -25,24 +25,24 @@ proc validIp(sender, address: IpAddress): bool = # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml return true -proc verifyNodesRecords(enrs: openArray[Record], fromNode: Node, nodesLimit: int, +proc verifyNodesRecords(sprs: openArray[SignedPeerRecord], fromNode: Node, nodesLimit: int, distances: Option[seq[uint16]]): seq[Node] = - ## Verify and convert ENRs to a sequence of nodes. Only ENRs that pass - ## verification will be added. ENRs are verified for duplicates, invalid + ## Verify and convert SPRs to a sequence of nodes. Only SPRs that pass + ## verification will be added. SPRs are verified for duplicates, invalid ## addresses and invalid distances if those are specified. var seen: HashSet[Node] var count = 0 - for r in enrs: - # Check and allow for processing of maximum `findNodeResultLimit` ENRs - # returned. This limitation is required so no huge lists of invalid ENRs + for r in sprs: + # Check and allow for processing of maximum `findNodeResultLimit` SPRs + # returned. This limitation is required so no huge lists of invalid SPRs # are processed for no reason, and for not overwhelming a routing table # with nodes from a malicious actor. - # The discovery v5 specification specifies no limit on the amount of ENRs + # The discovery v5 specification specifies no limit on the amount of SPRs # that can be returned, but clients usually stick with the bucket size limit # as in original Kademlia. Because of this it is chosen not to fail # immediatly, but still process maximum `findNodeResultLimit`. if count >= nodesLimit: - debug "Too many ENRs", enrs = enrs.len(), + debug "Too many SPRs", sprs = sprs.len(), limit = nodesLimit, sender = fromNode.record.toURI break @@ -79,8 +79,8 @@ proc verifyNodesRecords(enrs: openArray[Record], fromNode: Node, nodesLimit: int seen.incl(n) result.add(n) -proc verifyNodesRecords*(enrs: openArray[Record], fromNode: Node, nodesLimit: int): seq[Node] = - verifyNodesRecords(enrs, fromNode, nodesLimit, none[seq[uint16]]()) +proc verifyNodesRecords*(sprs: openArray[SignedPeerRecord], fromNode: Node, nodesLimit: int): seq[Node] = + verifyNodesRecords(sprs, fromNode, nodesLimit, none[seq[uint16]]()) -proc verifyNodesRecords*(enrs: openArray[Record], fromNode: Node, nodesLimit: int, distances: seq[uint16]): seq[Node] = - verifyNodesRecords(enrs, fromNode, nodesLimit, some[seq[uint16]](distances)) +proc verifyNodesRecords*(sprs: openArray[SignedPeerRecord], fromNode: Node, nodesLimit: int, distances: seq[uint16]): seq[Node] = + verifyNodesRecords(sprs, fromNode, nodesLimit, some[seq[uint16]](distances)) diff --git a/libp2pdht/private/eth/p2p/discoveryv5/protocol.nim b/libp2pdht/private/eth/p2p/discoveryv5/protocol.nim index 0bdb1c0..9cf3d6b 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/protocol.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/protocol.nim @@ -76,13 +76,13 @@ import std/[tables, sets, options, math, sequtils, algorithm], stew/shims/net as stewNet, json_serialization/std/net, - stew/[endians2, results], chronicles, chronos, chronos/timer, stint, bearssl, + stew/[base64, endians2, results], chronicles, chronos, chronos/timer, stint, bearssl, metrics, eth/[rlp, keys, async_utils], libp2p/routing_record, - "."/[transport, messages, messages_encoding, node, routing_table, enr, random2, ip_vote, nodes_verification] + "."/[transport, messages, messages_encoding, node, routing_table, spr, random2, ip_vote, nodes_verification] import nimcrypto except toHex -export options, results, node, enr +export options, results, node, spr declareCounter discovery_message_requests_outgoing, "Discovery protocol outgoing message requests", labels = ["response"] @@ -91,7 +91,7 @@ declareCounter discovery_message_requests_incoming, declareCounter discovery_unsolicited_messages, "Discovery protocol unsolicited or timed-out messages" declareCounter discovery_enr_auto_update, - "Amount of discovery IP:port address ENR auto updates" + "Amount of discovery IP:port address SPR auto updates" logScope: topics = "discv5" @@ -100,15 +100,15 @@ const alpha = 3 ## Kademlia concurrency factor lookupRequestLimit = 3 ## Amount of distances requested in a single Findnode ## message for a lookup or query - findNodeResultLimit = 16 ## Maximum amount of ENRs in the total Nodes messages + findNodeResultLimit = 16 ## Maximum amount of SPRs in the total Nodes messages ## that will be processed - maxNodesPerMessage = 3 ## Maximum amount of ENRs per individual Nodes message + maxNodesPerMessage = 3 ## Maximum amount of SPRs per individual Nodes message refreshInterval = 5.minutes ## Interval of launching a random query to ## refresh the routing table. revalidateMax = 10000 ## Revalidation of a peer is done between 0 and this ## value in milliseconds ipMajorityInterval = 5.minutes ## Interval for checking the latest IP:Port - ## majority and updating this when ENR auto update is set. + ## majority and updating this when SPR auto update is set. initialLookups = 1 ## Amount of lookups done when populating the routing table responseTimeout* = 4.seconds ## timeout for the response of a request-response ## call @@ -128,7 +128,7 @@ type revalidateLoop: Future[void] ipMajorityLoop: Future[void] lastLookup: chronos.Moment - bootstrapRecords*: seq[Record] + bootstrapRecords*: seq[SignedPeerRecord] ipVote: IpVote enrAutoUpdate: bool talkProtocols*: Table[seq[byte], TalkProtocol] # TODO: Table is a bit of @@ -159,24 +159,28 @@ proc addNode*(d: Protocol, node: Node): bool = else: return false -proc addNode*(d: Protocol, r: Record): bool = - ## Add `Node` from a `Record` to discovery routing table. +proc addNode*(d: Protocol, r: SignedPeerRecord): bool = + ## Add `Node` from a `SignedPeerRecord` to discovery routing table. ## - ## Returns false only if no valid `Node` can be created from the `Record` or + ## Returns false only if no valid `Node` can be created from the `SignedPeerRecord` or ## on the conditions of `addNode` from a `Node`. let node = newNode(r) if node.isOk(): return d.addNode(node[]) -proc addNode*(d: Protocol, enr: EnrUri): bool = - ## Add `Node` from a ENR URI to discovery routing table. +proc addNode*(d: Protocol, spr: SprUri): bool = + ## Add `Node` from a SPR URI to discovery routing table. ## - ## Returns false if no valid ENR URI, or on the conditions of `addNode` from - ## an `Record`. - var r: Record - let res = r.fromURI(enr) - if res: - return d.addNode(r) + ## Returns false if no valid SPR URI, or on the conditions of `addNode` from + ## an `SignedPeerRecord`. + try: + var r: SignedPeerRecord + let res = r.fromURI(spr) + if res: + return d.addNode(r) + except Base64Error as e: + error "Base64 error decoding SPR URI", error = e.msg + return false proc getNode*(d: Protocol, id: NodeId): Option[Node] = ## Get the node with id from the routing table. @@ -213,15 +217,17 @@ proc nodesDiscovered*(d: Protocol): int = d.routingTable.len func privKey*(d: Protocol): lent keys.PrivateKey = d.privateKey -func getRecord*(d: Protocol): Record = - ## Get the ENR of the local node. +func getRecord*(d: Protocol): SignedPeerRecord = + ## Get the SPR of the local node. d.localNode.record -proc updateRecord*( - d: Protocol, enrFields: openArray[(string, seq[byte])]): DiscResult[void] = +proc updateRecord*(d: Protocol): 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) + + # TODO: Do we need this proc? This simply serves so that seqNo will be + # incremented to satisfy the tests... + d.localNode.record.incSeqNo(d.privateKey) + # 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? @@ -240,27 +246,27 @@ proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address, reqId: RequestId, if nodes.len == 0: # In case of 0 nodes, a reply is still needed - d.sendNodes(toId, toAddr, NodesMessage(total: 1, enrs: @[]), reqId) + d.sendNodes(toId, toAddr, NodesMessage(total: 1, sprs: @[]), reqId) return var message: NodesMessage # TODO: Do the total calculation based on the max UDP packet size we want to - # send and the ENR size of all (max 16) nodes. + # send and the SPR size of all (max 16) nodes. # Which UDP packet size to take? 1280? 576? message.total = ceil(nodes.len / maxNodesPerMessage).uint32 for i in 0 ..< nodes.len: - message.enrs.add(nodes[i].record) - if message.enrs.len == maxNodesPerMessage: + message.sprs.add(nodes[i].record) + if message.sprs.len == maxNodesPerMessage: d.sendNodes(toId, toAddr, message, reqId) - message.enrs.setLen(0) + message.sprs.setLen(0) - if message.enrs.len != 0: + if message.sprs.len != 0: d.sendNodes(toId, toAddr, message, reqId) proc handlePing(d: Protocol, fromId: NodeId, fromAddr: Address, ping: PingMessage, reqId: RequestId) = - let pong = PongMessage(enrSeq: d.localNode.record.seqNum, ip: fromAddr.ip, + let pong = PongMessage(sprSeq: d.localNode.record.seqNum, ip: fromAddr.ip, port: fromAddr.port.uint16) trace "Respond message packet", dstId = fromId, address = fromAddr, kind = MessageKind.pong @@ -369,7 +375,7 @@ proc replaceNode(d: Protocol, n: Node) = # For now we never remove bootstrap nodes. It might make sense to actually # do so and to retry them only in case we drop to a really low amount of # peers in the routing table. - debug "Message request to bootstrap node failed", enr = toURI(n.record) + debug "Message request to bootstrap node failed", spr = toURI(n.record) proc waitMessage(d: Protocol, fromNode: Node, reqId: RequestId): @@ -384,7 +390,7 @@ proc waitMessage(d: Protocol, fromNode: Node, reqId: RequestId): d.awaitedMessages[key] = result proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): - Future[DiscResult[seq[Record]]] {.async.} = + Future[DiscResult[seq[SignedPeerRecord]]] {.async.} = ## Wait for one or more nodes replies. ## ## The first reply will hold the total number of replies expected, and based @@ -394,12 +400,12 @@ proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): var op = await d.waitMessage(fromNode, reqId) if op.isSome: if op.get.kind == nodes: - var res = op.get.nodes.enrs + var res = op.get.nodes.sprs let total = op.get.nodes.total for i in 1 ..< total: op = await d.waitMessage(fromNode, reqId) if op.isSome and op.get.kind == nodes: - res.add(op.get.nodes.enrs) + res.add(op.get.nodes.sprs) else: # No error on this as we received some nodes. break @@ -443,7 +449,7 @@ proc ping*(d: Protocol, toNode: Node): ## ## Returns the received pong message or an error. let reqId = d.sendRequest(toNode, - PingMessage(enrSeq: d.localNode.record.seqNum)) + PingMessage(sprSeq: d.localNode.record.seqNum)) let resp = await d.waitMessage(toNode, reqId) if resp.isSome(): @@ -464,7 +470,7 @@ proc findNode*(d: Protocol, toNode: Node, distances: seq[uint16]): ## Send a discovery findNode message. ## ## Returns the received nodes or an error. - ## Received ENRs are already validated and converted to `Node`. + ## Received SPRs are already validated and converted to `Node`. let reqId = d.sendRequest(toNode, FindNodeMessage(distances: distances)) let nodes = await d.waitNodes(toNode, reqId) @@ -799,8 +805,8 @@ proc revalidateNode*(d: Protocol, n: Node) {.async.} = if pong.isOk(): let res = pong.get() - if res.enrSeq > n.record.seqNum: - # Request new ENR + if res.sprSeq > n.record.seqNum: + # Request new SPR let nodes = await d.findNode(n, @[0'u16]) if nodes.isOk() and nodes[].len > 0: discard d.addNode(nodes[][0]) @@ -841,7 +847,7 @@ proc refreshLoop(d: Protocol) {.async.} = proc ipMajorityLoop(d: Protocol) {.async.} = ## When `enrAutoUpdate` is enabled, the IP:port combination returned - ## by the majority will be used to update the local ENR. + ## by the majority will be used to update the local SPR. ## This should be safe as long as the routing table is not overwhelmed by ## malicious nodes trying to provide invalid addresses. ## Why is that? @@ -869,14 +875,14 @@ proc ipMajorityLoop(d: Protocol) {.async.} = let res = d.localNode.update(d.privateKey, ip = some(address.ip), udpPort = some(address.port)) if res.isErr: - warn "Failed updating ENR with newly discovered external address", + warn "Failed updating SPR with newly discovered external address", majority, previous, error = res.error else: discovery_enr_auto_update.inc() - info "Updated ENR with newly discovered external address", + info "Updated SPR with newly discovered external address", majority, previous, uri = toURI(d.localNode.record) else: - warn "Discovered new external address but ENR auto update is off", + warn "Discovered new external address but SPR auto update is off", majority, previous else: debug "Discovered external address matches current address", majority, @@ -904,8 +910,8 @@ proc newProtocol*( enrIp: Option[ValidIpAddress], enrTcpPort, enrUdpPort: Option[Port], localEnrFields: openArray[(string, seq[byte])] = [], - bootstrapRecords: openArray[Record] = [], - previousRecord = none[enr.Record](), + bootstrapRecords: openArray[SignedPeerRecord] = [], + previousRecord = none[SignedPeerRecord](), bindPort: Port, bindIp = IPv4_any(), enrAutoUpdate = false, @@ -917,27 +923,30 @@ proc newProtocol*( # Anyhow, nim-beacon-chain would also require some changes to support port # remapping through NAT and this API is also subject to change once we # introduce support for ipv4 + ipv6 binding/listening. - let extraFields = mapIt(localEnrFields, toFieldPair(it[0], it[1])) + + # TODO: Implement SignedPeerRecord custom fields? + # let extraFields = mapIt(localEnrFields, toFieldPair(it[0], it[1])) + # TODO: - # - Defect as is now or return a result for enr errors? - # - In case incorrect key, allow for new enr based on new key (new node id)? - var record: Record + # - Defect as is now or return a result for spr errors? + # - In case incorrect key, allow for new spr based on new key (new node id)? + var record: SignedPeerRecord if previousRecord.isSome(): record = previousRecord.get() - record.update(privKey, enrIp, enrTcpPort, enrUdpPort, - extraFields).expect("Record within size limits and correct key") + record.update(privKey, enrIp, enrTcpPort, enrUdpPort) + .expect("SignedPeerRecord within size limits and correct key") else: - record = enr.Record.init(1, privKey, enrIp, enrTcpPort, enrUdpPort, - extraFields).expect("Record within size limits") + record = SignedPeerRecord.init(1, privKey, enrIp, enrTcpPort, enrUdpPort) + .expect("SignedPeerRecord within size limits") - info "ENR initialized", ip = enrIp, tcp = enrTcpPort, udp = enrUdpPort, + info "SPR initialized", ip = enrIp, tcp = enrTcpPort, udp = enrUdpPort, seqNum = record.seqNum, uri = toURI(record) if enrIp.isNone(): if enrAutoUpdate: - notice "No external IP provided for the ENR, this node will not be " & - "discoverable until the ENR is updated with the discovered external IP address" + notice "No external IP provided for the SPR, this node will not be " & + "discoverable until the SPR is updated with the discovered external IP address" else: - warn "No external IP provided for the ENR, this node will not be discoverable" + warn "No external IP provided for the SPR, this node will not be discoverable" let node = newNode(record).expect("Properly initialized record") diff --git a/libp2pdht/private/eth/p2p/discoveryv5/routing_table.nim b/libp2pdht/private/eth/p2p/discoveryv5/routing_table.nim index 441d99d..d5542d2 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/routing_table.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/routing_table.nim @@ -11,7 +11,7 @@ import std/[algorithm, times, sequtils, bitops, sets, options], stint, chronicles, metrics, bearssl, chronos, stew/shims/net as stewNet, eth/net/utils, - "."/[node, random2, enr] + "."/[node, random2, spr] export options @@ -67,9 +67,9 @@ type ## ## As entries are not verified (=contacted) immediately before or on entry, it ## is possible that a malicious node could fill (poison) the routing table or - ## a specific bucket with ENRs with IPs it does not control. The effect of + ## a specific bucket with SPRs with IPs it does not control. The effect of ## this would be that a node that actually owns the IP could have a difficult - ## time getting its ENR distrubuted in the DHT and as a consequence would + ## time getting its SPR distrubuted in the DHT and as a consequence would ## not be reached from the outside as much (or at all). However, that node can ## still search and find nodes to connect to. So it would practically be a ## similar situation as a node that is not reachable behind the NAT because @@ -102,7 +102,7 @@ func logDistance*(a, b: NodeId): uint16 = ## ## According the specification, this is the log base 2 of the distance. But it ## is rather the log base 2 of the distance + 1, as else the 0 value can not - ## be used (e.g. by FindNode call to return peer its own ENR) + ## be used (e.g. by FindNode call to return peer its own SPR) ## For NodeId of 256 bits, range is 0-256. let a = a.toBytesBE let b = b.toBytesBE @@ -330,7 +330,7 @@ proc addNode*(r: var RoutingTable, n: Node): NodeStatus = ## total routing table, the node will not be added to the bucket, nor its ## replacement cache. - # Don't allow nodes without an address field in the ENR to be added. + # Don't allow nodes without an address field in the SPR to be added. # This could also be reworked by having another Node type that always has an # address. if n.address.isNone(): @@ -351,7 +351,7 @@ proc addNode*(r: var RoutingTable, n: Node): NodeStatus = if not ipLimitInc(r, bucket, n): return IpLimitReached ipLimitDec(r, bucket, bucket.nodes[nodeIdx]) - # Copy over the seen status, we trust here that after the ENR update the + # Copy over the seen status, we trust here that after the SPR update the # node will still be reachable, but it might not be the case. n.seen = bucket.nodes[nodeIdx].seen bucket.nodes[nodeIdx] = n @@ -368,7 +368,7 @@ proc addNode*(r: var RoutingTable, n: Node): NodeStatus = # newly additions are added as least recently seen (in fact they have not been # seen yet from our node its perspective). # However, in discovery v5 a node can also be added after a incoming request - # if a handshake is done and an ENR is provided, and considering that this + # if a handshake is done and an SPR is provided, and considering that this # handshake needs to be done, it is more likely that this node is reachable. # However, it is not certain and depending on different NAT mechanisms and # timers it might still fail. For this reason we currently do not add a way to diff --git a/libp2pdht/private/eth/p2p/discoveryv5/spr.nim b/libp2pdht/private/eth/p2p/discoveryv5/spr.nim new file mode 100644 index 0000000..2f31158 --- /dev/null +++ b/libp2pdht/private/eth/p2p/discoveryv5/spr.nim @@ -0,0 +1,360 @@ +# Copyright (c) 2020-2022 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. +# +import + chronicles, + std/[options, sequtils, strutils, sugar], + pkg/stew/[results, byteutils, arrayops], + stew/endians2, + stew/shims/net, + stew/base64, + eth/rlp, + eth/keys, + libp2p/crypto/crypto, + libp2p/crypto/secp, + libp2p/routing_record, + libp2p/multicodec + +export routing_record + +from chronos import TransportAddress, initTAddress + +export options, results + +type + SprUri* = distinct string + + RecordResult*[T] = Result[T, cstring] + +proc seqNum*(r: SignedPeerRecord): uint64 = + r.data.seqNo + +#proc encode +proc append*(rlpWriter: var RlpWriter, value: SignedPeerRecord) = + # echo "encoding to:" & $value.signedPeerRecord.encode.get + var encoded = value.encode + trace "Encoding SignedPeerRecord for RLP", bytes = encoded.get(@[]) + if encoded.isErr: + error "Error encoding SignedPeerRecord for RLP", error = encoded.error + rlpWriter.append encoded.get(@[]) + +proc fromBytes(r: var SignedPeerRecord, s: openArray[byte]): bool = + trace "Decoding SignedPeerRecord for RLP", bytes = s + + let decoded = SignedPeerRecord.decode(@s) + if decoded.isErr: + error "Error decoding SignedPeerRecord", error = decoded.error + return false + + r = decoded.get + return true + +proc read*(rlp: var Rlp, T: typedesc[SignedPeerRecord]): + T {.raises: [RlpError, ValueError, Defect].} = + # echo "read:" & $rlp.rawData + ## code directly borrowed from spr.nim + trace "Reading RLP SignedPeerRecord", rawData = rlp.rawData, toBytes = rlp.toBytes + if not rlp.hasData() or not result.fromBytes(rlp.toBytes): + # 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") + rlp.skipElem() + +proc get*(r: SignedPeerRecord, T: type crypto.PublicKey): Option[T] = + ## Get the `PublicKey` from provided `Record`. Return `none` when there is + ## no `PublicKey` in the record. + some(r.envelope.publicKey) + +func pkToPk(pk: crypto.PublicKey) : Option[keys.PublicKey] = + some((keys.PublicKey)(pk.skkey)) + +func pkToPk(pk: keys.PublicKey) : Option[crypto.PublicKey] = + some(crypto.PublicKey.init((secp.SkPublicKey)(pk))) + +func pkToPk(pk: crypto.PrivateKey) : Option[keys.PrivateKey] = + some((keys.PrivateKey)(pk.skkey)) + +func pkToPk(pk: keys.PrivateKey) : Option[crypto.PrivateKey] = + some(crypto.PrivateKey.init((secp.SkPrivateKey)(pk))) + +proc get*(r: SignedPeerRecord, T: type keys.PublicKey): Option[T] = + ## Get the `PublicKey` from provided `Record`. Return `none` when there is + ## no `PublicKey` in the record. + ## PublicKey* = distinct SkPublicKey + let + pk = r.envelope.publicKey + pkToPk(pk) + +proc incSeqNo*( + r: var SignedPeerRecord, + pk: keys.PrivateKey): RecordResult[void] = + + let cryptoPk = pk.pkToPk.get() # TODO: remove when eth/keys removed + + r.data.seqNo.inc() + r = ? SignedPeerRecord.init(cryptoPk, r.data).mapErr( + (e: CryptoError) => + ("Error initialising SignedPeerRecord with incremented seqNo: " & + $e).cstring + ) + ok() + + +proc update*(r: var SignedPeerRecord, pk: crypto.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port] = none[Port]()): + RecordResult[void] = + ## Update a `SignedPeerRecord` 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. + ## + ## Can fail in case of wrong `PrivateKey`, if the size of the resulting record + ## exceeds `maxSprSize` or if maximum sequence number is reached. The `Record` + ## will not be altered in these cases. + + # TODO: handle custom field pairs? + # TODO: We have a mapping issue here because PeerRecord has multiple + # addresses and the proc signature only allows updating of a single + # ip/tcpPort/udpPort/extraFields + + let + pubkey = r.get(crypto.PublicKey) + keysPubKey = pubkey.get.pkToPk.get # remove when move away from eth/keys + keysPrivKey = pk.pkToPk.get + if pubkey.isNone() or keysPubKey != keysPrivKey.toPublicKey: + return err("Public key does not correspond with given private key") + + var + changed = false + transProto = IpTransportProtocol.udpProtocol + transProtoPort: Port + + var updated: MultiAddress + + if r.data.addresses.len == 0: + changed = true + if ip.isNone: + return err "No existing address in SignedPeerRecord with no IP provided" + + if udpPort.isNone and tcpPort.isNone: + return err "No existing address in SignedPeerRecord with no port provided" + + let ipAddr = try: ValidIpAddress.init(ip.get) + except ValueError as e: + return err ("Existing address contains invalid address: " & $e.msg).cstring + if tcpPort.isSome: + transProto = IpTransportProtocol.tcpProtocol + transProtoPort = tcpPort.get + if udpPort.isSome: + transProto = IpTransportProtocol.udpProtocol + transProtoPort = udpPort.get + + updated = MultiAddress.init(ipAddr, transProto, transProtoPort) + + else: + let + existing = r.data.addresses[0].address + existingNetProto = ? existing[0].mapErr((e: string) => e.cstring) + existingTransProto = ? existing[1].mapErr((e: string) => e.cstring) + existingNetProtoFam = ? existingNetProto.protoCode + .mapErr((e: string) => e.cstring) + existingNetProtoAddr = ? existingNetProto.protoAddress + .mapErr((e: string) => e.cstring) + existingTransProtoCodec = ? existingTransProto.protoCode + .mapErr((e: string) => e.cstring) + existingTransProtoPort = ? existingTransProto.protoAddress + .mapErr((e: string) => e.cstring) + existingIp = + if existingNetProtoFam == MultiCodec.codec("ip6"): + ipv6 array[16, byte].initCopyFrom(existingNetProtoAddr) + else: + ipv4 array[4, byte].initCopyFrom(existingNetProtoAddr) + + ipAddr = ip.get(existingIp) + + + if tcpPort.isNone and udpPort.isNone: + transProto = + if existingTransProtoCodec == MultiCodec.codec("udp"): + IpTransportProtocol.udpProtocol + else: IpTransportProtocol.tcpProtocol + transProtoPort = Port(uint16.fromBytesBE(existingTransProtoPort)) + + else: + if tcpPort.isSome: + transProto = IpTransportProtocol.tcpProtocol + transProtoPort = tcpPort.get + if udpPort.isSome: + transProto = IpTransportProtocol.udpProtocol + transProtoPort = udpPort.get + + updated = MultiAddress.init(ipAddr, transProto, transProtoPort) + changed = existing != updated + + r.data.addresses[0].address = updated + + # increase the sequence number only if we've updated the multiaddress + if changed: r.data.seqNo.inc() + + r = ? SignedPeerRecord.init(pk, r.data) + .mapErr((e: CryptoError) => + ("Failed to update SignedPeerRecord: " & $e).cstring + ) + + return ok() + +proc update*(r: var SignedPeerRecord, pk: keys.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port] = none[Port]()): + RecordResult[void] = + let cPk = pkToPk(pk).get + r.update(cPk, ip, tcpPort, udpPort) + +proc toTypedRecord*(r: SignedPeerRecord) : RecordResult[SignedPeerRecord] = ok(r) + +proc ip*(r: SignedPeerRecord): Option[array[4, byte]] = + let ma = r.data.addresses[0].address + + let code = ma[0].get.protoCode() + if code.isOk and code.get == multiCodec("ip4"): + var ipbuf: array[4, byte] + let res = ma[0].get.protoArgument(ipbuf) + if res.isOk: + return some(ipbuf) + +# err("Incorrect IPv4 address") +# else: +# if (?(?ma[1]).protoArgument(pbuf)) == 0: +# err("Incorrect port number") +# else: +# res.port = Port(fromBytesBE(uint16, pbuf)) +# ok(res) +# else: + +# else: +# err("MultiAddress must be wire address (tcp, udp or unix)") + +proc udp*(r: SignedPeerRecord): Option[int] = + let ma = r.data.addresses[0].address + + let code = ma[1].get.protoCode() + if code.isOk and code.get == multiCodec("udp"): + var pbuf: array[2, byte] + let res = ma[1].get.protoArgument(pbuf) + if res.isOk: + let p = fromBytesBE(uint16, pbuf) + return some(p.int) + +proc fromBase64*(r: var SignedPeerRecord, s: string): bool = + ## Loads SPR from base64-encoded rlp-encoded bytes, and validates the + ## signature. + let bytes = Base64Url.decode(s) + r.fromBytes(bytes) + +proc fromURI*(r: var SignedPeerRecord, s: string): bool = + ## Loads SignedPeerRecord from its text encoding. Validates the signature. + ## TODO + const prefix = "spr:" + if s.startsWith(prefix): + result = r.fromBase64(s[prefix.len .. ^1]) + +template fromURI*(r: var SignedPeerRecord, url: SprUri): bool = + fromURI(r, string(url)) + +proc toBase64*(r: SignedPeerRecord): string = + let encoded = r.encode + if encoded.isErr: + error "Failed to encode SignedPeerRecord", error = encoded.error + result = Base64Url.encode(encoded.get(@[])) + +proc toURI*(r: SignedPeerRecord): string = "spr:" & r.toBase64 + +proc init*(T: type SignedPeerRecord, seqNum: uint64, + pk: crypto.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port]): + RecordResult[T] = + ## Initialize a `SignedPeerRecord` 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 `maxSprSize`. + + let peerId = PeerId.init(pk).get + + if tcpPort.isSome() and udpPort.isSome: + warn "Both tcp and udp ports specified, using udp in multiaddress", + tcpPort, udpPort + + var + ipAddr = try: ValidIpAddress.init("127.0.0.1") + except ValueError as e: + return err ("Existing address contains invalid address: " & $e.msg).cstring + proto: IpTransportProtocol + protoPort: Port + + if ip.isSome(): + + ipAddr = ip.get + + if tcpPort.isSome(): + proto = IpTransportProtocol.tcpProtocol + protoPort = tcpPort.get() + if udpPort.isSome(): + proto = IpTransportProtocol.udpProtocol + protoPort = udpPort.get() + else: + if tcpPort.isSome(): + proto = IpTransportProtocol.tcpProtocol + protoPort = tcpPort.get() + if udpPort.isSome(): + proto = IpTransportProtocol.udpProtocol + protoPort = udpPort.get() + + + let ma = MultiAddress.init(ipAddr, proto, protoPort) + # if ip.isSome: + # let + # ipAddr = ip.get + # proto = ipAddr.family + # address = if proto == IPv4: ipAddr.address_v4 + # else: ipAddr.address_v6 + # u and udpPort.isSome + # # let ta = initTAddress(ip.get, udpPort.get) + # # echo ta + # # ma = MultiAddress.init(ta).get + # #let ma1 = MultiAddress.init("/ip4/127.0.0.1").get() #TODO + # #let ma2 = MultiAddress.init(multiCodec("udp"), udpPort.get.int).get + # #ma = ma1 & ma2 + # ma = MultiAddress.init("/ip4/127.0.0.1/udp/" & $udpPort.get.int).get #TODO + # else: + # ma = MultiAddress.init() + # # echo "not implemented" + + let pr = PeerRecord.init(peerId, @[ma], seqNum) + SignedPeerRecord.init(pk, pr).mapErr((e: CryptoError) => ("Failed to init SignedPeerRecord: " & $e).cstring) + +proc init*(T: type SignedPeerRecord, seqNum: uint64, + pk: keys.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port]): + RecordResult[T] = + let kPk = pkToPk(pk).get + SignedPeerRecord.init(seqNum, kPk, ip, tcpPort, udpPort) + +proc contains*(r: SignedPeerRecord, fp: (string, seq[byte])): bool = + # TODO: use FieldPair for this, but that is a bit cumbersome. Perhaps the + # `get` call can be improved to make this easier. + # let field = r.tryGet(fp[0], seq[byte]) + # if field.isSome(): + # if field.get() == fp[1]: + # return true + # TODO: Implement if SignedPeerRecord custom field pairs are implemented + debugEcho "`contains` is not yet implemented for SignedPeerRecords" + return false + +proc `==`*(a, b: SignedPeerRecord): bool = a.data == b.data diff --git a/libp2pdht/private/eth/p2p/discoveryv5/transport.nim b/libp2pdht/private/eth/p2p/discoveryv5/transport.nim index c24d412..c49770c 100644 --- a/libp2pdht/private/eth/p2p/discoveryv5/transport.nim +++ b/libp2pdht/private/eth/p2p/discoveryv5/transport.nim @@ -24,7 +24,7 @@ type bindAddress: Address ## UDP binding address transp: DatagramTransport pendingRequests: Table[AESGCMNonce, PendingRequest] - codec*: Codec + codec*: Codec rng: ref BrHmacDrbgContext PendingRequest = object @@ -135,12 +135,12 @@ proc receive*(t: Transport, a: Address, packet: openArray[byte]) = trace "Received handshake message packet", srcId = packet.srcIdHs, address = a, kind = packet.message.kind t.client.handleMessage(packet.srcIdHs, a, packet.message) - # For a handshake message it is possible that we received an newer ENR. + # For a handshake message it is possible that we received an newer SPR. # In that case we can add/update it to the routing table. if packet.node.isSome(): let node = packet.node.get() - # Lets not add nodes without correct IP in the ENR to the routing table. - # The ENR could contain bogus IPs and although they would get removed + # Lets not add nodes without correct IP in the SPR to the routing table. + # The SPR could contain bogus IPs and although they would get removed # on the next revalidation, one could spam these as the handshake # message occurs on (first) incoming messages. if node.address.isSome() and a == node.address.get(): diff --git a/tests/dht/test_helper.nim b/tests/dht/test_helper.nim index 01a913f..dd8687b 100644 --- a/tests/dht/test_helper.nim +++ b/tests/dht/test_helper.nim @@ -1,8 +1,9 @@ import stew/shims/net, bearssl, chronos, eth/keys, - libp2pdht/discv5/[enr, node, routing_table], + libp2pdht/discv5/[spr, node, routing_table], libp2pdht/discv5/protocol as discv5_protocol, + libp2p/crypto/crypto, libp2p/multiaddress export net @@ -12,11 +13,11 @@ proc localAddress*(port: int): Address = proc initDiscoveryNode*( rng: ref BrHmacDrbgContext, - privKey: PrivateKey, + privKey: keys.PrivateKey, address: Address, - bootstrapRecords: openArray[Record] = [], + bootstrapRecords: openArray[SignedPeerRecord] = [], localEnrFields: openArray[(string, seq[byte])] = [], - previousRecord = none[enr.Record]()): + previousRecord = none[SignedPeerRecord]()): discv5_protocol.Protocol = # set bucketIpLimit to allow bucket split let config = DiscoveryConfig.init(1000, 24, 5) @@ -40,21 +41,53 @@ proc nodeIdInNodes*(id: NodeId, nodes: openArray[Node]): bool = for n in nodes: if id == n.id: return true -proc generateNode*(privKey: PrivateKey, port: int = 20302, - ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1"), - localEnrFields: openArray[FieldPair] = []): Node = - let port = Port(port) - let enr = enr.Record.init(1, privKey, some(ip), - some(port), some(port), localEnrFields).expect("Properly intialized private key") - result = newNode(enr).expect("Properly initialized node") +proc generateNode*(privKey: keys.PrivateKey, port: int = 20302, + ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1")): Node = + let + port = Port(port) + spr = SignedPeerRecord.init(1, privKey, some(ip), some(port), some(port)) + .expect("Properly intialized private key") + result = newNode(spr).expect("Properly initialized node") proc generateNRandomNodes*(rng: ref BrHmacDrbgContext, n: int): seq[Node] = var res = newSeq[Node]() for i in 1..n: - let node = generateNode(PrivateKey.random(rng[])) + let node = generateNode(keys.PrivateKey.random(rng[])) res.add(node) res +proc nodeAndPrivKeyAtDistance*(n: Node, rng: var BrHmacDrbgContext, d: uint32, + ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1")): (Node, keys.PrivateKey) = + while true: + let pk = keys.PrivateKey.random(rng) + let node = generateNode(pk, ip = ip) + if logDistance(n.id, node.id) == d: + return (node, pk) + +proc nodeAtDistance*(n: Node, rng: var BrHmacDrbgContext, d: uint32, + ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1")): Node = + let (node, _) = n.nodeAndPrivKeyAtDistance(rng, d, ip) + node + +proc nodesAtDistance*( + n: Node, rng: var BrHmacDrbgContext, d: uint32, amount: int, + ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1")): seq[Node] = + for i in 0.. encode - privKeyB = PrivateKey.fromHex(nodeBKey)[] # receive -> decode + privKeyA = keys.PrivateKey.fromHex(nodeAKey)[] # sender -> encode + privKeyB = keys.PrivateKey.fromHex(nodeBKey)[] # receive -> decode let - enrRecA = enr.Record.init(1, privKeyA, + enrRecA = SignedPeerRecord.init(1, privKeyA, some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)), some(Port(9000))).expect("Properly intialized private key") - enrRecB = enr.Record.init(1, privKeyB, + enrRecB = SignedPeerRecord.init(1, privKeyB, some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)), some(Port(9000))).expect("Properly intialized private key") @@ -280,7 +281,7 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": const readKey = "0x00000000000000000000000000000000" pingReqId = "0x00000001" - pingEnrSeq = 2'u64 + pingSprSeq = 2'u64 encodedPacket = "00000000000000000000000000000000088b3d4342774649325f313964a39e55" & @@ -300,14 +301,14 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": decoded.get().messageOpt.isSome() decoded.get().messageOpt.get().reqId.id == hexToSeqByte(pingReqId) decoded.get().messageOpt.get().kind == ping - decoded.get().messageOpt.get().ping.enrSeq == pingEnrSeq + decoded.get().messageOpt.get().ping.sprSeq == pingSprSeq test "Whoareyou Packet": const whoareyouChallengeData = "0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000" whoareyouRequestNonce = "0x0102030405060708090a0b0c" whoareyouIdNonce = "0x0102030405060708090a0b0c0d0e0f10" - whoareyouEnrSeq = 0 + whoareyouSprSeq = 0 encodedPacket = "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad" & @@ -321,7 +322,7 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": decoded.get().flag == Flag.Whoareyou decoded.get().whoareyou.requestNonce == hexToByteArray[gcmNonceSize](whoareyouRequestNonce) decoded.get().whoareyou.idNonce == hexToByteArray[idNonceSize](whoareyouIdNonce) - decoded.get().whoareyou.recordSeq == whoareyouEnrSeq + decoded.get().whoareyou.recordSeq == whoareyouSprSeq decoded.get().whoareyou.challengeData == hexToSeqByte(whoareyouChallengeData) codecB.decodePacket(nodeA.address.get(), @@ -330,14 +331,14 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": test "Ping Handshake Message Packet": const pingReqId = "0x00000001" - pingEnrSeq = 1'u64 + pingSprSeq = 1'u64 # # handshake inputs: # whoareyouChallengeData = "0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000001" whoareyouRequestNonce = "0x0102030405060708090a0b0c" whoareyouIdNonce = "0x0102030405060708090a0b0c0d0e0f10" - whoareyouEnrSeq = 1'u64 + whoareyouSprSeq = 1'u64 encodedPacket = "00000000000000000000000000000000088b3d4342774649305f313964a39e55" & @@ -352,7 +353,7 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": whoareyouData = WhoareyouData( requestNonce: hexToByteArray[gcmNonceSize](whoareyouRequestNonce), idNonce: hexToByteArray[idNonceSize](whoareyouIdNonce), - recordSeq: whoareyouEnrSeq, + recordSeq: whoareyouSprSeq, challengeData: hexToSeqByte(whoareyouChallengeData)) pubkey = some(privKeyA.toPublicKey()) challenge = Challenge(whoareyouData: whoareyouData, pubkey: pubkey) @@ -367,44 +368,45 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": decoded.isOk() decoded.get().message.reqId.id == hexToSeqByte(pingReqId) decoded.get().message.kind == ping - decoded.get().message.ping.enrSeq == pingEnrSeq + decoded.get().message.ping.sprSeq == pingSprSeq decoded.get().node.isNone() codecB.decodePacket(nodeA.address.get(), hexToSeqByte(encodedPacket & "00")).isErr() - test "Ping Handshake Message Packet with ENR": + test "Ping Handshake Message Packet with SPR": const pingReqId = "0x00000001" - pingEnrSeq = 1'u64 + pingSprSeq = 1'u64 # # handshake inputs: # whoareyouChallengeData = "0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000" whoareyouRequestNonce = "0x0102030405060708090a0b0c" whoareyouIdNonce = "0x0102030405060708090a0b0c0d0e0f10" - whoareyouEnrSeq = 0'u64 + whoareyouSprSeq = 0'u64 encodedPacket = - "00000000000000000000000000000000088b3d4342774649305f313964a39e55" & - "ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" & - "4c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be9856" & - "2fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b2" & - "1481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1" & - "f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6" & - "cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1" & - "2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a" & - "80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e" & - "4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b1394" & - "71" + "2746cce362989b5d7e2496490b25f952e9198c524b06c7e9e069c5f7c8d2c84b" & + "943322ac741826023cb35086eee94baaf98f81217c3dbcb022afb1464555b144" & + "69b49cb19fe1f3459b4bbb03a52fc588bcc69d7ff50842ee6c3fc3ffd58d425f" & + "e8c7bec9777fcb15d9c9e37c4aa3b226274f6631526d6d2127f39e1daff277fd" & + "e867a8222ae509922d9e94456f7cbde14c1788894708713789b28b307ac983c8" & + "31ebc00113ded4011af2bfa06078c8f0a3401e8c034b3ae5506fb002a0355bf1" & + "48b19022bae8b088a0c0bdc22dc3d5ce4a6c5ad700a3f8a82be214c2bef98afe" & + "2dbf4ffaaf816602d470dcfe8184b1db8d873d8813984f86b6350ff5d00d466c" & + "06de59f1797ad01a68bb9c07b9cb56e6989ab0e94d32c60e435a48aa7c89d602" & + "3863bd1605a33f895903657fe72f79ded24b366486a1c02a893702ec7d299ea8" & + "7afe0bb771fad244b8d4d0bd7bf4dc833a17c4db2f926eb7614788308a6f98af" & + "9a0e20bd75af75175645058702122b15" let whoareyouData = WhoareyouData( requestNonce: hexToByteArray[gcmNonceSize](whoareyouRequestNonce), idNonce: hexToByteArray[idNonceSize](whoareyouIdNonce), - recordSeq: whoareyouEnrSeq, + recordSeq: whoareyouSprSeq, challengeData: hexToSeqByte(whoareyouChallengeData)) - pubkey = none(PublicKey) + pubkey = none(keys.PublicKey) challenge = Challenge(whoareyouData: whoareyouData, pubkey: pubkey) key = HandshakeKey(nodeId: nodeA.id, address: nodeA.address.get()) @@ -417,14 +419,14 @@ suite "Discovery v5.1 Packet Encodings Test Vectors": decoded.isOk() decoded.get().message.reqId.id == hexToSeqByte(pingReqId) decoded.get().message.kind == ping - decoded.get().message.ping.enrSeq == pingEnrSeq + decoded.get().message.ping.sprSeq == pingSprSeq decoded.get().node.isSome() codecB.decodePacket(nodeA.address.get(), hexToSeqByte(encodedPacket & "00")).isErr() suite "Discovery v5.1 Additional Encode/Decode": - var rng = newRng() + var rng = keys.newRng() test "Encryption/Decryption": let @@ -465,7 +467,7 @@ suite "Discovery v5.1 Additional Encode/Decode": var nonce: AESGCMNonce brHmacDrbgGenerate(rng[], nonce) let - privKey = PrivateKey.random(rng[]) + privKey = keys.PrivateKey.random(rng[]) nodeId = privKey.toPublicKey().toNodeId() authdata = newSeq[byte](32) staticHeader = encodeStaticHeader(Flag.OrdinaryMessage, nonce, @@ -484,18 +486,18 @@ suite "Discovery v5.1 Additional Encode/Decode": var codecA, codecB: Codec nodeA, nodeB: Node - privKeyA, privKeyB: PrivateKey + privKeyA, privKeyB: keys.PrivateKey setup: - privKeyA = PrivateKey.random(rng[]) # sender -> encode - privKeyB = PrivateKey.random(rng[]) # receiver -> decode + privKeyA = keys.PrivateKey.random(rng[]) # sender -> encode + privKeyB = keys.PrivateKey.random(rng[]) # receiver -> decode let - enrRecA = enr.Record.init(1, privKeyA, + enrRecA = SignedPeerRecord.init(1, privKeyA, some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)), some(Port(9000))).expect("Properly intialized private key") - enrRecB = enr.Record.init(1, privKeyB, + enrRecB = SignedPeerRecord.init(1, privKeyB, some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)), some(Port(9000))).expect("Properly intialized private key") @@ -506,7 +508,7 @@ suite "Discovery v5.1 Additional Encode/Decode": test "Encode / Decode Ordinary Random Message Packet": let - m = PingMessage(enrSeq: 0) + m = PingMessage(sprSeq: 0) reqId = RequestId.init(rng[]) message = encodeMessage(m, reqId) @@ -526,7 +528,7 @@ suite "Discovery v5.1 Additional Encode/Decode": let recordSeq = 0'u64 let data = encodeWhoareyouPacket(rng[], codecA, nodeB.id, - nodeB.address.get(), requestNonce, recordSeq, none(PublicKey)) + nodeB.address.get(), requestNonce, recordSeq, none(keys.PublicKey)) let decoded = codecB.decodePacket(nodeA.address.get(), data) @@ -546,7 +548,7 @@ suite "Discovery v5.1 Additional Encode/Decode": brHmacDrbgGenerate(rng[], requestNonce) let recordSeq = 1'u64 - m = PingMessage(enrSeq: 0) + m = PingMessage(sprSeq: 0) reqId = RequestId.init(rng[]) message = encodeMessage(m, reqId) pubkey = some(privKeyA.toPublicKey()) @@ -569,18 +571,18 @@ suite "Discovery v5.1 Additional Encode/Decode": decoded.isOk() decoded.get().message.reqId == reqId decoded.get().message.kind == ping - decoded.get().message.ping.enrSeq == 0 + decoded.get().message.ping.sprSeq == 0 decoded.get().node.isNone() - test "Encode / Decode Handshake Message Packet with ENR": + test "Encode / Decode Handshake Message Packet with SPR": var requestNonce: AESGCMNonce brHmacDrbgGenerate(rng[], requestNonce) let recordSeq = 0'u64 - m = PingMessage(enrSeq: 0) + m = PingMessage(sprSeq: 0) reqId = RequestId.init(rng[]) message = encodeMessage(m, reqId) - pubkey = none(PublicKey) + pubkey = none(keys.PublicKey) # Encode/decode whoareyou packet to get the handshake stored and the # whoareyou data returned. It's either that or construct the header for the @@ -600,13 +602,13 @@ suite "Discovery v5.1 Additional Encode/Decode": decoded.isOk() decoded.get().message.reqId == reqId decoded.get().message.kind == ping - decoded.get().message.ping.enrSeq == 0 + decoded.get().message.ping.sprSeq == 0 decoded.get().node.isSome() decoded.get().node.get().record.seqNum == 1 test "Encode / Decode Ordinary Message Packet": let - m = PingMessage(enrSeq: 0) + m = PingMessage(sprSeq: 0) reqId = RequestId.init(rng[]) message = encodeMessage(m, reqId) @@ -628,5 +630,5 @@ suite "Discovery v5.1 Additional Encode/Decode": decoded.get().messageOpt.isSome() decoded.get().messageOpt.get().reqId == reqId decoded.get().messageOpt.get().kind == ping - decoded.get().messageOpt.get().ping.enrSeq == 0 + decoded.get().messageOpt.get().ping.sprSeq == 0 decoded[].requestNonce == nonce