Overhaul of ENR implementation - part I (#707)

- Rework adding and updating of fields by having an insert call
that gets used everywhere. Avoiding also duplicate keys. One
side-effect of this is that ENR sequence number will always get
updated on an update call, even if nothing changes.
- Deprecate initRecord as it is only used in tests and is flawed
- Assert when predefined keys go into the extra custom pairs.
Any of the predefined keys are only to be passed now via specific
parameters to make sure that the correct types are stored in ENR.
- Clearify the Opt.none behaviour for Record.update
- When setting ipv6, allow for tcp/udp port fields to be used
default
- General clean-up
- Rework/clean-up completely the ENR tests.
This commit is contained in:
Kim De Mey 2024-06-24 14:57:39 +02:00 committed by GitHub
parent 26212c881b
commit 7f20d79945
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 412 additions and 287 deletions

View File

@ -11,7 +11,7 @@
{.push raises: [].} {.push raises: [].}
import import
std/[strutils, macros, algorithm, net], std/[strutils, sequtils, macros, algorithm, net],
nimcrypto/[keccak, utils], nimcrypto/[keccak, utils],
stew/base64, stew/base64,
results, results,
@ -19,33 +19,17 @@ import
".."/../[rlp, keys], ".."/../[rlp, keys],
../../net/utils ../../net/utils
export options, results, keys export results, rlp, keys
const const
maxEnrSize = 300 ## Maximum size of an encoded node record, in bytes. maxEnrSize = 300 ## Maximum size of an encoded node record, in bytes.
minRlpListLen = 4 ## Minimum node record RLP list has: signature, seqId, minRlpListLen = 4 ## Minimum node record RLP list has: signature, seqId,
## "id" key and value. ## "id" key and value.
PreDefinedKeys = ["id", "secp256k1", "ip", "ip6", "tcp", "tcp6", "udp", "udp6"]
## Predefined keys in the ENR spec, these have specific constraints on the
## type of the associated value.
type type
FieldPair* = (string, Field)
Record* = object
seqNum*: uint64
raw*: seq[byte] # RLP encoded record
pairs: seq[FieldPair] # sorted list of all key/value pairs
EnrUri* = distinct string
TypedRecord* = object
id*: string
secp256k1*: Opt[array[33, byte]]
ip*: Opt[array[4, byte]]
ip6*: Opt[array[16, byte]]
tcp*: Opt[int]
udp*: Opt[int]
tcp6*: Opt[int]
udp6*: Opt[int]
FieldKind = enum FieldKind = enum
kString, kString,
kNum, kNum,
@ -64,6 +48,28 @@ type
listRaw: seq[byte] ## Differently from the other kinds, this is is stored listRaw: seq[byte] ## Differently from the other kinds, this is is stored
## as raw (encoded) RLP data, and thus treated as such further on. ## as raw (encoded) RLP data, and thus treated as such further on.
FieldPair* = (string, Field)
Record* = object
seqNum*: uint64 ## ENR sequence number
pairs*: seq[FieldPair] ## List of all key:value pairs. List must have
## at least the id k:v pair and the secp256k1 k:v pair. The list of pairs
## must remain sorted and without duplicate keys. Use the insert func to
## ensure this.
raw*: seq[byte] ## RLP encoded record
EnrUri* = distinct string
TypedRecord* = object
id*: string
secp256k1*: Opt[array[33, byte]]
ip*: Opt[array[4, byte]]
ip6*: Opt[array[16, byte]]
tcp*: Opt[int]
udp*: Opt[int]
tcp6*: Opt[int]
udp6*: Opt[int]
EnrResult*[T] = Result[T, cstring] EnrResult*[T] = Result[T, cstring]
template toField[T](v: T): Field = template toField[T](v: T): Field =
@ -94,8 +100,45 @@ func `==`(a, b: Field): bool =
else: else:
false false
template toFieldPair*(key: string, value: auto): FieldPair =
(key, toField(value))
func cmp(a, b: FieldPair): int = cmp(a[0], b[0]) func cmp(a, b: FieldPair): int = cmp(a[0], b[0])
func hasPredefinedKey(pair: FieldPair): bool =
PreDefinedKeys.contains(pair[0])
func hasPredefinedKey(pairs: openArray[FieldPair]): bool =
for pair in pairs:
if hasPredefinedKey(pair):
return true
false
func find(pairs: openArray[FieldPair], key: string): Opt[int] =
## Search for key in key:value pairs.
##
## Returns some(index of key) if key is found. Else returns none.
for i, (k, v) in pairs:
if k == key:
return Opt.some(i)
Opt.none(int)
func insert(pairs: var seq[FieldPair], item: FieldPair) =
## Insert item in key:value pairs.
##
## If a FieldPair with key is already present, the value is updated, otherwise
## the pair is inserted in the correct position to keep the pairs sorted.
let index = find(pairs, item[0])
if index.isSome():
pairs[index.get()] = item
else:
pairs.insert(item, pairs.lowerBound(item, cmp))
func insert(pairs: var seq[FieldPair], b: openArray[FieldPair]) =
## Insert all items in key:value pairs.
for item in b:
pairs.insert(item)
func makeEnrRaw( func makeEnrRaw(
seqNum: uint64, pk: PrivateKey, seqNum: uint64, pk: PrivateKey,
pairs: openArray[FieldPair]): EnrResult[seq[byte]] = pairs: openArray[FieldPair]): EnrResult[seq[byte]] =
@ -112,16 +155,18 @@ func makeEnrRaw(
of kList: w.appendRawBytes(v.listRaw) # No encoding needs to happen of kList: w.appendRawBytes(v.listRaw) # No encoding needs to happen
w.finish() w.finish()
let toSign = block: let content =
var w = initRlpList(pairs.len * 2 + 1) block:
w.append(seqNum, pairs) var w = initRlpList(pairs.len * 2 + 1)
w.append(seqNum, pairs)
let sig = signNR(pk, toSign) let signature = signNR(pk, content)
var raw = block: let raw =
var w = initRlpList(pairs.len * 2 + 2) block:
w.append(sig.toRaw()) var w = initRlpList(pairs.len * 2 + 2)
w.append(seqNum, pairs) w.append(signature.toRaw())
w.append(seqNum, pairs)
if raw.len > maxEnrSize: if raw.len > maxEnrSize:
err("Record exceeds maximum size") err("Record exceeds maximum size")
@ -129,7 +174,7 @@ func makeEnrRaw(
ok(raw) ok(raw)
func makeEnrAux( func makeEnrAux(
seqNum: uint64, pk: PrivateKey, seqNum: uint64, id: string, pk: PrivateKey,
pairs: openArray[FieldPair]): EnrResult[Record] = pairs: openArray[FieldPair]): EnrResult[Record] =
var record: Record var record: Record
record.pairs = @pairs record.pairs = @pairs
@ -137,76 +182,70 @@ func makeEnrAux(
let pubkey = pk.toPublicKey() let pubkey = pk.toPublicKey()
record.pairs.add(("id", Field(kind: kString, str: "v4"))) record.pairs.insert(("id", Field(kind: kString, str: id)))
record.pairs.add(("secp256k1", record.pairs.insert(("secp256k1",
Field(kind: kBytes, bytes: @(pubkey.toRawCompressed())))) 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) record.raw = ? makeEnrRaw(seqNum, pk, record.pairs)
ok(record) ok(record)
macro initRecord*( macro initRecord*(
seqNum: uint64, pk: PrivateKey, seqNum: uint64, pk: PrivateKey,
pairs: untyped{nkTableConstr}): untyped = pairs: untyped{nkTableConstr}): untyped {.deprecated: "Please use Record.init instead".} =
## Initialize a `Record` with given sequence number, private key and k:v ## Initialize a `Record` with given sequence number, private key and k:v
## pairs. ## pairs.
## ##
## Can fail in case the record exceeds the `maxEnrSize`. ## Can fail in case the record exceeds the `maxEnrSize`.
# Note: Deprecated as it is flawed. It allows for any type to be stored in the
# predefined keys. It also allows for duplicate keys (which could be fixed)
# and no longer sorts the pairs. It can however be moved and used for testing
# purposes.
for c in pairs: for c in pairs:
c.expectKind(nnkExprColonExpr) c.expectKind(nnkExprColonExpr)
c[1] = newCall(bindSym"toField", c[1]) c[1] = newCall(bindSym"toField", c[1])
result = quote do: result = quote do:
makeEnrAux(`seqNum`, `pk`, `pairs`) makeEnrAux(`seqNum`, "v4", `pk`, `pairs`)
template toFieldPair*(key: string, value: auto): FieldPair = func insertAddress(
(key, toField(value))
func addAddress(
fields: var seq[FieldPair], fields: var seq[FieldPair],
ip: Opt[IpAddress], ip: Opt[IpAddress],
tcpPort, udpPort: Opt[Port]) = tcpPort, udpPort: Opt[Port]) =
## Add address information in new fields. Incomplete address ## Insert address data.
## information is allowed (example: Port but not IP) as that information ## Incomplete address information is allowed (example: Port but not IP) as
## might be already in the ENR or added later. ## that information might be already in the ENR or added later.
if ip.isSome(): if ip.isSome():
let case ip.value.family
ipExt = ip.get() of IPv4:
isV6 = ipExt.family == IPv6 fields.insert(("ip", ip.value.address_v4.toField))
of IPv6:
fields.insert(("ip6", ip.value.address_v6.toField))
fields.add(if isV6: ("ip6", ipExt.address_v6.toField) if tcpPort.isSome():
else: ("ip", ipExt.address_v4.toField)) fields.insert(("tcp", tcpPort.get().uint16.toField))
if tcpPort.isSome(): if udpPort.isSome():
fields.add(((if isV6: "tcp6" else: "tcp"), tcpPort.get().uint16.toField)) fields.insert(("udp", udpPort.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))
func init*( func init*(
T: type Record, T: type Record,
seqNum: uint64, pk: PrivateKey, seqNum: uint64, pk: PrivateKey,
ip: Opt[IpAddress], ip: Opt[IpAddress] = Opt.none(IpAddress),
tcpPort, udpPort: Opt[Port], tcpPort: Opt[Port] = Opt.none(Port),
udpPort: Opt[Port] = Opt.none(Port),
extraFields: openArray[FieldPair] = []): extraFields: openArray[FieldPair] = []):
EnrResult[T] = EnrResult[T] =
## Initialize a `Record` with given sequence number, private key, optional ## Initialize a `Record` with given sequence number, private key, optional
## ip address, tcp port, udp port, and optional custom k:v pairs. ## ip address, tcp port, udp port, and optional custom k:v pairs.
## ##
## Can fail in case the record exceeds the `maxEnrSize`. ## Can fail in case the record exceeds the `maxEnrSize`.
doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs")
var fields = newSeq[FieldPair]() var fields = newSeq[FieldPair]()
# TODO: Allow for initializing ENR with both ip4 and ipv6 address. fields.insertAddress(ip, tcpPort, udpPort)
fields.addAddress(ip, tcpPort, udpPort) fields.insert extraFields
fields.add extraFields makeEnrAux(seqNum, "v4", pk, fields)
makeEnrAux(seqNum, pk, fields)
func getField(r: Record, name: string, field: var Field): bool = func getField(r: Record, name: string, field: var Field): bool =
# It might be more correct to do binary search, # It might be more correct to do binary search,
@ -270,61 +309,10 @@ func get*(r: Record, T: type PublicKey): Opt[T] =
return Opt.some(pk[]) return Opt.some(pk[])
Opt.none(T) Opt.none(T)
func find(r: Record, key: string): Opt[int] =
## Search for key in record key:value pairs.
##
## Returns some(index of key) if key is found in record. Else return none.
for i, (k, v) in r.pairs:
if k == key:
return Opt.some(i)
Opt.none(int)
func update*( func update*(
record: var Record, pk: PrivateKey, record: var Record,
fieldPairs: openArray[FieldPair]): EnrResult[void] =
## Update a `Record` k:v pairs.
##
## In case any of the k:v pairs is updated or added (new), the sequence number
## of the `Record` will be incremented and a new signature will be applied.
##
## Can fail in case of wrong `PrivateKey`, if the size of the resulting record
## exceeds `maxEnrSize` or if maximum sequence number is reached. The `Record`
## will not be altered in these cases.
var r = record
let pubkey = r.get(PublicKey)
if pubkey.isNone() or pubkey.get() != pk.toPublicKey():
return err("Public key does not correspond with given private key")
var updated = false
for fieldPair in fieldPairs:
let index = r.find(fieldPair[0])
if(index.isSome()):
if r.pairs[index.get()][1] == fieldPair[1]:
# Exact k:v pair is already in record, nothing to do here.
continue
else:
# Need to update the value.
r.pairs[index.get()] = fieldPair
updated = true
else:
# Add new k:v pair.
r.pairs.insert(fieldPair, lowerBound(r.pairs, fieldPair, cmp))
updated = true
if updated:
if r.seqNum == high(type r.seqNum): # highly unlikely
return err("Maximum sequence number reached")
r.seqNum.inc()
r.raw = ? makeEnrRaw(r.seqNum, pk, r.pairs)
record = r
ok()
func update*(
r: var Record,
pk: PrivateKey, pk: PrivateKey,
ip: Opt[IpAddress], ip: Opt[IpAddress] = Opt.none(IpAddress),
tcpPort: Opt[Port] = Opt.none(Port), tcpPort: Opt[Port] = Opt.none(Port),
udpPort: Opt[Port] = Opt.none(Port), udpPort: Opt[Port] = Opt.none(Port),
extraFields: openArray[FieldPair] = []): extraFields: openArray[FieldPair] = []):
@ -332,18 +320,35 @@ func update*(
## Update a `Record` with given ip address, tcp port, udp port and optional ## Update a `Record` with given ip address, tcp port, udp port and optional
## custom k:v pairs. ## custom k:v pairs.
## ##
## In case any of the k:v pairs is updated or added (new), the sequence number ## If none of the k:v pairs are changed, the sequence number of the `Record`
## of the `Record` will be incremented and a new signature will be applied. ## will still be incremented and a new signature will be applied.
##
## Providing an `Opt.none` for `ip`, `tcpPort` or `udpPort` will leave the
## corresponding field untouched.
## ##
## Can fail in case of wrong `PrivateKey`, if the size of the resulting record ## 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` ## exceeds `maxEnrSize` or if maximum sequence number is reached. The `Record`
## will not be altered in these cases. ## will not be altered in these cases.
var fields = newSeq[FieldPair]() # TODO: deprecate this call and have individual functions for updating?
doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs")
# TODO: Make updating of both ipv4 and ipv6 address in ENR more convenient. var r = record
fields.addAddress(ip, tcpPort, udpPort)
fields.add extraFields let pubkey = r.get(PublicKey)
r.update(pk, fields) if pubkey.isNone() or pubkey.get() != pk.toPublicKey():
return err("Public key does not correspond with given private key")
r.pairs.insertAddress(ip, tcpPort, udpPort)
r.pairs.insert extraFields
if r.seqNum == high(type r.seqNum): # highly unlikely
return err("Maximum sequence number reached")
r.seqNum.inc()
r.raw = ? makeEnrRaw(r.seqNum, pk, r.pairs)
record = r
ok()
func tryGet*(r: Record, key: string, T: type): Opt[T] = func tryGet*(r: Record, key: string, T: type): Opt[T] =
## Get the value from the provided key. ## Get the value from the provided key.
@ -548,7 +553,7 @@ func read*(
rlp: var Rlp, T: type Record): rlp: var Rlp, T: type Record):
T {.raises: [RlpError, ValueError].} = T {.raises: [RlpError, ValueError].} =
var res: T var res: T
if not rlp.hasData() or not res.fromBytes(rlp.rawData): if not rlp.hasData() or not res.fromBytes(rlp.rawData()):
# TODO: This could also just be an invalid signature, would be cleaner to # TODO: This could also just be an invalid signature, would be cleaner to
# split of RLP deserialisation errors from this. # split of RLP deserialisation errors from this.
raise newException(ValueError, "Could not deserialize") raise newException(ValueError, "Could not deserialize")

View File

@ -253,7 +253,7 @@ func updateRecord*(
d: Protocol, enrFields: openArray[(string, seq[byte])]): DiscResult[void] = d: Protocol, enrFields: openArray[(string, seq[byte])]): DiscResult[void] =
## Update the ENR of the local node with provided `enrFields` k:v pairs. ## Update the ENR of the local node with provided `enrFields` k:v pairs.
let fields = mapIt(enrFields, toFieldPair(it[0], it[1])) let fields = mapIt(enrFields, toFieldPair(it[0], it[1]))
d.localNode.record.update(d.privateKey, fields) d.localNode.record.update(d.privateKey, extraFields = fields)
# TODO: Would it make sense to actively ping ("broadcast") to all the peers # 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? # we stored a handshake with in order to get that ENR updated?
@ -992,6 +992,8 @@ proc newProtocol*(
var record: Record var record: Record
if previousRecord.isSome(): if previousRecord.isSome():
record = previousRecord.get() record = previousRecord.get()
# TODO: this is faulty in case the intent is to remove a field with
# opt.none
record.update(privKey, enrIp, enrTcpPort, enrUdpPort, record.update(privKey, enrIp, enrTcpPort, enrUdpPort,
customEnrFields).expect("Record within size limits and correct key") customEnrFields).expect("Record within size limits and correct key")
else: else:
@ -1045,6 +1047,8 @@ proc newProtocol*(
record = record =
if previousRecord.isSome(): if previousRecord.isSome():
var res = previousRecord.get() var res = previousRecord.get()
# TODO: this is faulty in case the intent is to remove a field with
# opt.none
res.update(privKey, enrIp, enrTcpPort, enrUdpPort, res.update(privKey, enrIp, enrTcpPort, enrUdpPort,
customEnrFields).expect("Record within size limits and correct key") customEnrFields).expect("Record within size limits and correct key")
res res

View File

@ -437,9 +437,9 @@ suite "Discovery v5 Tests":
previousRecord = Opt.some(updatesNode.getRecord())) previousRecord = Opt.some(updatesNode.getRecord()))
check: check:
node.getRecord().seqNum == 1 node.getRecord().seqNum == 1
noUpdatesNode.getRecord().seqNum == 1 noUpdatesNode.getRecord().seqNum == 2
updatesNode.getRecord().seqNum == 2 updatesNode.getRecord().seqNum == 3
moreUpdatesNode.getRecord().seqNum == 3 moreUpdatesNode.getRecord().seqNum == 4
# Defect (for now?) on incorrect key use # Defect (for now?) on incorrect key use
expect ResultDefect: expect ResultDefect:

View File

@ -8,85 +8,156 @@
import import
std/[sequtils, net], std/[sequtils, net],
stew/byteutils,
unittest2, unittest2,
../../eth/p2p/discoveryv5/enr, ../../eth/[keys, rlp] ../../eth/p2p/discoveryv5/enr, ../../eth/[keys, rlp]
let rng = newRng() let rng = newRng()
suite "ENR": proc testRlpEncodingLoop*(r: enr.Record): bool =
test "Serialization": let encoded = rlp.encode(r)
var pk = PrivateKey.fromHex( let decoded = rlp.decode(encoded, enr.Record)
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] decoded == r
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[]
check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
let uri = r.toURI()
var r2: Record
let sigValid = r2.fromURI(uri)
check(sigValid)
check($r2 == $r)
check(r2.raw == r.raw)
suite "ENR test vector tests":
# Tests using the test vector from:
# https://github.com/ethereum/devp2p/blob/master/enr.md#test-vectors
const
uri = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"
pk = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"
seqNum = 1
id = "v4"
ip = "7f000001"
secp256k1 = "03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138"
udp = 0x765f
test "Test vector full encode loop":
var r: Record
let valid = r.fromURI(uri)
check valid
let res = toTypedRecord(r)
check res.isOk()
let typedRecord = res.value
check:
r.seqNum == seqNum
typedRecord.id == id
typedRecord.ip.value() == array[4, byte].fromHex(ip)
typedRecord.secp256k1.value() == array[33, byte].fromHex(secp256k1)
typedRecord.udp.value() == udp
typedRecord.tcp.isNone()
$r == """(1, id: "v4", ip: 127.0.0.1, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)"""
r.toURI() == uri
test "Test vector Record.init":
let privKey = PrivateKey.fromHex(
pk).expect("valid private key")
var r = Record.init(1, privKey,
Opt.some(IpAddress(family: IPv4, address_v4: array[4, byte].fromHex(ip))),
Opt.none(Port), Opt.some(Port(udp)))
check:
r.isOk()
r.value.seqNum == seqNum
r.value.toURI() == uri
suite "ENR encoding tests":
test "RLP serialisation": test "RLP serialisation":
var pk = PrivateKey.fromHex( let
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] keypair = KeyPair.random(rng[])
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[] ip = parseIpAddress("1.2.3.4")
check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""") port = Opt.some(Port(9000))
let encoded = rlp.encode(r) enr = Record.init(
let decoded = rlp.decode(encoded, enr.Record) 100, keypair.seckey, Opt.some(ip), port, port)
check($decoded == $r)
check(decoded.raw == r.raw)
test "RLP deserialisation without data": check:
enr.isOk()
testRlpEncodingLoop(enr.value)
test "Empty RLP":
expect ValueError: expect ValueError:
let decoded = rlp.decode([], enr.Record) let _ = rlp.decode([], enr.Record)
var r: Record var r: Record
check not fromBytes(r, []) check not fromBytes(r, [])
test "Base64 deserialisation without data": test "Invalid RLP":
var r: Record expect RlpError:
let sigValid = r.fromURI("enr:") let _ = rlp.decode([byte 0xf7], enr.Record)
check(not sigValid)
test "Parsing":
var r: Record var r: Record
let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") check not fromBytes(r, [byte 0xf7])
check(sigValid)
check($r == """(1, id: "v4", ip: 127.0.0.1, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""") test "No RLP list":
expect ValueError:
let _ = rlp.decode([byte 0x7f], enr.Record)
test "Bad base64":
var r: Record var r: Record
let sigValid = r.fromURI("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") check not fromBytes(r, [byte 0x7f])
check(not sigValid)
test "Bad rlp": test "ENR with RLP list value":
var r: Record type
let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOOnrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8") RlpTestList = object
check(not sigValid) number: uint16
data: seq[byte]
text: string
test "Create from ENode address":
let let
keypair = KeyPair.random(rng[]) rlpList = RlpTestList(number: 72, data: @[byte 0x0, 0x1, 0x2], text: "Hi there")
ip = parseIpAddress("10.20.30.40") pk = PrivateKey.fromHex(
port = Opt.some(Port(9000)) "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
ip = parseIpAddress("5.6.7.8")
port = Opt.some(Port(1234))
customPairs = [toFieldPair("some_list", rlpList)]
enr = Record.init( enr = Record.init(
100, keypair.seckey, Opt.some(ip), port, port,@[])[] 123, pk, Opt.some(ip), Opt.none(Port), port, customPairs)
typedEnr = get enr.toTypedRecord()
check: check:
typedEnr.secp256k1.isSome() enr.isOk()
typedEnr.secp256k1.get == keypair.pubkey.toRawCompressed() $enr.value == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, some_list: (Raw RLP list) 0xCE4883000102884869207468657265, udp: 1234)"""
testRlpEncodingLoop(enr.value)
typedEnr.ip.isSome() test "Base64 encode loop":
typedEnr.ip.get() == [byte 10, 20, 30, 40] const encodedBase64 = "-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"
var r: Record
check:
r.fromBase64(encodedBase64)
toBase64(r) == encodedBase64
typedEnr.tcp.isSome() test "Invalid base64":
typedEnr.tcp.get() == 9000 var r: Record
let valid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
check not valid
typedEnr.udp.isSome() test "URI encode loop":
typedEnr.udp.get() == 9000 let
keypair = KeyPair.random(rng[])
ip = parseIpAddress("1.2.3.4")
port = Opt.some(Port(9000))
res = Record.init(
100, keypair.seckey, Opt.some(ip), port, port)
check res.isOk()
let enr = res.value()
let uri = enr.toURI()
var enr2: Record
let valid = enr2.fromURI(uri)
check(valid)
check(enr == enr2)
test "ENR without address": test "Invalid URI: empty":
var r: Record
let valid = r.fromURI("")
check not valid
test "Invalid URI: no payload":
var r: Record
let valid = r.fromURI("enr:")
check not valid
suite "ENR init tests":
test "Record.init minimum fields":
let let
keypair = KeyPair.random(rng[]) keypair = KeyPair.random(rng[])
port = Opt.none(Port) port = Opt.none(Port)
@ -95,6 +166,8 @@ suite "ENR":
typedEnr = get enr.toTypedRecord() typedEnr = get enr.toTypedRecord()
check: check:
testRlpEncodingLoop(enr)
typedEnr.secp256k1.isSome() typedEnr.secp256k1.isSome()
typedEnr.secp256k1.get() == keypair.pubkey.toRawCompressed() typedEnr.secp256k1.get() == keypair.pubkey.toRawCompressed()
@ -106,17 +179,113 @@ suite "ENR":
typedEnr.tcp6.isNone() typedEnr.tcp6.isNone()
typedEnr.udp6.isNone() typedEnr.udp6.isNone()
test "ENR init size too big": test "Record.init only ipv4":
let pk = PrivateKey.fromHex( let
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] keypair = KeyPair.random(rng[])
ip = parseIpAddress("1.2.3.4")
port = Opt.some(Port(9000))
enr = Record.init(
100, keypair.seckey, Opt.some(ip), port, port)[]
typedEnr = get enr.toTypedRecord()
check:
typedEnr.ip.isSome()
typedEnr.ip.get() == [byte 1, 2, 3, 4]
typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9000
typedEnr.udp.isSome()
typedEnr.udp.get() == 9000
test "Record.init only ipv6":
let
keypair = KeyPair.random(rng[])
ip = parseIpAddress("::1")
port = Opt.some(Port(9000))
enr = Record.init(
100, keypair.seckey, Opt.some(ip), port, port)[]
typedEnr = get enr.toTypedRecord()
check:
typedEnr.ip.isNone()
typedEnr.tcp.isSome()
typedEnr.tcp.value() == 9000
typedEnr.udp.isSome()
typedEnr.udp.value() == 9000
typedEnr.ip6.isSome()
typedEnr.ip6.get() == [byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
typedEnr.tcp6.isNone()
typedEnr.udp6.isNone()
test "Record.init max ENR size":
let
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
block: # This gives ENR of 300 bytes encoded block: # This gives ENR of 300 bytes encoded
let r = initRecord(1, pk, {"maxvalue": repeat(byte 2, 169),}) let r = Record.init(
1, pk, extraFields = [toFieldPair("maxvalue", repeat(byte 2, 169))]
)
check r.isOk() check r.isOk()
block: # This gives ENR of 301 bytes encoded block: # This gives ENR of 301 bytes encoded
let r = initRecord(1, pk, {"maxplus1": repeat(byte 2, 170),}) let r = Record.init(
1, pk, extraFields = [toFieldPair("maxplus1", repeat(byte 2, 170))]
)
check r.isErr() check r.isErr()
test "PreDefinedKeys in custom pairs":
let
keypair = KeyPair.random(rng[])
customPairs = [toFieldPair("ip", @[byte 1, 1, 1, 1])]
expect AssertionDefect:
let _ = Record.init(
1, keypair.seckey, extraFields = customPairs)
test "Duplicate key":
# With duplicate key, the last one should be used (insert)
let
keypair = KeyPair.random(rng[])
customPairs = [
toFieldPair("test1", @[byte 1, 1, 1, 1]),
toFieldPair("test2", "abc"),
toFieldPair("test1", "1.2.3.4")
]
let res = Record.init(
1, keypair.seckey, extraFields = customPairs)
check: res.isOk()
let
enr = res.value
test1Field = enr.get("test1", string)
test2Field = enr.get("test2", string)
check:
test1Field.isOk()
test2Field.isOk()
test1Field.value == "1.2.3.4"
test2Field.value == "abc"
test "Record.init sorted":
let
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
customPairs = [
toFieldPair("abc", 1234'u),
toFieldPair("z", [byte 0]),
toFieldPair("123", "abc"),
toFieldPair("a12", 1'u)
]
r = Record.init(123, pk, extraFields = customPairs)
check:
r.isOk()
$r.value == """(123, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, z: 0x00)"""
suite "ENR update tests":
test "ENR update": test "ENR update":
let let
pk = PrivateKey.fromHex( pk = PrivateKey.fromHex(
@ -125,67 +294,81 @@ suite "ENR":
var r = Record.init(1, pk, Opt.none(IpAddress), Opt.none(Port), Opt.none(Port))[] var r = Record.init(1, pk, Opt.none(IpAddress), Opt.none(Port), Opt.none(Port))[]
block: # Insert new k:v pair, update of seqNum should occur. block: # Insert new k:v pair, update of seqNum should occur.
let updated = r.update(pk, [newField]) let updated = r.update(pk, extraFields = [newField])
check updated.isOk() check updated.isOk()
check: check:
r.get("test", uint).get() == 123 r.get("test", uint).get() == 123
r.seqNum == 2 r.seqNum == 2
block: # Insert same k:v pair, no update of seqNum should occur. block: # Insert same k:v pair, update of seqNum still occurs.
let updated = r.update(pk, [newField]) let updated = r.update(pk, extraFields = [newField])
check updated.isOk() check updated.isOk()
check: check:
r.get("test", uint).get() == 123 r.get("test", uint).get() == 123
r.seqNum == 2 r.seqNum == 3
block: # Insert k:v pair with changed value, update of seqNum should occur. block: # Insert k:v pair with changed value, update of seqNum should occur.
let updatedField = toFieldPair("test", 1234'u) let updatedField = toFieldPair("test", 1234'u)
let updated = r.update(pk, [updatedField]) let updated = r.update(pk, extraFields = [updatedField])
check updated.isOk() check updated.isOk()
check: check:
r.get("test", uint).get() == 1234 r.get("test", uint).get() == 1234
r.seqNum == 3 r.seqNum == 4
test "ENR update sorted": test "ENR update sorted":
let pk = PrivateKey.fromHex( let
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] pk = PrivateKey.fromHex(
var r = initRecord(123, pk, {"abc": 1234'u, "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
"z": [byte 0], customPairs = [
"123": "abc", toFieldPair("abc", 1234'u),
"a12": 1'u})[] toFieldPair("z", [byte 0]),
toFieldPair("123", "abc"),
toFieldPair("a12", 1'u)
]
res = Record.init(123, pk, extraFields = customPairs)
check res.isOk()
var r = res.value
check $r == """(123, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, z: 0x00)""" check $r == """(123, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, z: 0x00)"""
let newField = toFieldPair("test", 123'u) let newField = toFieldPair("test", 123'u)
let newField2 = toFieldPair("zzz", 123'u) let newField2 = toFieldPair("zzz", 123'u)
let updated = r.update(pk, [newField, newField2]) let updated = r.update(pk, extraFields = [newField, newField2])
check updated.isOk() check updated.isOk()
check $r == """(124, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, test: 123, z: 0x00, zzz: 123)""" check $r == """(124, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, test: 123, z: 0x00, zzz: 123)"""
test "ENR update size too big": test "ENR update too large":
let pk = PrivateKey.fromHex( let
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
customPairs = [toFieldPair("maxvalue", repeat(byte 2, 169))]
var r = initRecord(1, pk, {"maxvalue": repeat(byte 2, 169),}) res = Record.init(123, pk, extraFields = customPairs)
check r.isOk()
check res.isOk()
var r = res.value
let newField = toFieldPair("test", 123'u) let newField = toFieldPair("test", 123'u)
let updated = r[].update(pk, [newField]) let updated = r.update(pk, extraFields = [newField])
check updated.isErr() check updated.isErr()
test "ENR update invalid key": test "ENR update with wrong private key":
let pk = PrivateKey.fromHex( let
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
var r = initRecord(1, pk, {"abc": 1'u,}) res = Record.init(123, pk)
check r.isOk() check res.isOk()
var r = res.value
let let
wrongPk = PrivateKey.random(rng[]) wrongPk = PrivateKey.random(rng[])
newField = toFieldPair("test", 123'u) newField = toFieldPair("test", 123'u)
updated = r[].update(wrongPk, [newField]) updated = r.update(wrongPk, extraFields = [newField])
check updated.isErr() check updated.isErr()
test "ENR update address": test "ENR update addresses":
let let
pk = PrivateKey.fromHex( pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[] "5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
@ -200,7 +383,7 @@ suite "ENR":
r.tryGet("ip", uint).isNone() r.tryGet("ip", uint).isNone()
r.tryGet("tcp", uint).isSome() r.tryGet("tcp", uint).isSome()
r.tryGet("udp", uint).isSome() r.tryGet("udp", uint).isSome()
r.seqNum == 1 r.seqNum == 2
block: block:
let updated = r.update(pk, Opt.none(IpAddress), let updated = r.update(pk, Opt.none(IpAddress),
@ -210,7 +393,7 @@ suite "ENR":
r.tryGet("ip", uint).isNone() r.tryGet("ip", uint).isNone()
r.tryGet("tcp", uint).isSome() r.tryGet("tcp", uint).isSome()
r.tryGet("udp", uint).isSome() r.tryGet("udp", uint).isSome()
r.seqNum == 2 r.seqNum == 3
block: block:
let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")), let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")),
@ -229,10 +412,10 @@ suite "ENR":
typedEnr.udp.isSome() typedEnr.udp.isSome()
typedEnr.udp.get() == 9000 typedEnr.udp.get() == 9000
r.seqNum == 3 r.seqNum == 4
block: block:
let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")), let updated = r.update(pk, Opt.some(parseIpAddress("1.2.3.4")),
Opt.some(Port(9001)), Opt.some(Port(9001))) Opt.some(Port(9001)), Opt.some(Port(9001)))
check updated.isOk() check updated.isOk()
@ -240,7 +423,7 @@ suite "ENR":
check: check:
typedEnr.ip.isSome() typedEnr.ip.isSome()
typedEnr.ip.get() == [byte 10, 20, 30, 40] typedEnr.ip.get() == [byte 1, 2, 3, 4]
typedEnr.tcp.isSome() typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9001 typedEnr.tcp.get() == 9001
@ -248,71 +431,4 @@ suite "ENR":
typedEnr.udp.isSome() typedEnr.udp.isSome()
typedEnr.udp.get() == 9001 typedEnr.udp.get() == 9001
r.seqNum == 4 r.seqNum == 5
test "ENR with RLP list value":
type
RlpTestList = object
number: uint16
data: seq[byte]
text: string
let rlpList =
RlpTestList(number: 72, data: @[byte 0x0, 0x1, 0x2], text: "Hi there")
let pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8],
"some_list": rlpList})[]
check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, some_list: (Raw RLP list) 0xCE4883000102884869207468657265, udp: 1234)""")
let encoded = rlp.encode(r)
let decoded = rlp.decode(encoded, enr.Record)
check($decoded == $r)
check(decoded.raw == r.raw)
test "ENR IP addresses ":
let pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
block: # valid ipv4
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[]
check($r == """(123, id: "v4", ip: 5.6.7.8, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
let encoded = rlp.encode(r)
let decoded = rlp.decode(encoded, enr.Record)
check($decoded == $r)
check(decoded.raw == r.raw)
block: # invalid ipv4
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7]})[]
check($r == """(123, id: "v4", ip: (Invalid) 0x050607, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
let encoded = rlp.encode(r)
let decoded = rlp.decode(encoded, enr.Record)
check($decoded == $r)
check(decoded.raw == r.raw)
block: # valid ipv4 + ipv6
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8],
"ip6": [byte 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6]})[]
check($r == """(123, id: "v4", ip: 5.6.7.8, ip6: 102::102:304:506, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
let encoded = rlp.encode(r)
let decoded = rlp.decode(encoded, enr.Record)
check($decoded == $r)
check(decoded.raw == r.raw)
block: # invalid ipv4 + ipv6
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8, 9],
"ip6": [byte 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5]})[]
check($r == """(123, id: "v4", ip: (Invalid) 0x0506070809, ip6: (Invalid) 0x010200000000000000000102030405, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
let encoded = rlp.encode(r)
let decoded = rlp.decode(encoded, enr.Record)
check($decoded == $r)
check(decoded.raw == r.raw)