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.
This commit is contained in:
Csaba Kiraly 2022-03-07 01:19:49 +01:00 committed by Eric Mastro
parent b843c8823c
commit 9a01791e11
19 changed files with 744 additions and 942 deletions

View File

@ -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",

View File

@ -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
export spr, encoding, messages, messages_encoding, node, nodes_verification, protocol, routing_table, sessions, transport

View File

@ -1,4 +0,0 @@
import
../private/eth/p2p/discoveryv5/enr
export enr

4
libp2pdht/discv5/spr.nim Normal file
View File

@ -0,0 +1,4 @@
import
../private/eth/p2p/discoveryv5/spr
export spr

View File

@ -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..<ephKeyPos + int(ephKeySize)]
ephKey = ? PublicKey.fromRaw(ephKeyRaw)
ephKey = ? keys.PublicKey.fromRaw(ephKeyRaw)
var record: Option[enr.Record]
var record: Option[SignedPeerRecord]
let recordPos = ephKeyPos + int(ephKeySize)
if authdata.len() > 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(

View File

@ -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)

View File

@ -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]

View File

@ -15,7 +15,7 @@ import
chronicles,
libp2p/routing_record,
libp2p/signed_envelope,
"."/[messages, enr],
"."/[messages, spr],
../../../../dht/providers_encoding
from stew/objects import checkedEnumAssign

View File

@ -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():

View File

@ -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))

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -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..<amount:
result.add(nodeAtDistance(n, rng, d, ip))
proc nodesAtDistanceUniqueIp*(
n: Node, rng: var BrHmacDrbgContext, d: uint32, amount: int,
ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1")): seq[Node] =
var ta = initTAddress(ip, Port(0))
for i in 0..<amount:
ta.inc()
result.add(nodeAtDistance(n, rng, d, ValidIpAddress.init(ta.address())))
proc addSeenNode*(d: discv5_protocol.Protocol, n: Node): bool =
# Add it as a seen node, warning: for testing convenience only!
n.seen = true
d.addNode(n)
func udpExample*(_: type MultiAddress): MultiAddress =
## creates a new udp multiaddress on a random port
Multiaddress.init("/ip4/0.0.0.0/udp/0")
@ -64,3 +97,19 @@ func udpExamples*(_: type MultiAddress, count: int): seq[MultiAddress] =
for i in 1..count:
res.add Multiaddress.init("/ip4/0.0.0.0/udp/" & $i).get
return res
proc toSignedPeerRecord*(privKey: crypto.PrivateKey) : SignedPeerRecord =
## handle conversion between the two worlds
let pr = PeerRecord.init(
peerId = PeerId.init(privKey.getPublicKey.get).get,
addresses = MultiAddress.udpExamples(3))
return SignedPeerRecord.init(privKey, pr)
.expect("Should init SignedPeerRecord with private key")
proc example*(T: type SignedPeerRecord): T =
let
rng = crypto.newRng()
privKey = crypto.PrivateKey.random(rng[]).expect("Valid rng")
privKey.toSignedPeerRecord

View File

@ -26,22 +26,9 @@ import
libp2p/multicodec,
libp2p/signed_envelope
# suite "Providers Tests: node alone":
proc toSignedPeerRecord(privKey: crypto.PrivateKey) : SignedPeerRecord =
## handle conversion between the two worlds
let pr = PeerRecord.init(
peerId = PeerId.init(privKey.getPublicKey.get).get,
addresses = MultiAddress.udpExamples(3))
return SignedPeerRecord.init(privKey, pr).expect("Should init SignedPeerRecord with private key")
# trace "IDs", discNodeId, digest, mh, peerId=result.peerId.hex
proc bootstrapNodes(
nodecount: int,
bootnodes: openArray[Record],
bootnodes: openArray[SignedPeerRecord],
rng = keys.newRng()
) : seq[(discv5_protocol.Protocol, keys.PrivateKey)] =

View File

@ -1,87 +0,0 @@
import
stew/shims/net, bearssl, chronos,
eth/keys,
libp2pdht/discv5/[enr, node, routing_table],
libp2pdht/discv5/protocol as discv5_protocol
export net
proc localAddress*(port: int): Address =
Address(ip: ValidIpAddress.init("127.0.0.1"), port: Port(port))
proc initDiscoveryNode*(
rng: ref BrHmacDrbgContext,
privKey: PrivateKey,
address: Address,
bootstrapRecords: openArray[Record] = [],
localEnrFields: openArray[(string, seq[byte])] = [],
previousRecord = none[enr.Record]()):
discv5_protocol.Protocol =
# set bucketIpLimit to allow bucket split
let config = DiscoveryConfig.init(1000, 24, 5)
let protocol = newProtocol(
privKey,
some(address.ip),
some(address.port), some(address.port),
bindPort = address.port,
bootstrapRecords = bootstrapRecords,
localEnrFields = localEnrFields,
previousRecord = previousRecord,
config = config,
rng = rng)
protocol.open()
protocol
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 generateNRandomNodes*(rng: ref BrHmacDrbgContext, n: int): seq[Node] =
var res = newSeq[Node]()
for i in 1..n:
let node = generateNode(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, PrivateKey) =
while true:
let pk = 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..<amount:
result.add(nodeAtDistance(n, rng, d, ip))
proc nodesAtDistanceUniqueIp*(
n: Node, rng: var BrHmacDrbgContext, d: uint32, amount: int,
ip: ValidIpAddress = ValidIpAddress.init("127.0.0.1")): seq[Node] =
var ta = initTAddress(ip, Port(0))
for i in 0..<amount:
ta.inc()
result.add(nodeAtDistance(n, rng, d, ValidIpAddress.init(ta.address())))
proc addSeenNode*(d: discv5_protocol.Protocol, n: Node): bool =
# Add it as a seen node, warning: for testing convenience only!
n.seen = true
d.addNode(n)

View File

@ -5,26 +5,26 @@ import
chronos, chronicles, stint, asynctest, stew/shims/net,
stew/byteutils, bearssl,
eth/keys,
libp2pdht/discv5/[transport, enr, node, routing_table, encoding, sessions, messages, nodes_verification],
libp2pdht/discv5/[transport, spr, node, routing_table, encoding, sessions, messages, nodes_verification],
libp2pdht/discv5/protocol as discv5_protocol,
./discv5_test_helper
../dht/test_helper
suite "Discovery v5 Tests":
var rng: ref HmacDrbgContext
setup:
rng = newRng()
rng = keys.newRng()
test "GetNode":
# TODO: This could be tested in just a routing table only context
let
node = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20302))
targetNode = generateNode(PrivateKey.random(rng[]))
node = initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20302))
targetNode = generateNode(keys.PrivateKey.random(rng[]))
check node.addNode(targetNode)
for i in 0..<1000:
discard node.addNode(generateNode(PrivateKey.random(rng[])))
discard node.addNode(generateNode(keys.PrivateKey.random(rng[])))
let n = node.getNode(targetNode.id)
check n.isSome()
@ -35,12 +35,12 @@ suite "Discovery v5 Tests":
test "Node deletion":
let
bootnode = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20301))
rng, keys.PrivateKey.random(rng[]), localAddress(20301))
node1 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302),
rng, keys.PrivateKey.random(rng[]), localAddress(20302),
@[bootnode.localNode.record])
node2 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20303),
rng, keys.PrivateKey.random(rng[]), localAddress(20303),
@[bootnode.localNode.record])
pong1 = await discv5_protocol.ping(node1, bootnode.localNode)
pong2 = await discv5_protocol.ping(node1, node2.localNode)
@ -136,10 +136,10 @@ suite "Discovery v5 Tests":
("e24a7bc9051058f918646b0f6e3d16884b2a55a15553b89bab910d55ebc36116", 256'u16)
]
let targetId = toNodeId(PublicKey.fromHex(targetKey)[])
let targetId = toNodeId(keys.PublicKey.fromHex(targetKey)[])
for (key, d) in testValues:
let id = toNodeId(PrivateKey.fromHex(key)[].toPublicKey())
let id = toNodeId(keys.PrivateKey.fromHex(key)[].toPublicKey())
check logDistance(targetId, id) == d
test "Distance to id check":
@ -170,7 +170,7 @@ suite "Discovery v5 Tests":
("1a5b34809116e3790b2258a45e7ef03b11af786503fb1a6d4b4a8ca021ad653c", 256'u16)
]
let targetId = toNodeId(PublicKey.fromHex(targetKey)[])
let targetId = toNodeId(keys.PublicKey.fromHex(targetKey)[])
for (id, d) in testValues:
check idAtDistance(targetId, d) == parse(id, UInt256, 16)
@ -178,9 +178,9 @@ suite "Discovery v5 Tests":
test "FindNode Test":
const dist = 253'u16
let
mainNodeKey = PrivateKey.fromHex(
mainNodeKey = keys.PrivateKey.fromHex(
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617")[]
testNodeKey = PrivateKey.fromHex(
testNodeKey = keys.PrivateKey.fromHex(
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")[]
mainNode = initDiscoveryNode(rng, mainNodeKey, localAddress(20301))
testNode = initDiscoveryNode(rng, testNodeKey, localAddress(20302))
@ -194,14 +194,14 @@ suite "Discovery v5 Tests":
check (await testNode.ping(mainNode.localNode)).isOk()
check (await mainNode.ping(testNode.localNode)).isOk()
# Get ENR of the node itself
# Get SPR of the node itself
var discovered =
await findNode(testNode, mainNode.localNode, @[0'u16])
check:
discovered.isOk
discovered[].len == 1
discovered[][0] == mainNode.localNode
# Get ENRs of nodes added at provided logarithmic distance
# Get SPRs of nodes added at provided logarithmic distance
discovered =
await findNode(testNode, mainNode.localNode, @[dist])
check discovered.isOk
@ -246,11 +246,11 @@ suite "Discovery v5 Tests":
test "FindNode with test table":
let mainNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301))
# Generate 1000 random nodes and add to our main node's routing table
for i in 0..<1000:
discard mainNode.addSeenNode(generateNode(PrivateKey.random(rng[]))) # for testing only!
discard mainNode.addSeenNode(generateNode(keys.PrivateKey.random(rng[]))) # for testing only!
let
neighbours = mainNode.neighbours(mainNode.localNode.id)
@ -261,7 +261,7 @@ suite "Discovery v5 Tests":
let
testNode = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302),
rng, keys.PrivateKey.random(rng[]), localAddress(20302),
@[mainNode.localNode.record])
discovered = await findNode(testNode, mainNode.localNode,
@[closestDistance])
@ -277,13 +277,13 @@ suite "Discovery v5 Tests":
nodeCount = 17
let bootNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301))
bootNode.start()
var nodes = newSeqOfCap[discv5_protocol.Protocol](nodeCount)
nodes.add(bootNode)
for i in 1 ..< nodeCount:
nodes.add(initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301 + i),
nodes.add(initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301 + i),
@[bootNode.localNode.record]))
# Make sure all nodes have "seen" each other by forcing pings
@ -311,10 +311,10 @@ suite "Discovery v5 Tests":
test "Resolve target":
let
mainNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301))
lookupNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20302))
targetKey = PrivateKey.random(rng[])
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20302))
targetKey = keys.PrivateKey.random(rng[])
targetAddress = localAddress(20303)
targetNode = initDiscoveryNode(rng, targetKey, targetAddress)
targetId = targetNode.localNode.id
@ -334,12 +334,12 @@ suite "Discovery v5 Tests":
n.get().record.seqNum == targetSeqNum
# Node will be removed because of failed findNode request.
# Bring target back online, update seqNum in ENR, check if we get the
# updated ENR.
# Bring target back online, update seqNum in SPR, check if we get the
# updated SPR.
block:
targetNode.open()
# Request the target ENR and manually add it to the routing table.
# Ping for handshake based ENR passing will not work as our previous
# Request the target SPR and manually add it to the routing table.
# Ping for handshake based SPR passing will not work as our previous
# session will still be in the LRU cache.
let nodes = await mainNode.findNode(targetNode.localNode, @[0'u16])
check:
@ -348,8 +348,8 @@ suite "Discovery v5 Tests":
mainNode.addNode(nodes[][0])
targetSeqNum.inc()
# need to add something to get the enr sequence number incremented
let update = targetNode.updateRecord({"addsomefield": @[byte 1]})
# need to add something to get the spr sequence number incremented
let update = targetNode.updateRecord()
check update.isOk()
var n = mainNode.getNode(targetId)
@ -367,14 +367,14 @@ suite "Discovery v5 Tests":
# Add the updated version
discard mainNode.addNode(n.get())
# Update seqNum in ENR again, ping lookupNode to be added in routing table,
# close targetNode, resolve should lookup, check if we get updated ENR.
# Update seqNum in SPR again, ping lookupNode to be added in routing table,
# close targetNode, resolve should lookup, check if we get updated SPR.
block:
targetSeqNum.inc()
let update = targetNode.updateRecord({"addsomefield": @[byte 2]})
let update = targetNode.updateRecord()
check update.isOk()
# ping node so that its ENR gets added
# ping node so that its SPR gets added
check (await targetNode.ping(lookupNode.localNode)).isOk()
# ping node so that it becomes "seen" and thus will be forwarded on a
# findNode request
@ -391,31 +391,30 @@ suite "Discovery v5 Tests":
await mainNode.closeWait()
await lookupNode.closeWait()
test "Random nodes with enr field filter":
# We no longer support field filtering
# test "Random nodes with spr field filter":
# let
# lookupNode = initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301))
# targetNode = generateNode(keys.PrivateKey.random(rng[]))
# otherNode = generateNode(keys.PrivateKey.random(rng[]))
# anotherNode = generateNode(keys.PrivateKey.random(rng[]))
# check:
# lookupNode.addNode(targetNode)
# lookupNode.addNode(otherNode)
# lookupNode.addNode(anotherNode)
# let discovered = lookupNode.randomNodes(10)
# check discovered.len == 3
# let discoveredFiltered = lookupNode.randomNodes(10,
# ("test", @[byte 1,2,3,4]))
# check discoveredFiltered.len == 1 and discoveredFiltered.contains(targetNode)
# await lookupNode.closeWait()
test "New protocol with spr":
let
lookupNode = initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301))
targetFieldPair = toFieldPair("test", @[byte 1,2,3,4])
targetNode = generateNode(PrivateKey.random(rng[]), localEnrFields = [targetFieldPair])
otherFieldPair = toFieldPair("test", @[byte 1,2,3,4,5])
otherNode = generateNode(PrivateKey.random(rng[]), localEnrFields = [otherFieldPair])
anotherNode = generateNode(PrivateKey.random(rng[]))
check:
lookupNode.addNode(targetNode)
lookupNode.addNode(otherNode)
lookupNode.addNode(anotherNode)
let discovered = lookupNode.randomNodes(10)
check discovered.len == 3
let discoveredFiltered = lookupNode.randomNodes(10,
("test", @[byte 1,2,3,4]))
check discoveredFiltered.len == 1 and discoveredFiltered.contains(targetNode)
await lookupNode.closeWait()
test "New protocol with enr":
let
privKey = PrivateKey.random(rng[])
privKey = keys.PrivateKey.random(rng[])
ip = some(ValidIpAddress.init("127.0.0.1"))
port = Port(20301)
node = newProtocol(privKey, ip, some(port), some(port), bindPort = port,
@ -436,23 +435,23 @@ suite "Discovery v5 Tests":
# Defect (for now?) on incorrect key use
expect ResultDefect:
let incorrectKeyUpdates = newProtocol(PrivateKey.random(rng[]),
let incorrectKeyUpdates = newProtocol(keys.PrivateKey.random(rng[]),
ip, some(port), some(port), bindPort = port, rng = rng,
previousRecord = some(updatesNode.getRecord()))
test "Update node record with revalidate":
let
mainNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301))
testNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20302))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20302))
testNodeId = testNode.localNode.id
check:
# Get node with current ENR in routing table.
# Get node with current SPR in routing table.
# Handshake will get done here.
(await testNode.ping(mainNode.localNode)).isOk()
testNode.updateRecord({"test" : @[byte 1]}).isOk()
testNode.updateRecord().isOk()
testNode.localNode.record.seqNum == 2
# Get the node from routing table, seqNum should still be 1.
@ -461,7 +460,7 @@ suite "Discovery v5 Tests":
n.isSome()
n.get.record.seqNum == 1
# This should not do a handshake and thus the new ENR must come from the
# This should not do a handshake and thus the new SPR must come from the
# findNode(0)
await mainNode.revalidateNode(n.get)
@ -477,16 +476,16 @@ suite "Discovery v5 Tests":
test "Update node record with handshake":
let
mainNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20301))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20301))
testNode =
initDiscoveryNode(rng, PrivateKey.random(rng[]), localAddress(20302))
initDiscoveryNode(rng, keys.PrivateKey.random(rng[]), localAddress(20302))
testNodeId = testNode.localNode.id
# Add the node (from the record, so new node!) so no handshake is done yet.
check: mainNode.addNode(testNode.localNode.record)
check:
testNode.updateRecord({"test" : @[byte 1]}).isOk()
testNode.updateRecord().isOk()
testNode.localNode.record.seqNum == 2
# Get the node from routing table, seqNum should still be 1.
@ -495,7 +494,7 @@ suite "Discovery v5 Tests":
n.isSome()
n.get.record.seqNum == 1
# This should do a handshake and update the ENR through that.
# This should do a handshake and update the SPR through that.
check (await testNode.ping(mainNode.localNode)).isOk()
# Get the node from routing table, and check if record got updated.
@ -510,17 +509,17 @@ suite "Discovery v5 Tests":
test "Verify records of nodes message":
let
port = Port(9000)
fromNoderecord = enr.Record.init(1, PrivateKey.random(rng[]),
fromNoderecord = SignedPeerRecord.init(1, keys.PrivateKey.random(rng[]),
some(ValidIpAddress.init("11.12.13.14")),
some(port), some(port))[]
fromNode = newNode(fromNoderecord)[]
pk = PrivateKey.random(rng[])
pk = keys.PrivateKey.random(rng[])
targetDistance = @[logDistance(fromNode.id, pk.toPublicKey().toNodeId())]
limit = 16
block: # Duplicates
let
record = enr.Record.init(
record = SignedPeerRecord.init(
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
@ -530,7 +529,7 @@ suite "Discovery v5 Tests":
check nodes.len == 1
# Node id duplicates
let recordSameId = enr.Record.init(
let recordSameId = SignedPeerRecord.init(
1, pk, some(ValidIpAddress.init("212.13.14.15")),
some(port), some(port))[]
records.add(recordSameId)
@ -539,7 +538,7 @@ suite "Discovery v5 Tests":
block: # No address
let
recordNoAddress = enr.Record.init(
recordNoAddress = SignedPeerRecord.init(
1, pk, none(ValidIpAddress), some(port), some(port))[]
records = [recordNoAddress]
test = verifyNodesRecords(records, fromNode, limit, targetDistance)
@ -547,7 +546,7 @@ suite "Discovery v5 Tests":
block: # Invalid address - site local
let
recordInvalidAddress = enr.Record.init(
recordInvalidAddress = SignedPeerRecord.init(
1, pk, some(ValidIpAddress.init("10.1.2.3")),
some(port), some(port))[]
records = [recordInvalidAddress]
@ -556,7 +555,7 @@ suite "Discovery v5 Tests":
block: # Invalid address - loopback
let
recordInvalidAddress = enr.Record.init(
recordInvalidAddress = SignedPeerRecord.init(
1, pk, some(ValidIpAddress.init("127.0.0.1")),
some(port), some(port))[]
records = [recordInvalidAddress]
@ -565,7 +564,7 @@ suite "Discovery v5 Tests":
block: # Invalid distance
let
recordInvalidDistance = enr.Record.init(
recordInvalidDistance = SignedPeerRecord.init(
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
records = [recordInvalidDistance]
@ -574,7 +573,7 @@ suite "Discovery v5 Tests":
block: # Invalid distance but distance validation is disabled
let
recordInvalidDistance = enr.Record.init(
recordInvalidDistance = SignedPeerRecord.init(
1, pk, some(ValidIpAddress.init("12.13.14.15")),
some(port), some(port))[]
records = [recordInvalidDistance]
@ -593,15 +592,15 @@ suite "Discovery v5 Tests":
test "Handshake cleanup: different ids":
# Node to test the handshakes on.
let receiveNode = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302))
rng, keys.PrivateKey.random(rng[]), localAddress(20302))
# Create random packets with same ip but different node ids
# and "receive" them on receiveNode
let a = localAddress(20303)
for i in 0 ..< 5:
let
privKey = PrivateKey.random(rng[])
enrRec = enr.Record.init(1, privKey,
privKey = keys.PrivateKey.random(rng[])
enrRec = SignedPeerRecord.init(1, privKey,
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record")
@ -624,13 +623,13 @@ suite "Discovery v5 Tests":
test "Handshake cleanup: different ips":
# Node to test the handshakes on.
let receiveNode = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302))
rng, keys.PrivateKey.random(rng[]), localAddress(20302))
# Create random packets with same node ids but different ips
# and "receive" them on receiveNode
let
privKey = PrivateKey.random(rng[])
enrRec = enr.Record.init(1, privKey,
privKey = keys.PrivateKey.random(rng[])
enrRec = SignedPeerRecord.init(1, privKey,
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record")
@ -654,14 +653,14 @@ suite "Discovery v5 Tests":
test "Handshake duplicates":
# Node to test the handshakes on.
let receiveNode = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302))
rng, keys.PrivateKey.random(rng[]), localAddress(20302))
# Create random packets with same node ids and same ips
# and "receive" them on receiveNode
let
a = localAddress(20303)
privKey = PrivateKey.random(rng[])
enrRec = enr.Record.init(1, privKey,
privKey = keys.PrivateKey.random(rng[])
enrRec = SignedPeerRecord.init(1, privKey,
some(ValidIpAddress.init("127.0.0.1")), some(Port(9000)),
some(Port(9000))).expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record")
@ -687,9 +686,9 @@ suite "Discovery v5 Tests":
test "Talkreq no protocol":
let
node1 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302))
rng, keys.PrivateKey.random(rng[]), localAddress(20302))
node2 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20303))
rng, keys.PrivateKey.random(rng[]), localAddress(20303))
talkresp = await discv5_protocol.talkReq(node1, node2.localNode,
@[byte 0x01], @[])
@ -703,9 +702,9 @@ suite "Discovery v5 Tests":
test "Talkreq echo protocol":
let
node1 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302))
rng, keys.PrivateKey.random(rng[]), localAddress(20302))
node2 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20303))
rng, keys.PrivateKey.random(rng[]), localAddress(20303))
talkProtocol = "echo".toBytes()
proc handler(protocol: TalkProtocol, request: seq[byte], fromId: NodeId, fromUdpAddress: Address): seq[byte]
@ -728,9 +727,9 @@ suite "Discovery v5 Tests":
test "Talkreq register protocols":
let
node1 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20302))
rng, keys.PrivateKey.random(rng[]), localAddress(20302))
node2 = initDiscoveryNode(
rng, PrivateKey.random(rng[]), localAddress(20303))
rng, keys.PrivateKey.random(rng[]), localAddress(20303))
talkProtocol = "echo".toBytes()
proc handler(protocol: TalkProtocol, request: seq[byte], fromId: NodeId, fromUdpAddress: Address): seq[byte]

View File

@ -6,17 +6,18 @@ import
asynctest/unittest2,
stint, stew/byteutils, stew/shims/net,
eth/[keys,rlp],
libp2pdht/discv5/[messages, messages_encoding, encoding, enr, node, sessions]
libp2pdht/discv5/[messages, messages_encoding, encoding, spr, node, sessions],
../dht/test_helper
suite "Discovery v5.1 Protocol Message Encodings":
test "Ping Request":
let
enrSeq = 1'u64
p = PingMessage(enrSeq: enrSeq)
sprSeq = 1'u64
p = PingMessage(sprSeq: sprSeq)
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(p, reqId)
check encoded.toHex == "01c20101"
check byteutils.toHex(encoded) == "01c20101"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -25,18 +26,18 @@ suite "Discovery v5.1 Protocol Message Encodings":
check:
message.reqId == reqId
message.kind == ping
message.ping.enrSeq == enrSeq
message.ping.sprSeq == sprSeq
test "Pong Response":
let
enrSeq = 1'u64
sprSeq = 1'u64
ip = IpAddress(family: IpAddressFamily.IPv4, address_v4: [127.byte, 0, 0, 1])
port = 5000'u16
p = PongMessage(enrSeq: enrSeq, ip: ip, port: port)
p = PongMessage(sprSeq: sprSeq, ip: ip, port: port)
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(p, reqId)
check encoded.toHex == "02ca0101847f000001821388"
check byteutils.toHex(encoded) == "02ca0101847f000001821388"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -45,7 +46,7 @@ suite "Discovery v5.1 Protocol Message Encodings":
check:
message.reqId == reqId
message.kind == pong
message.pong.enrSeq == enrSeq
message.pong.sprSeq == sprSeq
message.pong.ip == ip
message.pong.port == port
@ -56,7 +57,7 @@ suite "Discovery v5.1 Protocol Message Encodings":
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(fn, reqId)
check encoded.toHex == "03c501c3820100"
check byteutils.toHex(encoded) == "03c501c3820100"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -74,7 +75,7 @@ suite "Discovery v5.1 Protocol Message Encodings":
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(n, reqId)
check encoded.toHex == "04c30101c0"
check byteutils.toHex(encoded) == "04c30101c0"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -84,19 +85,19 @@ suite "Discovery v5.1 Protocol Message Encodings":
message.reqId == reqId
message.kind == nodes
message.nodes.total == total
message.nodes.enrs.len() == 0
message.nodes.sprs.len() == 0
test "Nodes Response (multiple)":
var e1, e2: Record
check e1.fromURI("enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg")
check e2.fromURI("enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU")
var s1, s2: SignedPeerRecord
check s1.fromURI("spr:CiQIARIgWu2YZ5TQVW1gWEfvQijVHqSBtjCbwDt9VppJvYpHX9wSAgMBGlUKJgAkCAESIFrtmGeU0FVtYFhH70Io1R6kgbYwm8A7fVaaSb2KR1_cEKz1xZEGGgsKCQQAAAAAkQIAARoLCgkEAAAAAJECAAIaCwoJBAAAAACRAgADKkAjkK9DeWc82uzd1AEjRr-ksQyRiQ7vYGV4Af3FAEi0JgHvMC8RCQdqn2wBYxvBcyO8o1XMEEKCG01AUZrJlCkD")
check s2.fromURI("spr:CiQIARIguW3cNKnlvRsJVmV0ddgFMmvfAQLi0zf4tlt_6WGA03YSAgMBGlUKJgAkCAESILlt3DSp5b0bCVZldHXYBTJr3wEC4tM3-LZbf-lhgNN2EKz1xZEGGgsKCQQAAAAAkQIAARoLCgkEAAAAAJECAAIaCwoJBAAAAACRAgADKkC4Y9NkDHf-71LOvZon0NjmyzQnkm4IlAJGMDPS0cbSgIF3-2cECC5mRiXHjcHWlI5hPpxUURxFyIgSp7XX1jIL")
let
total = 0x1'u32
n = NodesMessage(total: total, enrs: @[e1, e2])
n = NodesMessage(total: total, sprs: @[s1, s2])
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(n, reqId)
check encoded.toHex == "04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235"
check byteutils.toHex(encoded) == "04f9018f0101f9018ab8c30a24080112205aed986794d0556d605847ef4228d51ea481b6309bc03b7d569a49bd8a475fdc120203011a550a260024080112205aed986794d0556d605847ef4228d51ea481b6309bc03b7d569a49bd8a475fdc10acf5c591061a0b0a090400000000910200011a0b0a090400000000910200021a0b0a090400000000910200032a402390af4379673cdaecddd4012346bfa4b10c91890eef60657801fdc50048b42601ef302f1109076a9f6c01631bc17323bca355cc1042821b4d40519ac9942903b8c30a2408011220b96ddc34a9e5bd1b0956657475d805326bdf0102e2d337f8b65b7fe96180d376120203011a550a26002408011220b96ddc34a9e5bd1b0956657475d805326bdf0102e2d337f8b65b7fe96180d37610acf5c591061a0b0a090400000000910200011a0b0a090400000000910200021a0b0a090400000000910200032a40b863d3640c77feef52cebd9a27d0d8e6cb3427926e089402463033d2d1c6d2808177fb6704082e664625c78dc1d6948e613e9c54511c45c88812a7b5d7d6320b"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -106,9 +107,9 @@ suite "Discovery v5.1 Protocol Message Encodings":
message.reqId == reqId
message.kind == nodes
message.nodes.total == total
message.nodes.enrs.len() == 2
message.nodes.enrs[0] == e1
message.nodes.enrs[1] == e2
message.nodes.sprs.len() == 2
message.nodes.sprs[0] == s1
message.nodes.sprs[1] == s2
test "Talk Request":
let
@ -116,7 +117,7 @@ suite "Discovery v5.1 Protocol Message Encodings":
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(tr, reqId)
check encoded.toHex == "05c901846563686f826869"
check byteutils.toHex(encoded) == "05c901846563686f826869"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -134,7 +135,7 @@ suite "Discovery v5.1 Protocol Message Encodings":
reqId = RequestId(id: @[1.byte])
let encoded = encodeMessage(tr, reqId)
check encoded.toHex == "06c401826869"
check byteutils.toHex(encoded) == "06c401826869"
let decoded = decodeMessage(encoded)
check decoded.isOk()
@ -147,12 +148,12 @@ suite "Discovery v5.1 Protocol Message Encodings":
test "Ping with too large RequestId":
let
enrSeq = 1'u64
p = PingMessage(enrSeq: enrSeq)
sprSeq = 1'u64
p = PingMessage(sprSeq: sprSeq)
# 1 byte too large
reqId = RequestId(id: @[0.byte, 1, 2, 3, 4, 5, 6, 7, 8])
let encoded = encodeMessage(p, reqId)
check encoded.toHex == "01cb8900010203040506070801"
check byteutils.toHex(encoded) == "01cb8900010203040506070801"
let decoded = decodeMessage(encoded)
check decoded.isErr()
@ -176,8 +177,8 @@ suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
sharedSecret = "0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e"
let
pub = PublicKey.fromHex(publicKey)[]
priv = PrivateKey.fromHex(secretKey)[]
pub = keys.PublicKey.fromHex(publicKey)[]
priv = keys.PrivateKey.fromHex(secretKey)[]
eph = ecdhRawFull(priv, pub)
check:
eph.data == hexToSeqByte(sharedSecret)
@ -197,8 +198,8 @@ suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
let secrets = deriveKeys(
NodeId.fromHex(nodeIdA),
NodeId.fromHex(nodeIdB),
PrivateKey.fromHex(ephemeralKey)[],
PublicKey.fromHex(destPubkey)[],
keys.PrivateKey.fromHex(ephemeralKey)[],
keys.PublicKey.fromHex(destPubkey)[],
hexToSeqByte(challengeData))
check:
@ -216,7 +217,7 @@ suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
idSignature = "0x94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6"
let
privKey = PrivateKey.fromHex(staticKey)[]
privKey = keys.PrivateKey.fromHex(staticKey)[]
signature = createIdSignature(
privKey,
hexToSeqByte(challengeData),
@ -254,18 +255,18 @@ suite "Discovery v5.1 Packet Encodings Test Vectors":
var
codecA, codecB: Codec
nodeA, nodeB: Node
privKeyA, privKeyB: PrivateKey
privKeyA, privKeyB: keys.PrivateKey
setup:
privKeyA = PrivateKey.fromHex(nodeAKey)[] # sender -> 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