From e91ae5452c164af86d647c606c1dbd222de55e91 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Thu, 17 Feb 2022 13:28:53 +0100 Subject: [PATCH] changing ENR to libp2p Signed Peer Records Lots of glue code, but only a func->proc change in the original Assumes secp keys in libp2p. --- eth/p2p/discoveryv5/encoding.nim | 3 +- eth/p2p/discoveryv5/libp2p_record.nim | 250 +++++++++++++++++++++ eth/p2p/discoveryv5/messages.nim | 2 +- eth/p2p/discoveryv5/messages_encoding.nim | 3 +- eth/p2p/discoveryv5/node.nim | 4 +- eth/p2p/discoveryv5/nodes_verification.nim | 3 +- eth/p2p/discoveryv5/protocol.nim | 3 +- eth/p2p/discoveryv5/routing_table.nim | 3 +- tests/dht/test_helper.nim | 5 +- 9 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 eth/p2p/discoveryv5/libp2p_record.nim diff --git a/eth/p2p/discoveryv5/encoding.nim b/eth/p2p/discoveryv5/encoding.nim index 7115778..b3b32f1 100644 --- a/eth/p2p/discoveryv5/encoding.nim +++ b/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] + "."/[messages, messages_encoding, node, hkdf, sessions], + "."/libp2p_record as enr from stew/objects import checkedEnumAssign diff --git a/eth/p2p/discoveryv5/libp2p_record.nim b/eth/p2p/discoveryv5/libp2p_record.nim new file mode 100644 index 0000000..82fa7e6 --- /dev/null +++ b/eth/p2p/discoveryv5/libp2p_record.nim @@ -0,0 +1,250 @@ +# 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, sugar], + pkg/stew/[results, byteutils], + stew/endians2, + stew/shims/net, + stew/base64, + eth/rlp, + eth/keys, + libp2p/crypto/crypto, + libp2p/crypto/secp, + libp2p/routing_record, + libp2p/multicodec +from chronos import TransportAddress, initTAddress + +export options, results + +type + Record* = object + peerRecord: PeerRecord + signedPeerRecord: Envelope + + EnrUri* = distinct string + + FieldPair* = (string, Field) + ## dummy implementation + + Field = object + ## dummy implementation + + RecordResult*[T] = Result[T, cstring] + +proc seqNum*(r: Record): uint64 = + r.peerRecord.seqNo + +#proc encode +proc append*(rlpWriter: var RlpWriter, value: Record) = + # echo "encoding to:" & $value.signedPeerRecord.encode.get + rlpWriter.append(value.signedPeerRecord.encode.get) + +#proc decode +# some(rlp.decode(authdata.toOpenArray(recordPos, authdata.high),enr.Record)) +# template decode*(bytes: openArray[byte], T: type): untyped = +# mixin read +# var rlp = rlpFromBytes(bytes) +# rlp.read(T) +# 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 fromBytes*(r: var Record, s: openArray[byte]): bool = + ## Loads ENR from rlp-encoded bytes, and validates the signature. + +proc fromBytes(r: var Record, s: openArray[byte]): bool = + # echo "decoding from:" & $s & $s.len + let + #TODO: thos is double work, + EnvelopeDomain = $multiCodec("libp2p-peer-record") # envelope domain as per RFC0002 + envelope = Envelope.decode(@s[2..^1], EnvelopeDomain) #TODO: this is just to remove RLP header. Ugly! + if envelope.isErr: + #echo "invalid ENV " & $envelope.error + return false + + let + spr = PeerRecord.decode(envelope.get.payload).mapErr(x => EnvelopeInvalidProtobuf) + if spr.isErr: + #echo "invalid SPR " & $spr.error + return false + + r.peerRecord = spr.get + r.signedPeerRecord = envelope.get + return true + +proc read*(rlp: var Rlp, T: typedesc[Record]): + T {.raises: [RlpError, ValueError, Defect].} = + # echo "read:" & $rlp.rawData + ## code directly borrowed from enr.nim + 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 get*(r: Record, T: type crypto.PublicKey): Option[T] = + ## Get the `PublicKey` from provided `Record`. Return `none` when there is + ## no `PublicKey` in the record. + some(r.signedPeerRecord.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: Record, 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.signedPeerRecord.publicKey + pkToPk(pk) + +proc update*(r: var Record, pk: crypto.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port] = none[Port](), + extraFields: openArray[FieldPair] = []): + RecordResult[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. + r.signedPeerRecord = Envelope.init(pk, r.peerRecord).get + #TODO: handle fields + +proc update*(r: var Record, pk: keys.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port] = none[Port](), + extraFields: openArray[FieldPair] = []): + RecordResult[void] = + let cPk = pkToPk(pk).get + r.update(cPk, ip, tcpPort, udpPort, extraFields) + +proc toTypedRecord*(r: Record) : RecordResult[Record] = ok(r) + +proc ip*(r: Record): Option[array[4, byte]] = + let ma = r.peerRecord.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: Record): Option[int] = + let ma = r.peerRecord.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 fromURI*(r: var Record, s: string): bool = + ## Loads Record from its text encoding. Validates the signature. + ## TODO + #error "fromURI not implemented" + false +# 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.signedPeerRecord.encode.get) + +proc toURI*(r: Record): string = "spr:" & r.toBase64 + +proc init*(T: type Record, seqNum: uint64, + pk: crypto.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port], + extraFields: openArray[FieldPair] = []): + RecordResult[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`. + + let peerId = PeerId.init(pk).get + var ma:MultiAddress + if ip.isSome 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" + + var res: Record + res.peerRecord = PeerRecord.init(peerId, seqNum, @[ma]) + res.signedPeerRecord = Envelope.init(pk, res.peerRecord).get + ok(res) + +proc init*(T: type Record, seqNum: uint64, + pk: keys.PrivateKey, + ip: Option[ValidIpAddress], + tcpPort, udpPort: Option[Port], + extraFields: openArray[FieldPair] = []): + RecordResult[T] = + let kPk = pkToPk(pk).get + Record.init(seqNum, kPk, ip, tcpPort, udpPort, extraFields) + +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. + # TODO: implement + #error "not implemented" + return false + +template toFieldPair*(key: string, value: auto): FieldPair = + #error "not implemented" + (key, Field()) + +proc update*(record: var Record, pk: keys.PrivateKey, + fieldPairs: openArray[FieldPair]): RecordResult[void] = + #error "not implemented" + err("not implemented") + +proc `==`*(a, b: Record): bool = a.signedPeerRecord == b.signedPeerRecord diff --git a/eth/p2p/discoveryv5/messages.nim b/eth/p2p/discoveryv5/messages.nim index 46a9117..35f33b1 100644 --- a/eth/p2p/discoveryv5/messages.nim +++ b/eth/p2p/discoveryv5/messages.nim @@ -16,7 +16,7 @@ import std/[hashes, net], stew/arrayops, eth/[rlp, keys], - ./enr + "."/libp2p_record as enr type MessageKind* = enum diff --git a/eth/p2p/discoveryv5/messages_encoding.nim b/eth/p2p/discoveryv5/messages_encoding.nim index 7c13ce2..3b3d950 100644 --- a/eth/p2p/discoveryv5/messages_encoding.nim +++ b/eth/p2p/discoveryv5/messages_encoding.nim @@ -1,6 +1,7 @@ import eth/[rlp], - "."/[messages, enr] + "."/[messages], + "."/libp2p_record as enr from stew/objects import checkedEnumAssign diff --git a/eth/p2p/discoveryv5/node.nim b/eth/p2p/discoveryv5/node.nim index 4a9bbdb..00a0cfd 100644 --- a/eth/p2p/discoveryv5/node.nim +++ b/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 + "."/libp2p_record as enr export stint @@ -58,7 +58,7 @@ 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], +proc 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) diff --git a/eth/p2p/discoveryv5/nodes_verification.nim b/eth/p2p/discoveryv5/nodes_verification.nim index 45fd89f..826a99f 100644 --- a/eth/p2p/discoveryv5/nodes_verification.nim +++ b/eth/p2p/discoveryv5/nodes_verification.nim @@ -3,7 +3,8 @@ import std/[sets, options], stew/results, stew/shims/net, chronicles, chronos, - "."/[node, enr, routing_table] + "."/[node, routing_table], + "."/libp2p_record as enr logScope: topics = "nodes-verification" diff --git a/eth/p2p/discoveryv5/protocol.nim b/eth/p2p/discoveryv5/protocol.nim index 09fb2df..c414f01 100644 --- a/eth/p2p/discoveryv5/protocol.nim +++ b/eth/p2p/discoveryv5/protocol.nim @@ -78,7 +78,8 @@ import stew/shims/net as stewNet, json_serialization/std/net, stew/[endians2, results], chronicles, chronos, stint, bearssl, metrics, eth/[rlp, keys, async_utils], - "."/[transport, messages, messages_encoding, node, routing_table, enr, random2, ip_vote, nodes_verification] + "."/[transport, messages, messages_encoding, node, routing_table, random2, ip_vote, nodes_verification], + "."/libp2p_record as enr import nimcrypto except toHex diff --git a/eth/p2p/discoveryv5/routing_table.nim b/eth/p2p/discoveryv5/routing_table.nim index 441d99d..c374a9f 100644 --- a/eth/p2p/discoveryv5/routing_table.nim +++ b/eth/p2p/discoveryv5/routing_table.nim @@ -11,7 +11,8 @@ 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], + "."/libp2p_record as enr export options diff --git a/tests/dht/test_helper.nim b/tests/dht/test_helper.nim index 421e32b..5aedc05 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, - ../../eth/p2p/discoveryv5/[enr, node, routing_table], - ../../eth/p2p/discoveryv5/protocol as discv5_protocol + ../../eth/p2p/discoveryv5/[node, routing_table], + ../../eth/p2p/discoveryv5/protocol as discv5_protocol, + ../../eth/p2p/discoveryv5/libp2p_record as enr export net