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: [].}
import
std/[strutils, macros, algorithm, net],
std/[strutils, sequtils, macros, algorithm, net],
nimcrypto/[keccak, utils],
stew/base64,
results,
@ -19,33 +19,17 @@ import
".."/../[rlp, keys],
../../net/utils
export options, results, keys
export results, rlp, keys
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.
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
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
kString,
kNum,
@ -64,6 +48,28 @@ type
listRaw: seq[byte] ## Differently from the other kinds, this is is stored
## 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]
template toField[T](v: T): Field =
@ -94,8 +100,45 @@ func `==`(a, b: Field): bool =
else:
false
template toFieldPair*(key: string, value: auto): FieldPair =
(key, toField(value))
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(
seqNum: uint64, pk: PrivateKey,
pairs: openArray[FieldPair]): EnrResult[seq[byte]] =
@ -112,16 +155,18 @@ func makeEnrRaw(
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 content =
block:
var w = initRlpList(pairs.len * 2 + 1)
w.append(seqNum, pairs)
let sig = signNR(pk, toSign)
let signature = signNR(pk, content)
var raw = block:
var w = initRlpList(pairs.len * 2 + 2)
w.append(sig.toRaw())
w.append(seqNum, pairs)
let raw =
block:
var w = initRlpList(pairs.len * 2 + 2)
w.append(signature.toRaw())
w.append(seqNum, pairs)
if raw.len > maxEnrSize:
err("Record exceeds maximum size")
@ -129,7 +174,7 @@ func makeEnrRaw(
ok(raw)
func makeEnrAux(
seqNum: uint64, pk: PrivateKey,
seqNum: uint64, id: string, pk: PrivateKey,
pairs: openArray[FieldPair]): EnrResult[Record] =
var record: Record
record.pairs = @pairs
@ -137,76 +182,70 @@ func makeEnrAux(
let pubkey = pk.toPublicKey()
record.pairs.add(("id", Field(kind: kString, str: "v4")))
record.pairs.add(("secp256k1",
record.pairs.insert(("id", Field(kind: kString, str: id)))
record.pairs.insert(("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 =
pairs: untyped{nkTableConstr}): untyped {.deprecated: "Please use Record.init instead".} =
## Initialize a `Record` with given sequence number, private key and k:v
## pairs.
##
## 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:
c.expectKind(nnkExprColonExpr)
c[1] = newCall(bindSym"toField", c[1])
result = quote do:
makeEnrAux(`seqNum`, `pk`, `pairs`)
makeEnrAux(`seqNum`, "v4", `pk`, `pairs`)
template toFieldPair*(key: string, value: auto): FieldPair =
(key, toField(value))
func addAddress(
func insertAddress(
fields: var seq[FieldPair],
ip: Opt[IpAddress],
tcpPort, udpPort: Opt[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.
## Insert address data.
## Incomplete address information is allowed (example: Port but not IP) as
## that information might be already in the ENR or added later.
if ip.isSome():
let
ipExt = ip.get()
isV6 = ipExt.family == IPv6
case ip.value.family
of IPv4:
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)
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))
if tcpPort.isSome():
fields.insert(("tcp", tcpPort.get().uint16.toField))
if udpPort.isSome():
fields.insert(("udp", udpPort.get().uint16.toField))
func init*(
T: type Record,
seqNum: uint64, pk: PrivateKey,
ip: Opt[IpAddress],
tcpPort, udpPort: Opt[Port],
ip: Opt[IpAddress] = Opt.none(IpAddress),
tcpPort: Opt[Port] = Opt.none(Port),
udpPort: Opt[Port] = Opt.none(Port),
extraFields: openArray[FieldPair] = []):
EnrResult[T] =
## Initialize a `Record` with given sequence number, private key, optional
## ip address, tcp port, udp port, and optional custom k:v pairs.
##
## Can fail in case the record exceeds the `maxEnrSize`.
doAssert(not hasPredefinedKey(extraFields), "Predefined key in custom pairs")
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)
fields.insertAddress(ip, tcpPort, udpPort)
fields.insert extraFields
makeEnrAux(seqNum, "v4", pk, fields)
func getField(r: Record, name: string, field: var Field): bool =
# 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[])
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*(
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(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,
record: var Record,
pk: PrivateKey,
ip: Opt[IpAddress],
ip: Opt[IpAddress] = Opt.none(IpAddress),
tcpPort: Opt[Port] = Opt.none(Port),
udpPort: Opt[Port] = Opt.none(Port),
extraFields: openArray[FieldPair] = []):
@ -332,18 +320,35 @@ func update*(
## 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.
## If none of the k:v pairs are changed, the sequence number of the `Record`
## will still be incremented and a new signature will be applied.
##
## Providing an `Opt.none` for `ip`, `tcpPort` or `udpPort` will leave the
## corresponding field untouched.
##
## 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: 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.
fields.addAddress(ip, tcpPort, udpPort)
fields.add extraFields
r.update(pk, fields)
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")
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] =
## Get the value from the provided key.
@ -548,7 +553,7 @@ func read*(
rlp: var Rlp, T: type Record):
T {.raises: [RlpError, ValueError].} =
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
# split of RLP deserialisation errors from this.
raise newException(ValueError, "Could not deserialize")

View File

@ -253,7 +253,7 @@ func updateRecord*(
d: Protocol, enrFields: openArray[(string, seq[byte])]): 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)
d.localNode.record.update(d.privateKey, extraFields = fields)
# 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?
@ -992,6 +992,8 @@ proc newProtocol*(
var record: Record
if previousRecord.isSome():
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,
customEnrFields).expect("Record within size limits and correct key")
else:
@ -1045,6 +1047,8 @@ proc newProtocol*(
record =
if previousRecord.isSome():
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,
customEnrFields).expect("Record within size limits and correct key")
res

View File

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

View File

@ -8,85 +8,156 @@
import
std/[sequtils, net],
stew/byteutils,
unittest2,
../../eth/p2p/discoveryv5/enr, ../../eth/[keys, rlp]
let rng = newRng()
suite "ENR":
test "Serialization":
var pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
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)
proc testRlpEncodingLoop*(r: enr.Record): bool =
let encoded = rlp.encode(r)
let decoded = rlp.decode(encoded, enr.Record)
decoded == r
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":
var pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
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)
let
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)
test "RLP deserialisation without data":
check:
enr.isOk()
testRlpEncodingLoop(enr.value)
test "Empty RLP":
expect ValueError:
let decoded = rlp.decode([], enr.Record)
let _ = rlp.decode([], enr.Record)
var r: Record
check not fromBytes(r, [])
test "Base64 deserialisation without data":
var r: Record
let sigValid = r.fromURI("enr:")
check(not sigValid)
test "Invalid RLP":
expect RlpError:
let _ = rlp.decode([byte 0xf7], enr.Record)
test "Parsing":
var r: Record
let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
check(sigValid)
check($r == """(1, id: "v4", ip: 127.0.0.1, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""")
check not fromBytes(r, [byte 0xf7])
test "No RLP list":
expect ValueError:
let _ = rlp.decode([byte 0x7f], enr.Record)
test "Bad base64":
var r: Record
let sigValid = r.fromURI("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
check(not sigValid)
check not fromBytes(r, [byte 0x7f])
test "Bad rlp":
var r: Record
let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOOnrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
check(not sigValid)
test "ENR with RLP list value":
type
RlpTestList = object
number: uint16
data: seq[byte]
text: string
test "Create from ENode address":
let
keypair = KeyPair.random(rng[])
ip = parseIpAddress("10.20.30.40")
port = Opt.some(Port(9000))
rlpList = RlpTestList(number: 72, data: @[byte 0x0, 0x1, 0x2], text: "Hi there")
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
ip = parseIpAddress("5.6.7.8")
port = Opt.some(Port(1234))
customPairs = [toFieldPair("some_list", rlpList)]
enr = Record.init(
100, keypair.seckey, Opt.some(ip), port, port,@[])[]
typedEnr = get enr.toTypedRecord()
123, pk, Opt.some(ip), Opt.none(Port), port, customPairs)
check:
typedEnr.secp256k1.isSome()
typedEnr.secp256k1.get == keypair.pubkey.toRawCompressed()
enr.isOk()
$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()
typedEnr.ip.get() == [byte 10, 20, 30, 40]
test "Base64 encode loop":
const encodedBase64 = "-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"
var r: Record
check:
r.fromBase64(encodedBase64)
toBase64(r) == encodedBase64
typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9000
test "Invalid base64":
var r: Record
let valid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
check not valid
typedEnr.udp.isSome()
typedEnr.udp.get() == 9000
test "URI encode loop":
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
keypair = KeyPair.random(rng[])
port = Opt.none(Port)
@ -95,6 +166,8 @@ suite "ENR":
typedEnr = get enr.toTypedRecord()
check:
testRlpEncodingLoop(enr)
typedEnr.secp256k1.isSome()
typedEnr.secp256k1.get() == keypair.pubkey.toRawCompressed()
@ -106,17 +179,113 @@ suite "ENR":
typedEnr.tcp6.isNone()
typedEnr.udp6.isNone()
test "ENR init size too big":
let pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
test "Record.init only ipv4":
let
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
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()
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()
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":
let
pk = PrivateKey.fromHex(
@ -125,67 +294,81 @@ suite "ENR":
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.
let updated = r.update(pk, [newField])
let updated = r.update(pk, extraFields = [newField])
check updated.isOk()
check:
r.get("test", uint).get() == 123
r.seqNum == 2
block: # Insert same k:v pair, no update of seqNum should occur.
let updated = r.update(pk, [newField])
block: # Insert same k:v pair, update of seqNum still occurs.
let updated = r.update(pk, extraFields = [newField])
check updated.isOk()
check:
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.
let updatedField = toFieldPair("test", 1234'u)
let updated = r.update(pk, [updatedField])
let updated = r.update(pk, extraFields = [updatedField])
check updated.isOk()
check:
r.get("test", uint).get() == 1234
r.seqNum == 3
r.seqNum == 4
test "ENR update sorted":
let pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
var r = initRecord(123, pk, {"abc": 1234'u,
"z": [byte 0],
"123": "abc",
"a12": 1'u})[]
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)
]
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)"""
let newField = toFieldPair("test", 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 $r == """(124, 123: "abc", a12: 1, abc: 1234, id: "v4", secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, test: 123, z: 0x00, zzz: 123)"""
test "ENR update size too big":
let pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
test "ENR update too large":
let
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),})
check r.isOk()
res = Record.init(123, pk, extraFields = customPairs)
check res.isOk()
var r = res.value
let newField = toFieldPair("test", 123'u)
let updated = r[].update(pk, [newField])
let updated = r.update(pk, extraFields = [newField])
check updated.isErr()
test "ENR update invalid key":
let pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
test "ENR update with wrong private key":
let
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d").expect("valid private key")
var r = initRecord(1, pk, {"abc": 1'u,})
check r.isOk()
res = Record.init(123, pk)
check res.isOk()
var r = res.value
let
wrongPk = PrivateKey.random(rng[])
newField = toFieldPair("test", 123'u)
updated = r[].update(wrongPk, [newField])
updated = r.update(wrongPk, extraFields = [newField])
check updated.isErr()
test "ENR update address":
test "ENR update addresses":
let
pk = PrivateKey.fromHex(
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
@ -200,7 +383,7 @@ suite "ENR":
r.tryGet("ip", uint).isNone()
r.tryGet("tcp", uint).isSome()
r.tryGet("udp", uint).isSome()
r.seqNum == 1
r.seqNum == 2
block:
let updated = r.update(pk, Opt.none(IpAddress),
@ -210,7 +393,7 @@ suite "ENR":
r.tryGet("ip", uint).isNone()
r.tryGet("tcp", uint).isSome()
r.tryGet("udp", uint).isSome()
r.seqNum == 2
r.seqNum == 3
block:
let updated = r.update(pk, Opt.some(parseIpAddress("10.20.30.40")),
@ -229,10 +412,10 @@ suite "ENR":
typedEnr.udp.isSome()
typedEnr.udp.get() == 9000
r.seqNum == 3
r.seqNum == 4
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)))
check updated.isOk()
@ -240,7 +423,7 @@ suite "ENR":
check:
typedEnr.ip.isSome()
typedEnr.ip.get() == [byte 10, 20, 30, 40]
typedEnr.ip.get() == [byte 1, 2, 3, 4]
typedEnr.tcp.isSome()
typedEnr.tcp.get() == 9001
@ -248,71 +431,4 @@ suite "ENR":
typedEnr.udp.isSome()
typedEnr.udp.get() == 9001
r.seqNum == 4
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)
r.seqNum == 5