mirror of https://github.com/status-im/nim-eth.git
Merge pull request #231 from status-im/discv5-results
Improve error handling in discv5
This commit is contained in:
commit
cea370c4fa
|
@ -1,9 +1,11 @@
|
|||
import
|
||||
std/[tables, options], nimcrypto, stint, chronicles,
|
||||
std/[tables, options], nimcrypto, stint, chronicles, stew/results,
|
||||
types, node, enr, hkdf, ../enode, eth/[rlp, keys]
|
||||
|
||||
export keys
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
const
|
||||
idNoncePrefix = "discovery-id-nonce"
|
||||
keyAgreementPrefix = "discovery v5 key agreement"
|
||||
|
@ -14,7 +16,6 @@ const
|
|||
## with
|
||||
|
||||
type
|
||||
|
||||
PacketTag* = array[tagSize, byte]
|
||||
|
||||
AuthResponse = object
|
||||
|
@ -40,21 +41,18 @@ type
|
|||
ephemeralKey*: array[64, byte]
|
||||
response*: seq[byte]
|
||||
|
||||
RandomSourceDepleted* = object of CatchableError
|
||||
DecodeError* = enum
|
||||
HandshakeError = "discv5: handshake failed"
|
||||
PacketError = "discv5: invalid packet",
|
||||
DecryptError = "discv5: decryption failed",
|
||||
UnsupportedMessage = "discv5: unsupported message"
|
||||
|
||||
DecodeStatus* = enum
|
||||
Success,
|
||||
HandshakeError,
|
||||
PacketError,
|
||||
DecryptError
|
||||
DecodeResult*[T] = Result[T, DecodeError]
|
||||
EncodeResult*[T] = Result[T, cstring]
|
||||
|
||||
proc randomBytes2*(v: var openarray[byte]) =
|
||||
# TODO if this is called randomBytes it breaks calling the real randomBytes
|
||||
# in other modules... sigh, nim modules and global namespaces...
|
||||
# ideally, a new random library will take the place of both these proc's
|
||||
# in as setting without exceptions for such low-level constructs..
|
||||
if randomBytes(v) != v.len:
|
||||
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
||||
proc mapErrTo[T, E](r: Result[T, E], v: static DecodeError):
|
||||
DecodeResult[T] =
|
||||
r.mapErr(proc (e: E): DecodeError = v)
|
||||
|
||||
proc idNonceHash(nonce, ephkey: openarray[byte]): MDigest[256] =
|
||||
var ctx: sha256
|
||||
|
@ -64,29 +62,24 @@ proc idNonceHash(nonce, ephkey: openarray[byte]): MDigest[256] =
|
|||
ctx.update(ephkey)
|
||||
ctx.finish()
|
||||
|
||||
proc signIDNonce*(c: Codec, idNonce, ephKey: openarray[byte]): SignatureNR =
|
||||
let sig = signNR(c.privKey, idNonceHash(idNonce, ephKey))
|
||||
if sig.isErr:
|
||||
raise newException(CatchableError, "Could not sign idNonce")
|
||||
sig[]
|
||||
proc signIDNonce*(privKey: PrivateKey, idNonce, ephKey: openarray[byte]):
|
||||
Result[SignatureNR, cstring] =
|
||||
signNR(privKey, idNonceHash(idNonce, ephKey))
|
||||
|
||||
proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey,
|
||||
idNonce: openarray[byte], result: var HandshakeSecrets) =
|
||||
let eph = ecdhRawFull(priv, pub)
|
||||
if eph.isErr:
|
||||
raise newException(CatchableError, "ecdhRawFull failed")
|
||||
idNonce: openarray[byte]): Result[HandshakeSecrets, cstring] =
|
||||
let eph = ? ecdhRawFull(priv, pub)
|
||||
|
||||
# TODO: Unneeded allocation here
|
||||
var info = newSeqOfCap[byte](idNoncePrefix.len + 32 * 2)
|
||||
for i, c in keyAgreementPrefix: info.add(byte(c))
|
||||
info.add(n1.toByteArrayBE())
|
||||
info.add(n2.toByteArrayBE())
|
||||
|
||||
# echo "EPH: ", eph.data.toHex, " idNonce: ", challenge.idNonce.toHex, "info: ", info.toHex
|
||||
|
||||
static: assert(sizeof(result) == aesKeySize * 3)
|
||||
var res = cast[ptr UncheckedArray[byte]](addr result)
|
||||
hkdf(sha256, eph[].data, idNonce, info, toOpenArray(res, 0, sizeof(result) - 1))
|
||||
var secrets: HandshakeSecrets
|
||||
static: assert(sizeof(secrets) == aesKeySize * 3)
|
||||
var res = cast[ptr UncheckedArray[byte]](addr secrets)
|
||||
hkdf(sha256, eph.data, idNonce, info, toOpenArray(res, 0, sizeof(secrets) - 1))
|
||||
ok(secrets)
|
||||
|
||||
proc encryptGCM*(key, nonce, pt, authData: openarray[byte]): seq[byte] =
|
||||
var ectx: GCM[aes128]
|
||||
|
@ -96,49 +89,57 @@ proc encryptGCM*(key, nonce, pt, authData: openarray[byte]): seq[byte] =
|
|||
ectx.getTag(result.toOpenArray(pt.len, result.high))
|
||||
ectx.clear()
|
||||
|
||||
proc makeAuthHeader(c: Codec, toId: NodeID, nonce: array[gcmNonceSize, byte],
|
||||
handshakeSecrets: var HandshakeSecrets,
|
||||
challenge: Whoareyou): seq[byte] =
|
||||
proc encodeAuthHeader(c: Codec,
|
||||
toId: NodeID,
|
||||
nonce: array[gcmNonceSize, byte],
|
||||
challenge: Whoareyou):
|
||||
EncodeResult[(seq[byte], HandshakeSecrets)] =
|
||||
var resp = AuthResponse(version: 5)
|
||||
let ln = c.localNode
|
||||
|
||||
# TODO: What goes over the wire now in case of no updated ENR?
|
||||
if challenge.recordSeq < ln.record.seqNum:
|
||||
resp.record = ln.record
|
||||
|
||||
let ephKeys = KeyPair.random().tryGet()
|
||||
let ephKeys = ? KeyPair.random()
|
||||
let signature = ? signIDNonce(c.privKey, challenge.idNonce,
|
||||
ephKeys.pubkey.toRaw)
|
||||
resp.signature = signature.toRaw
|
||||
|
||||
resp.signature = c.signIDNonce(challenge.idNonce, ephKeys.pubkey.toRaw).toRaw
|
||||
|
||||
deriveKeys(ln.id, toId, ephKeys.seckey, challenge.pubKey, challenge.idNonce,
|
||||
handshakeSecrets)
|
||||
let secrets = ? deriveKeys(ln.id, toId, ephKeys.seckey, challenge.pubKey,
|
||||
challenge.idNonce)
|
||||
|
||||
let respRlp = rlp.encode(resp)
|
||||
|
||||
var zeroNonce: array[gcmNonceSize, byte]
|
||||
let respEnc = encryptGCM(handshakeSecrets.authRespKey, zeroNonce, respRLP, [])
|
||||
let respEnc = encryptGCM(secrets.authRespKey, zeroNonce, respRLP, [])
|
||||
|
||||
let header = AuthHeader(auth: nonce, idNonce: challenge.idNonce,
|
||||
scheme: authSchemeName, ephemeralKey: ephKeys.pubkey.toRaw, response: respEnc)
|
||||
rlp.encode(header)
|
||||
scheme: authSchemeName, ephemeralKey: ephKeys.pubkey.toRaw,
|
||||
response: respEnc)
|
||||
ok((rlp.encode(header), secrets))
|
||||
|
||||
proc `xor`[N: static[int], T](a, b: array[N, T]): array[N, T] =
|
||||
for i in 0 .. a.high:
|
||||
result[i] = a[i] xor b[i]
|
||||
|
||||
proc packetTag(destNode, srcNode: NodeID): PacketTag =
|
||||
let destId = destNode.toByteArrayBE()
|
||||
let srcId = srcNode.toByteArrayBE()
|
||||
let destidHash = sha256.digest(destId)
|
||||
let
|
||||
destId = destNode.toByteArrayBE()
|
||||
srcId = srcNode.toByteArrayBE()
|
||||
destidHash = sha256.digest(destId)
|
||||
result = srcId xor destidHash.data
|
||||
|
||||
proc encodeEncrypted*(c: Codec,
|
||||
proc encodePacket*(c: Codec,
|
||||
toId: NodeID,
|
||||
toAddr: Address,
|
||||
packetData: seq[byte],
|
||||
message: openarray[byte],
|
||||
challenge: Whoareyou):
|
||||
(seq[byte], array[gcmNonceSize, byte]) =
|
||||
EncodeResult[(seq[byte], array[gcmNonceSize, byte])] =
|
||||
var nonce: array[gcmNonceSize, byte]
|
||||
randomBytes2(nonce)
|
||||
if randomBytes(nonce) != nonce.len:
|
||||
return err("Could not randomize bytes")
|
||||
|
||||
var headEnc: seq[byte]
|
||||
|
||||
var writeKey: AesKey
|
||||
|
@ -151,22 +152,20 @@ proc encodeEncrypted*(c: Codec,
|
|||
# yet. That's fine, we will be responded with whoareyou.
|
||||
discard c.db.loadKeys(toId, toAddr, readKey, writeKey)
|
||||
else:
|
||||
var sec: HandshakeSecrets
|
||||
headEnc = c.makeAuthHeader(toId, nonce, sec, challenge)
|
||||
var secrets: HandshakeSecrets
|
||||
(headEnc, secrets) = ? c.encodeAuthHeader(toId, nonce, challenge)
|
||||
|
||||
writeKey = sec.writeKey
|
||||
writeKey = secrets.writeKey
|
||||
# TODO: is it safe to ignore the error here?
|
||||
discard c.db.storeKeys(toId, toAddr, sec.readKey, sec.writeKey)
|
||||
discard c.db.storeKeys(toId, toAddr, secrets.readKey, secrets.writeKey)
|
||||
|
||||
var body = packetData
|
||||
let tag = packetTag(toId, c.localNode.id)
|
||||
|
||||
var headBuf = newSeqOfCap[byte](tag.len + headEnc.len)
|
||||
headBuf.add(tag)
|
||||
headBuf.add(headEnc)
|
||||
|
||||
headBuf.add(encryptGCM(writeKey, nonce, body, tag))
|
||||
return (headBuf, nonce)
|
||||
var packet = newSeqOfCap[byte](tag.len + headEnc.len)
|
||||
packet.add(tag)
|
||||
packet.add(headEnc)
|
||||
packet.add(encryptGCM(writeKey, nonce, message, tag))
|
||||
ok((packet, nonce))
|
||||
|
||||
proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
|
||||
Option[seq[byte]] =
|
||||
|
@ -187,62 +186,67 @@ proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
|
|||
|
||||
return some(res)
|
||||
|
||||
type
|
||||
DecodePacketResult = enum
|
||||
decodingSuccessful
|
||||
invalidPacketPayload
|
||||
invalidPacketType
|
||||
unsupportedPacketType
|
||||
proc decodeMessage(body: openarray[byte]):
|
||||
DecodeResult[Message] {.raises:[Defect].} =
|
||||
if body.len < 1:
|
||||
return err(PacketError)
|
||||
|
||||
proc decodePacketBody(typ: byte,
|
||||
body: openarray[byte],
|
||||
res: var Packet): DecodePacketResult =
|
||||
if typ < PacketKind.low.byte or typ > PacketKind.high.byte:
|
||||
return invalidPacketType
|
||||
if body[0] < MessageKind.low.byte or body[0] > MessageKind.high.byte:
|
||||
return err(PacketError)
|
||||
|
||||
let kind = cast[PacketKind](typ)
|
||||
res = Packet(kind: kind)
|
||||
var rlp = rlpFromBytes(body)
|
||||
let kind = cast[MessageKind](body[0])
|
||||
var message = Message(kind: kind)
|
||||
var rlp = rlpFromBytes(body.toOpenArray(1, body.high))
|
||||
if rlp.enterList:
|
||||
res.reqId = rlp.read(RequestId)
|
||||
try:
|
||||
message.reqId = rlp.read(RequestId)
|
||||
except RlpError:
|
||||
return err(PacketError)
|
||||
|
||||
proc decode[T](rlp: var Rlp, v: var T) {.inline, nimcall.} =
|
||||
proc decode[T](rlp: var Rlp, v: var T)
|
||||
{.inline, nimcall, raises:[RlpError, ValueError, Defect].} =
|
||||
for k, v in v.fieldPairs:
|
||||
v = rlp.read(typeof(v))
|
||||
|
||||
case kind
|
||||
of unused: return invalidPacketPayload
|
||||
of ping: rlp.decode(res.ping)
|
||||
of pong: rlp.decode(res.pong)
|
||||
of findNode: rlp.decode(res.findNode)
|
||||
of nodes: rlp.decode(res.nodes)
|
||||
of regtopic, ticket, regconfirmation, topicquery:
|
||||
# TODO Implement these packet types
|
||||
return unsupportedPacketType
|
||||
try:
|
||||
case kind
|
||||
of unused: return err(PacketError)
|
||||
of ping: rlp.decode(message.ping)
|
||||
of pong: rlp.decode(message.pong)
|
||||
of findNode: rlp.decode(message.findNode)
|
||||
of nodes: rlp.decode(message.nodes)
|
||||
of regtopic, ticket, regconfirmation, topicquery:
|
||||
# TODO: Implement support for topic advertisement
|
||||
return err(UnsupportedMessage)
|
||||
except RlpError, ValueError:
|
||||
return err(PacketError)
|
||||
|
||||
return decodingSuccessful
|
||||
ok(message)
|
||||
else:
|
||||
return invalidPacketPayload
|
||||
err(PacketError)
|
||||
|
||||
proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
|
||||
challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node): bool =
|
||||
challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node):
|
||||
DecodeResult[void] {.raises:[Defect].} =
|
||||
if head.scheme != authSchemeName:
|
||||
warn "Unknown auth scheme"
|
||||
return false
|
||||
return err(HandshakeError)
|
||||
|
||||
var ephKey = PublicKey.fromRaw(head.ephemeralKey)
|
||||
if ephKey.isErr:
|
||||
return false
|
||||
let ephKey = ? PublicKey.fromRaw(head.ephemeralKey).mapErrTo(HandshakeError)
|
||||
|
||||
deriveKeys(fromId, c.localNode.id, c.privKey, ephKey[], challenge.idNonce,
|
||||
secrets)
|
||||
secrets = ? deriveKeys(fromId, c.localNode.id, c.privKey, ephKey,
|
||||
challenge.idNonce).mapErrTo(HandshakeError)
|
||||
|
||||
var zeroNonce: array[gcmNonceSize, byte]
|
||||
let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, [])
|
||||
if respData.isNone():
|
||||
return false
|
||||
return err(HandshakeError)
|
||||
|
||||
let authResp = rlp.decode(respData.get(), AuthResponse)
|
||||
var authResp: AuthResponse
|
||||
try:
|
||||
authResp = rlp.decode(respData.get(), AuthResponse)
|
||||
except RlpError, ValueError:
|
||||
return err(HandshakeError)
|
||||
# TODO:
|
||||
# 1. Should allow for not having an ENR included, solved for now by sending
|
||||
# whoareyou with always recordSeq of 0
|
||||
|
@ -252,16 +256,18 @@ proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
|
|||
# More TODO:
|
||||
# This will also not work if ENR does not contain an IP address or if the
|
||||
# IP address is out of date and doesn't match current UDP end point
|
||||
newNode = newNode(authResp.record)
|
||||
return true
|
||||
try:
|
||||
newNode = newNode(authResp.record)
|
||||
ok()
|
||||
except KeyError, ValueError:
|
||||
err(HandshakeError)
|
||||
|
||||
proc decodeEncrypted*(c: var Codec,
|
||||
proc decodePacket*(c: var Codec,
|
||||
fromId: NodeID,
|
||||
fromAddr: Address,
|
||||
input: openArray[byte],
|
||||
authTag: var AuthTag,
|
||||
newNode: var Node,
|
||||
packet: var Packet): DecodeStatus =
|
||||
newNode: var Node): DecodeResult[Message] =
|
||||
var r = rlpFromBytes(input.toOpenArray(tagSize, input.high))
|
||||
var auth: AuthHeader
|
||||
|
||||
|
@ -270,23 +276,27 @@ proc decodeEncrypted*(c: var Codec,
|
|||
|
||||
if r.isList:
|
||||
# Handshake - rlp list indicates auth-header
|
||||
auth = r.read(AuthHeader)
|
||||
try:
|
||||
auth = r.read(AuthHeader)
|
||||
except RlpError:
|
||||
return err(PacketError)
|
||||
authTag = auth.auth
|
||||
|
||||
let key = HandShakeKey(nodeId: fromId, address: $fromAddr)
|
||||
let challenge = c.handshakes.getOrDefault(key)
|
||||
if challenge.isNil:
|
||||
trace "Decoding failed (no challenge)"
|
||||
return HandshakeError
|
||||
return err(HandshakeError)
|
||||
|
||||
if auth.idNonce != challenge.idNonce:
|
||||
trace "Decoding failed (different nonce)"
|
||||
return HandshakeError
|
||||
return err(HandshakeError)
|
||||
|
||||
var sec: HandshakeSecrets
|
||||
if not c.decodeAuthResp(fromId, auth, challenge, sec, newNode):
|
||||
if c.decodeAuthResp(fromId, auth, challenge, sec, newNode).isErr:
|
||||
trace "Decoding failed (bad auth)"
|
||||
return HandshakeError
|
||||
return err(HandshakeError)
|
||||
|
||||
c.handshakes.del(key)
|
||||
|
||||
# For an incoming handshake, we are not sure the address in the ENR is there
|
||||
|
@ -299,50 +309,43 @@ proc decodeEncrypted*(c: var Codec,
|
|||
# TODO: is it safe to ignore the error here?
|
||||
discard c.db.storeKeys(fromId, fromAddr, sec.readKey, sec.writeKey)
|
||||
readKey = sec.readKey
|
||||
|
||||
else:
|
||||
# Message packet or random packet - rlp bytes (size 12) indicates auth-tag
|
||||
authTag = r.read(AuthTag)
|
||||
try:
|
||||
authTag = r.read(AuthTag)
|
||||
except RlpError:
|
||||
return err(PacketError)
|
||||
auth.auth = authTag
|
||||
var writeKey: AesKey
|
||||
if not c.db.loadKeys(fromId, fromAddr, readKey, writeKey):
|
||||
trace "Decoding failed (no keys)"
|
||||
return DecryptError
|
||||
# doAssert(false, "TODO: HANDLE ME!")
|
||||
return err(DecryptError)
|
||||
|
||||
let headSize = tagSize + r.position
|
||||
|
||||
let body = decryptGCM(
|
||||
let message = decryptGCM(
|
||||
readKey, auth.auth,
|
||||
input.toOpenArray(headSize, input.high),
|
||||
input.toOpenArray(0, tagSize - 1))
|
||||
if body.isNone():
|
||||
if message.isNone():
|
||||
discard c.db.deleteKeys(fromId, fromAddr)
|
||||
return DecryptError
|
||||
return err(DecryptError)
|
||||
|
||||
let packetData = body.get()
|
||||
if packetData.len > 1:
|
||||
let status = decodePacketBody(packetData[0],
|
||||
packetData.toOpenArray(1, packetData.high), packet)
|
||||
if status == decodingSuccessful:
|
||||
return Success
|
||||
else:
|
||||
debug "Failed to decode discovery packet", reason = status
|
||||
return PacketError
|
||||
decodeMessage(message.get())
|
||||
|
||||
proc newRequestId*(): Result[RequestId, cstring] =
|
||||
var id: RequestId
|
||||
if randomBytes(addr id, sizeof(id)) != sizeof(id):
|
||||
err("Could not randomize bytes")
|
||||
else:
|
||||
return PacketError
|
||||
|
||||
proc newRequestId*(): RequestId =
|
||||
if randomBytes(addr result, sizeof(result)) != sizeof(result):
|
||||
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
||||
ok(id)
|
||||
|
||||
proc numFields(T: typedesc): int =
|
||||
for k, v in fieldPairs(default(T)): inc result
|
||||
|
||||
proc encodePacket*[T: SomePacket](p: T, reqId: RequestId): seq[byte] =
|
||||
proc encodeMessage*[T: SomeMessage](p: T, reqId: RequestId): seq[byte] =
|
||||
result = newSeqOfCap[byte](64)
|
||||
result.add(packetKind(T).ord)
|
||||
# result.add(rlp.encode(p))
|
||||
result.add(messageKind(T).ord)
|
||||
|
||||
const sz = numFields(T)
|
||||
var writer = initRlpList(sz + 1)
|
||||
|
@ -350,6 +353,3 @@ proc encodePacket*[T: SomePacket](p: T, reqId: RequestId): seq[byte] =
|
|||
for k, v in fieldPairs(p):
|
||||
writer.append(v)
|
||||
result.add(writer.finish())
|
||||
|
||||
proc encodePacket*[T: SomePacket](p: T): seq[byte] =
|
||||
encodePacket(p, newRequestId())
|
||||
|
|
|
@ -8,6 +8,8 @@ import
|
|||
|
||||
export options
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
const
|
||||
maxEnrSize = 300
|
||||
minRlpListLen = 4 # for signature, seqId, "id" key, id
|
||||
|
@ -47,6 +49,8 @@ type
|
|||
of kBytes:
|
||||
bytes: seq[byte]
|
||||
|
||||
EnrResult*[T] = Result[T, cstring]
|
||||
|
||||
template toField[T](v: T): Field =
|
||||
when T is string:
|
||||
Field(kind: kString, str: v)
|
||||
|
@ -59,20 +63,24 @@ template toField[T](v: T): Field =
|
|||
else:
|
||||
{.error: "Unsupported field type".}
|
||||
|
||||
proc makeEnrAux(seqNum: uint64, pk: PrivateKey, pairs: openarray[(string, Field)]): Record =
|
||||
result.pairs = @pairs
|
||||
result.seqNum = seqNum
|
||||
proc makeEnrAux(seqNum: uint64, pk: PrivateKey,
|
||||
pairs: openarray[(string, Field)]): EnrResult[Record] =
|
||||
var record: Record
|
||||
record.pairs = @pairs
|
||||
record.seqNum = seqNum
|
||||
|
||||
let pubkey = pk.toPublicKey().tryGet()
|
||||
let pubkey = ? pk.toPublicKey()
|
||||
|
||||
result.pairs.add(("id", Field(kind: kString, str: "v4")))
|
||||
result.pairs.add(("secp256k1", Field(kind: kBytes, bytes: @(pubkey.toRawCompressed()))))
|
||||
record.pairs.add(("id", Field(kind: kString, str: "v4")))
|
||||
record.pairs.add(("secp256k1",
|
||||
Field(kind: kBytes, bytes: @(pubkey.toRawCompressed()))))
|
||||
|
||||
# Sort by key
|
||||
result.pairs.sort() do(a, b: (string, Field)) -> int:
|
||||
record.pairs.sort() do(a, b: (string, Field)) -> int:
|
||||
cmp(a[0], b[0])
|
||||
|
||||
proc append(w: var RlpWriter, seqNum: uint64, pairs: openarray[(string, Field)]): seq[byte] =
|
||||
proc append(w: var RlpWriter, seqNum: uint64,
|
||||
pairs: openarray[(string, Field)]): seq[byte] =
|
||||
w.append(seqNum)
|
||||
for (k, v) in pairs:
|
||||
w.append(k)
|
||||
|
@ -83,19 +91,20 @@ proc makeEnrAux(seqNum: uint64, pk: PrivateKey, pairs: openarray[(string, Field)
|
|||
w.finish()
|
||||
|
||||
let toSign = block:
|
||||
var w = initRlpList(result.pairs.len * 2 + 1)
|
||||
w.append(seqNum, result.pairs)
|
||||
var w = initRlpList(record.pairs.len * 2 + 1)
|
||||
w.append(seqNum, record.pairs)
|
||||
|
||||
let sig = signNR(pk, toSign)
|
||||
if sig.isErr:
|
||||
raise newException(CatchableError, "Could not sign ENR (internal error)")
|
||||
let sig = ? signNR(pk, toSign)
|
||||
|
||||
result.raw = block:
|
||||
var w = initRlpList(result.pairs.len * 2 + 2)
|
||||
w.append(sig[].toRaw())
|
||||
w.append(seqNum, result.pairs)
|
||||
record.raw = block:
|
||||
var w = initRlpList(record.pairs.len * 2 + 2)
|
||||
w.append(sig.toRaw())
|
||||
w.append(seqNum, record.pairs)
|
||||
|
||||
macro initRecord*(seqNum: uint64, pk: PrivateKey, pairs: untyped{nkTableConstr}): untyped =
|
||||
ok(record)
|
||||
|
||||
macro initRecord*(seqNum: uint64, pk: PrivateKey,
|
||||
pairs: untyped{nkTableConstr}): untyped =
|
||||
for c in pairs:
|
||||
c.expectKind(nnkExprColonExpr)
|
||||
c[1] = newCall(bindSym"toField", c[1])
|
||||
|
@ -110,7 +119,8 @@ proc init*(T: type Record, seqNum: uint64,
|
|||
pk: PrivateKey,
|
||||
ip: Option[IpAddress],
|
||||
tcpPort, udpPort: Port,
|
||||
extraFields: openarray[FieldPair] = []): T =
|
||||
extraFields: openarray[FieldPair] = []):
|
||||
EnrResult[T] =
|
||||
var fields = newSeq[FieldPair]()
|
||||
|
||||
if ip.isSome():
|
||||
|
@ -138,11 +148,11 @@ proc getField(r: Record, name: string, field: var Field): bool =
|
|||
field = v
|
||||
return true
|
||||
|
||||
proc requireKind(f: Field, kind: FieldKind) =
|
||||
proc requireKind(f: Field, kind: FieldKind) {.raises: [ValueError].} =
|
||||
if f.kind != kind:
|
||||
raise newException(ValueError, "Wrong field kind")
|
||||
|
||||
proc get*(r: Record, key: string, T: type): T =
|
||||
proc get*(r: Record, key: string, T: type): T {.raises: [ValueError, Defect].} =
|
||||
var f: Field
|
||||
if r.getField(key, f):
|
||||
when T is SomeInteger:
|
||||
|
@ -173,7 +183,7 @@ proc get*(r: Record, key: string, T: type): T =
|
|||
else:
|
||||
raise newException(KeyError, "Key not found in ENR: " & key)
|
||||
|
||||
proc get*(r: Record, T: type PublicKey): Option[T] {.raises: [Defect].} =
|
||||
proc get*(r: Record, T: type PublicKey): Option[T] =
|
||||
var pubkeyField: Field
|
||||
if r.getField("secp256k1", pubkeyField) and pubkeyField.kind == kBytes:
|
||||
let pk = PublicKey.fromRaw(pubkeyField.bytes)
|
||||
|
@ -205,7 +215,8 @@ proc toTypedRecord*(r: Record): Option[TypedRecord] =
|
|||
|
||||
return some(tr)
|
||||
|
||||
proc verifySignatureV4(r: Record, sigData: openarray[byte], content: seq[byte]): bool =
|
||||
proc verifySignatureV4(r: Record, sigData: openarray[byte], content: seq[byte]):
|
||||
bool =
|
||||
let publicKey = r.get(PublicKey)
|
||||
if publicKey.isSome:
|
||||
let sig = SignatureNR.fromRaw(sigData)
|
||||
|
@ -213,7 +224,7 @@ proc verifySignatureV4(r: Record, sigData: openarray[byte], content: seq[byte]):
|
|||
var h = keccak256.digest(content)
|
||||
return verify(sig[], h, publicKey.get)
|
||||
|
||||
proc verifySignature(r: Record): bool =
|
||||
proc verifySignature(r: Record): bool {.raises: [RlpError, Defect].} =
|
||||
var rlp = rlpFromBytes(r.raw)
|
||||
let sz = rlp.listLen
|
||||
if not rlp.enterList:
|
||||
|
@ -236,7 +247,7 @@ proc verifySignature(r: Record): bool =
|
|||
# Unknown Identity Scheme
|
||||
discard
|
||||
|
||||
proc fromBytesAux(r: var Record): bool =
|
||||
proc fromBytesAux(r: var Record): bool {.raises: [RlpError, Defect].} =
|
||||
if r.raw.len > maxEnrSize:
|
||||
return false
|
||||
|
||||
|
@ -275,7 +286,7 @@ proc fromBytesAux(r: var Record): bool =
|
|||
verifySignature(r)
|
||||
|
||||
proc fromBytes*(r: var Record, s: openarray[byte]): bool =
|
||||
# Loads ENR from rlp-encoded bytes, and validated the signature.
|
||||
## Loads ENR from rlp-encoded bytes, and validated the signature.
|
||||
r.raw = @s
|
||||
try:
|
||||
result = fromBytesAux(r)
|
||||
|
@ -283,7 +294,8 @@ proc fromBytes*(r: var Record, s: openarray[byte]): bool =
|
|||
discard
|
||||
|
||||
proc fromBase64*(r: var Record, s: string): bool =
|
||||
# Loads ENR from base64-encoded rlp-encoded bytes, and validated the signature.
|
||||
## Loads ENR from base64-encoded rlp-encoded bytes, and validated the
|
||||
## signature.
|
||||
try:
|
||||
r.raw = Base64Url.decode(s)
|
||||
result = fromBytesAux(r)
|
||||
|
@ -291,7 +303,8 @@ proc fromBase64*(r: var Record, s: string): bool =
|
|||
discard
|
||||
|
||||
proc fromURI*(r: var Record, s: string): bool =
|
||||
# Loads ENR from its text encoding: base64-encoded rlp-encoded bytes, prefixed with "enr:".
|
||||
## Loads ENR from its text encoding: base64-encoded rlp-encoded bytes,
|
||||
## prefixed with "enr:".
|
||||
const prefix = "enr:"
|
||||
if s.startsWith(prefix):
|
||||
result = r.fromBase64(s[prefix.len .. ^1])
|
||||
|
@ -328,7 +341,8 @@ proc `$`*(r: Record): string =
|
|||
|
||||
proc `==`*(a, b: Record): bool = a.raw == b.raw
|
||||
|
||||
proc read*(rlp: var Rlp, T: typedesc[Record]): T {.inline.} =
|
||||
proc read*(rlp: var Rlp, T: typedesc[Record]):
|
||||
T {.inline, raises:[RlpError, ValueError, Defect].} =
|
||||
if not result.fromBytes(rlp.rawData):
|
||||
raise newException(ValueError, "Could not deserialize")
|
||||
rlp.skipElem()
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import nimcrypto
|
||||
|
||||
proc hkdf*(HashType: typedesc, secret, salt, info: openarray[byte], output: var openarray[byte]) =
|
||||
proc hkdf*(HashType: typedesc, ikm, salt, info: openarray[byte],
|
||||
output: var openarray[byte]) =
|
||||
var ctx: HMAC[HashType]
|
||||
ctx.init(salt)
|
||||
ctx.update(secret)
|
||||
ctx.update(ikm)
|
||||
let prk = ctx.finish().data
|
||||
const hashLen = HashType.bits div 8
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import
|
|||
std/[net, hashes], nimcrypto, stint, chronicles,
|
||||
types, enr, eth/keys, ../enode
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
type
|
||||
Node* = ref object
|
||||
node*: ENode
|
||||
|
@ -11,21 +13,13 @@ type
|
|||
proc toNodeId*(pk: PublicKey): NodeId =
|
||||
readUintBE[256](keccak256.digest(pk.toRaw()).data)
|
||||
|
||||
proc newNode*(enode: ENode): Node =
|
||||
Node(node: enode,
|
||||
id: enode.pubkey.toNodeId())
|
||||
|
||||
# TODO: Lets not allow to create a node where enode info is not in sync with the
|
||||
# record
|
||||
proc newNode*(enode: ENode, r: Record): Node =
|
||||
Node(node: enode,
|
||||
id: enode.pubkey.toNodeId(),
|
||||
record: r)
|
||||
|
||||
proc newNode*(uriString: string): Node =
|
||||
newNode ENode.fromString(uriString).tryGet()
|
||||
|
||||
proc newNode*(pk: PublicKey, address: Address): Node =
|
||||
newNode ENode(pubkey: pk, address: address)
|
||||
|
||||
proc newNode*(r: Record): Node =
|
||||
# TODO: Handle IPv6
|
||||
var a: Address
|
||||
|
@ -37,26 +31,31 @@ proc newNode*(r: Record): Node =
|
|||
a = Address(ip: IpAddress(family: IpAddressFamily.IPv4,
|
||||
address_v4: ipBytes),
|
||||
udpPort: Port udpPort)
|
||||
except KeyError:
|
||||
except KeyError, ValueError:
|
||||
# TODO: This will result in a 0.0.0.0 address. Might introduce more bugs.
|
||||
# Maybe we shouldn't allow the creation of Node from Record without IP.
|
||||
# Will need some refactor though.
|
||||
discard
|
||||
|
||||
let pk = PublicKey.fromRaw(r.get("secp256k1", seq[byte]))
|
||||
if pk.isErr:
|
||||
warn "Could not recover public key", err = pk.error
|
||||
let pk = r.get(PublicKey)
|
||||
if pk.isNone():
|
||||
warn "Could not recover public key from ENR"
|
||||
return
|
||||
|
||||
result = newNode(ENode(pubkey: pk[], address: a))
|
||||
result.record = r
|
||||
let enode = ENode(pubkey: pk.get(), address: a)
|
||||
result = Node(node: enode,
|
||||
id: enode.pubkey.toNodeId(),
|
||||
record: r)
|
||||
|
||||
proc hash*(n: Node): hashes.Hash = hash(n.node.pubkey.toRaw)
|
||||
proc `==`*(a, b: Node): bool = (a.isNil and b.isNil) or (not a.isNil and not b.isNil and a.node.pubkey == b.node.pubkey)
|
||||
proc `==`*(a, b: Node): bool =
|
||||
(a.isNil and b.isNil) or
|
||||
(not a.isNil and not b.isNil and a.node.pubkey == b.node.pubkey)
|
||||
|
||||
proc address*(n: Node): Address {.inline.} = n.node.address
|
||||
|
||||
proc updateEndpoint*(n: Node, a: Address) {.inline.} = n.node.address = a
|
||||
proc updateEndpoint*(n: Node, a: Address) {.inline.} =
|
||||
n.node.address = a
|
||||
|
||||
proc `$`*(n: Node): string =
|
||||
if n == nil:
|
||||
|
|
|
@ -89,7 +89,7 @@ const
|
|||
alpha = 3 ## Kademlia concurrency factor
|
||||
lookupRequestLimit = 3
|
||||
findNodeResultLimit = 15 # applies in FINDNODE handler
|
||||
maxNodesPerPacket = 3
|
||||
maxNodesPerMessage = 3
|
||||
lookupInterval = 60.seconds ## Interval of launching a random lookup to
|
||||
## populate the routing table. go-ethereum seems to do 3 runs every 30
|
||||
## minutes. Trinity starts one every minute.
|
||||
|
@ -111,14 +111,16 @@ type
|
|||
db: Database
|
||||
routingTable: RoutingTable
|
||||
codec*: Codec
|
||||
awaitedPackets: Table[(NodeId, RequestId), Future[Option[Packet]]]
|
||||
awaitedMessages: Table[(NodeId, RequestId), Future[Option[Message]]]
|
||||
lookupLoop: Future[void]
|
||||
revalidateLoop: Future[void]
|
||||
bootstrapRecords*: seq[Record]
|
||||
|
||||
PendingRequest = object
|
||||
node: Node
|
||||
packet: seq[byte]
|
||||
message: seq[byte]
|
||||
|
||||
RandomSourceDepleted* = object of CatchableError
|
||||
|
||||
proc addNode*(d: Protocol, node: Node) =
|
||||
discard d.routingTable.addNode(node)
|
||||
|
@ -171,18 +173,20 @@ proc whoareyouMagic(toNode: NodeId): array[magicSize, byte] =
|
|||
for i, c in prefix: data[sizeof(toNode) + i] = byte(c)
|
||||
sha256.digest(data).data
|
||||
|
||||
proc isWhoAreYou(d: Protocol, msg: openArray[byte]): bool =
|
||||
if msg.len > d.whoareyouMagic.len:
|
||||
result = d.whoareyouMagic == msg.toOpenArray(0, magicSize - 1)
|
||||
proc isWhoAreYou(d: Protocol, packet: openArray[byte]): bool =
|
||||
if packet.len > d.whoareyouMagic.len:
|
||||
result = d.whoareyouMagic == packet.toOpenArray(0, magicSize - 1)
|
||||
|
||||
proc decodeWhoAreYou(d: Protocol, msg: openArray[byte]): Whoareyou =
|
||||
proc decodeWhoAreYou(d: Protocol, packet: openArray[byte]): Whoareyou =
|
||||
result = Whoareyou()
|
||||
result[] = rlp.decode(msg.toOpenArray(magicSize, msg.high), WhoareyouObj)
|
||||
result[] = rlp.decode(packet.toOpenArray(magicSize, packet.high), WhoareyouObj)
|
||||
|
||||
proc sendWhoareyou(d: Protocol, address: Address, toNode: NodeId, authTag: AuthTag) =
|
||||
trace "sending who are you", to = $toNode, toAddress = $address
|
||||
let challenge = Whoareyou(authTag: authTag, recordSeq: 0)
|
||||
encoding.randomBytes2(challenge.idNonce)
|
||||
|
||||
if randomBytes(challenge.idNonce) != challenge.idNonce.len:
|
||||
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
||||
# If there is already a handshake going on for this nodeid then we drop this
|
||||
# new one. Handshake will get cleaned up after `handshakeTimeout`.
|
||||
# If instead overwriting the handshake would be allowed, the handshake timeout
|
||||
|
@ -206,39 +210,39 @@ proc sendWhoareyou(d: Protocol, address: Address, toNode: NodeId, authTag: AuthT
|
|||
proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address, reqId: RequestId,
|
||||
nodes: openarray[Node]) =
|
||||
proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address,
|
||||
packet: NodesPacket, reqId: RequestId) {.nimcall.} =
|
||||
let (data, _) = d.codec.encodeEncrypted(toId, toAddr,
|
||||
encodePacket(packet, reqId), challenge = nil)
|
||||
message: NodesMessage, reqId: RequestId) {.nimcall.} =
|
||||
let (data, _) = d.codec.encodePacket(toId, toAddr,
|
||||
encodeMessage(message, reqId), challenge = nil).tryGet()
|
||||
d.send(toAddr, data)
|
||||
|
||||
var packet: NodesPacket
|
||||
packet.total = ceil(nodes.len / maxNodesPerPacket).uint32
|
||||
var message: NodesMessage
|
||||
message.total = ceil(nodes.len / maxNodesPerMessage).uint32
|
||||
|
||||
for i in 0 ..< nodes.len:
|
||||
packet.enrs.add(nodes[i].record)
|
||||
if packet.enrs.len == 3:
|
||||
d.sendNodes(toId, toAddr, packet, reqId)
|
||||
packet.enrs.setLen(0)
|
||||
message.enrs.add(nodes[i].record)
|
||||
if message.enrs.len == 3: # TODO: Uh, what is this?
|
||||
d.sendNodes(toId, toAddr, message, reqId)
|
||||
message.enrs.setLen(0)
|
||||
|
||||
if packet.enrs.len != 0:
|
||||
d.sendNodes(toId, toAddr, packet, reqId)
|
||||
if message.enrs.len != 0:
|
||||
d.sendNodes(toId, toAddr, message, reqId)
|
||||
|
||||
proc handlePing(d: Protocol, fromId: NodeId, fromAddr: Address,
|
||||
ping: PingPacket, reqId: RequestId) =
|
||||
ping: PingMessage, reqId: RequestId) =
|
||||
let a = fromAddr
|
||||
var pong: PongPacket
|
||||
var pong: PongMessage
|
||||
pong.enrSeq = ping.enrSeq
|
||||
pong.ip = case a.ip.family
|
||||
of IpAddressFamily.IPv4: @(a.ip.address_v4)
|
||||
of IpAddressFamily.IPv6: @(a.ip.address_v6)
|
||||
pong.port = a.udpPort.uint16
|
||||
|
||||
let (data, _) = d.codec.encodeEncrypted(fromId, fromAddr,
|
||||
encodePacket(pong, reqId), challenge = nil)
|
||||
let (data, _) = d.codec.encodePacket(fromId, fromAddr,
|
||||
encodeMessage(pong, reqId), challenge = nil).tryGet()
|
||||
d.send(fromAddr, data)
|
||||
|
||||
proc handleFindNode(d: Protocol, fromId: NodeId, fromAddr: Address,
|
||||
fn: FindNodePacket, reqId: RequestId) =
|
||||
fn: FindNodeMessage, reqId: RequestId) =
|
||||
if fn.distance == 0:
|
||||
d.sendNodes(fromId, fromAddr, reqId, [d.localNode])
|
||||
else:
|
||||
|
@ -246,7 +250,7 @@ proc handleFindNode(d: Protocol, fromId: NodeId, fromAddr: Address,
|
|||
d.sendNodes(fromId, fromAddr, reqId,
|
||||
d.routingTable.neighboursAtDistance(distance))
|
||||
|
||||
proc receive*(d: Protocol, a: Address, msg: openArray[byte]) {.gcsafe,
|
||||
proc receive*(d: Protocol, a: Address, packet: openArray[byte]) {.gcsafe,
|
||||
raises: [
|
||||
Defect,
|
||||
# TODO This is now coming from Chronos's callSoon
|
||||
|
@ -256,37 +260,37 @@ proc receive*(d: Protocol, a: Address, msg: openArray[byte]) {.gcsafe,
|
|||
IOError,
|
||||
TransportAddressError,
|
||||
].} =
|
||||
if msg.len < tagSize: # or magicSize, can be either
|
||||
return # Invalid msg
|
||||
if packet.len < tagSize: # or magicSize, can be either
|
||||
return # Invalid packet
|
||||
|
||||
# debug "Packet received: ", length = msg.len
|
||||
# debug "Packet received: ", length = packet.len
|
||||
|
||||
if d.isWhoAreYou(msg):
|
||||
if d.isWhoAreYou(packet):
|
||||
trace "Received whoareyou", localNode = $d.localNode, address = a
|
||||
let whoareyou = d.decodeWhoAreYou(msg)
|
||||
let whoareyou = d.decodeWhoAreYou(packet)
|
||||
var pr: PendingRequest
|
||||
if d.pendingRequests.take(whoareyou.authTag, pr):
|
||||
let toNode = pr.node
|
||||
whoareyou.pubKey = toNode.node.pubkey # TODO: Yeah, rather ugly this.
|
||||
try:
|
||||
let (data, _) = d.codec.encodeEncrypted(toNode.id, toNode.address,
|
||||
pr.packet, challenge = whoareyou)
|
||||
let (data, _) = d.codec.encodePacket(toNode.id, toNode.address,
|
||||
pr.message, challenge = whoareyou).tryGet()
|
||||
d.send(toNode, data)
|
||||
except RandomSourceDepleted as err:
|
||||
debug "Failed to respond to a who-you-are msg " &
|
||||
except RandomSourceDepleted:
|
||||
debug "Failed to respond to a who-you-are packet " &
|
||||
"due to randomness source depletion."
|
||||
|
||||
else:
|
||||
var tag: array[tagSize, byte]
|
||||
tag[0 .. ^1] = msg.toOpenArray(0, tagSize - 1)
|
||||
tag[0 .. ^1] = packet.toOpenArray(0, tagSize - 1)
|
||||
let senderData = tag xor d.idHash
|
||||
let sender = readUintBE[256](senderData)
|
||||
|
||||
var authTag: AuthTag
|
||||
var node: Node
|
||||
var packet: Packet
|
||||
let decoded = d.codec.decodeEncrypted(sender, a, msg, authTag, node, packet)
|
||||
if decoded == DecodeStatus.Success:
|
||||
let decoded = d.codec.decodePacket(sender, a, packet, authTag, node)
|
||||
if decoded.isOk:
|
||||
let message = decoded[]
|
||||
if not node.isNil:
|
||||
# Not filling table with nodes without correct IP in the ENR
|
||||
if a.ip == node.address.ip:
|
||||
|
@ -294,30 +298,32 @@ proc receive*(d: Protocol, a: Address, msg: openArray[byte]) {.gcsafe,
|
|||
localNode = $d.localNode
|
||||
discard d.routingTable.addNode(node)
|
||||
|
||||
case packet.kind
|
||||
case message.kind
|
||||
of ping:
|
||||
d.handlePing(sender, a, packet.ping, packet.reqId)
|
||||
d.handlePing(sender, a, message.ping, message.reqId)
|
||||
of findNode:
|
||||
d.handleFindNode(sender, a, packet.findNode, packet.reqId)
|
||||
d.handleFindNode(sender, a, message.findNode, message.reqId)
|
||||
else:
|
||||
var waiter: Future[Option[Packet]]
|
||||
if d.awaitedPackets.take((sender, packet.reqId), waiter):
|
||||
waiter.complete(packet.some)
|
||||
var waiter: Future[Option[Message]]
|
||||
if d.awaitedMessages.take((sender, message.reqId), waiter):
|
||||
waiter.complete(some(message))
|
||||
else:
|
||||
debug "TODO: handle packet: ", packet = packet.kind, origin = a
|
||||
elif decoded == DecodeStatus.DecryptError:
|
||||
trace "Timed out or unrequested message", message = message.kind,
|
||||
origin = a
|
||||
elif decoded.error == DecodeError.DecryptError:
|
||||
debug "Could not decrypt packet, respond with whoareyou",
|
||||
localNode = $d.localNode, address = a
|
||||
# only sendingWhoareyou in case it is a decryption failure
|
||||
d.sendWhoareyou(a, sender, authTag)
|
||||
elif decoded == DecodeStatus.PacketError:
|
||||
# Still adding the node in case there is a packet error (could be
|
||||
# unsupported packet)
|
||||
elif decoded.error == DecodeError.UnsupportedMessage:
|
||||
# Still adding the node in case failure is because of unsupported message.
|
||||
if not node.isNil:
|
||||
if a.ip == node.address.ip:
|
||||
debug "Adding new node to routing table", node = $node,
|
||||
localNode = $d.localNode
|
||||
discard d.routingTable.addNode(node)
|
||||
# elif decoded.error == DecodeError.PacketError:
|
||||
# Not adding this node as from our perspective it is sending rubbish.
|
||||
|
||||
proc processClient(transp: DatagramTransport,
|
||||
raddr: TransportAddress): Future[void] {.async, gcsafe.} =
|
||||
|
@ -356,32 +362,32 @@ proc validIp(sender, address: IpAddress): bool =
|
|||
# TODO: This could be improved to do the clean-up immediatily in case a non
|
||||
# whoareyou response does arrive, but we would need to store the AuthTag
|
||||
# somewhere
|
||||
proc registerRequest(d: Protocol, n: Node, packet: seq[byte], nonce: AuthTag) =
|
||||
let request = PendingRequest(node: n, packet: packet)
|
||||
proc registerRequest(d: Protocol, n: Node, message: seq[byte], nonce: AuthTag) =
|
||||
let request = PendingRequest(node: n, message: message)
|
||||
if not d.pendingRequests.hasKeyOrPut(nonce, request):
|
||||
sleepAsync(responseTimeout).addCallback() do(data: pointer):
|
||||
d.pendingRequests.del(nonce)
|
||||
|
||||
proc waitPacket(d: Protocol, fromNode: Node, reqId: RequestId): Future[Option[Packet]] =
|
||||
result = newFuture[Option[Packet]]("waitPacket")
|
||||
proc waitMessage(d: Protocol, fromNode: Node, reqId: RequestId): Future[Option[Message]] =
|
||||
result = newFuture[Option[Message]]("waitMessage")
|
||||
let res = result
|
||||
let key = (fromNode.id, reqId)
|
||||
sleepAsync(responseTimeout).addCallback() do(data: pointer):
|
||||
d.awaitedPackets.del(key)
|
||||
d.awaitedMessages.del(key)
|
||||
if not res.finished:
|
||||
res.complete(none(Packet))
|
||||
d.awaitedPackets[key] = result
|
||||
res.complete(none(Message))
|
||||
d.awaitedMessages[key] = result
|
||||
|
||||
proc addNodesFromENRs(result: var seq[Node], enrs: openarray[Record]) =
|
||||
for r in enrs: result.add(newNode(r))
|
||||
|
||||
proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): Future[seq[Node]] {.async.} =
|
||||
var op = await d.waitPacket(fromNode, reqId)
|
||||
var op = await d.waitMessage(fromNode, reqId)
|
||||
if op.isSome and op.get.kind == nodes:
|
||||
result.addNodesFromENRs(op.get.nodes.enrs)
|
||||
let total = op.get.nodes.total
|
||||
for i in 1 ..< total:
|
||||
op = await d.waitPacket(fromNode, reqId)
|
||||
op = await d.waitMessage(fromNode, reqId)
|
||||
if op.isSome and op.get.kind == nodes:
|
||||
result.addNodesFromENRs(op.get.nodes.enrs)
|
||||
else:
|
||||
|
@ -389,28 +395,28 @@ proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): Future[seq[Node]]
|
|||
|
||||
proc sendPing(d: Protocol, toNode: Node): RequestId =
|
||||
let
|
||||
reqId = newRequestId()
|
||||
ping = PingPacket(enrSeq: d.localNode.record.seqNum)
|
||||
packet = encodePacket(ping, reqId)
|
||||
(data, nonce) = d.codec.encodeEncrypted(toNode.id, toNode.address, packet,
|
||||
challenge = nil)
|
||||
d.registerRequest(toNode, packet, nonce)
|
||||
reqId = newRequestId().tryGet()
|
||||
ping = PingMessage(enrSeq: d.localNode.record.seqNum)
|
||||
message = encodeMessage(ping, reqId)
|
||||
(data, nonce) = d.codec.encodePacket(toNode.id, toNode.address, message,
|
||||
challenge = nil).tryGet()
|
||||
d.registerRequest(toNode, message, nonce)
|
||||
d.send(toNode, data)
|
||||
return reqId
|
||||
|
||||
proc ping*(d: Protocol, toNode: Node): Future[Option[PongPacket]] {.async.} =
|
||||
proc ping*(d: Protocol, toNode: Node): Future[Option[PongMessage]] {.async.} =
|
||||
let reqId = d.sendPing(toNode)
|
||||
let resp = await d.waitPacket(toNode, reqId)
|
||||
let resp = await d.waitMessage(toNode, reqId)
|
||||
|
||||
if resp.isSome() and resp.get().kind == pong:
|
||||
return some(resp.get().pong)
|
||||
|
||||
proc sendFindNode(d: Protocol, toNode: Node, distance: uint32): RequestId =
|
||||
let reqId = newRequestId()
|
||||
let packet = encodePacket(FindNodePacket(distance: distance), reqId)
|
||||
let (data, nonce) = d.codec.encodeEncrypted(toNode.id, toNode.address, packet,
|
||||
challenge = nil)
|
||||
d.registerRequest(toNode, packet, nonce)
|
||||
let reqId = newRequestId().tryGet()
|
||||
let message = encodeMessage(FindNodeMessage(distance: distance), reqId)
|
||||
let (data, nonce) = d.codec.encodePacket(toNode.id, toNode.address, message,
|
||||
challenge = nil).tryGet()
|
||||
d.registerRequest(toNode, message, nonce)
|
||||
|
||||
d.send(toNode, data)
|
||||
return reqId
|
||||
|
@ -579,7 +585,8 @@ proc newProtocol*(privKey: PrivateKey, db: Database,
|
|||
a = Address(ip: externalIp.get(IPv4_any()),
|
||||
tcpPort: tcpPort, udpPort: udpPort)
|
||||
enode = ENode(pubkey: privKey.toPublicKey().tryGet(), address: a)
|
||||
enrRec = enr.Record.init(1, privKey, externalIp, tcpPort, udpPort, localEnrFields)
|
||||
enrRec = enr.Record.init(1, privKey, externalIp, tcpPort, udpPort,
|
||||
localEnrFields).expect("Properly intialized private key")
|
||||
node = newNode(enode, enrRec)
|
||||
|
||||
result = Protocol(
|
||||
|
|
|
@ -3,6 +3,8 @@ import
|
|||
stint, chronicles,
|
||||
types, node
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
type
|
||||
RoutingTable* = object
|
||||
thisNode: Node
|
||||
|
@ -101,8 +103,6 @@ proc split(k: KBucket): tuple[lower, upper: KBucket] =
|
|||
proc inRange(k: KBucket, n: Node): bool {.inline.} =
|
||||
k.istart <= n.id and n.id <= k.iend
|
||||
|
||||
proc isFull(k: KBucket): bool = k.len == BUCKET_SIZE
|
||||
|
||||
proc contains(k: KBucket, n: Node): bool = n in k.nodes
|
||||
|
||||
proc binaryGetBucketForNode(buckets: openarray[KBucket],
|
||||
|
@ -116,8 +116,10 @@ proc binaryGetBucketForNode(buckets: openarray[KBucket],
|
|||
if bucket.istart <= id and id <= bucket.iend:
|
||||
result = bucket
|
||||
|
||||
# TODO: Is this really an error that should occur? Feels a lot like a work-
|
||||
# around to another problem. Set to Defect for now.
|
||||
if result.isNil:
|
||||
raise newException(ValueError, "No bucket found for node with id " & $id)
|
||||
raise (ref Defect)(msg: "No bucket found for node with id " & $id)
|
||||
|
||||
proc computeSharedPrefixBits(nodes: openarray[Node]): int =
|
||||
## Count the number of prefix bits shared by all nodes.
|
||||
|
@ -186,9 +188,6 @@ proc contains*(r: RoutingTable, n: Node): bool = n in r.bucketForNode(n.id)
|
|||
proc bucketsByDistanceTo(r: RoutingTable, id: NodeId): seq[KBucket] =
|
||||
sortedByIt(r.buckets, it.distanceTo(id))
|
||||
|
||||
proc notFullBuckets(r: RoutingTable): seq[KBucket] =
|
||||
r.buckets.filterIt(not it.isFull)
|
||||
|
||||
proc neighbours*(r: RoutingTable, id: NodeId, k: int = BUCKET_SIZE): seq[Node] =
|
||||
## Return up to k neighbours of the given node.
|
||||
result = newSeqOfCap[Node](k * 2)
|
||||
|
@ -241,7 +240,7 @@ proc setJustSeen*(r: RoutingTable, n: Node) =
|
|||
b.nodes[0] = n
|
||||
b.lastUpdated = epochTime()
|
||||
|
||||
proc nodeToRevalidate*(r: RoutingTable): Node {.raises:[].} =
|
||||
proc nodeToRevalidate*(r: RoutingTable): Node =
|
||||
var buckets = r.buckets
|
||||
shuffle(buckets)
|
||||
# TODO: Should we prioritize less-recently-updated buckets instead?
|
||||
|
|
|
@ -27,9 +27,9 @@ type
|
|||
|
||||
Database* = ref object of RootRef
|
||||
|
||||
PacketKind* = enum
|
||||
MessageKind* = enum
|
||||
# TODO This is needed only to make Nim 1.0.4 happy
|
||||
# Without it, the `PacketKind` type cannot be used as
|
||||
# Without it, the `MessageKind` type cannot be used as
|
||||
# a discriminator in case objects.
|
||||
unused = 0x00
|
||||
|
||||
|
@ -44,43 +44,43 @@ type
|
|||
|
||||
RequestId* = uint64
|
||||
|
||||
PingPacket* = object
|
||||
PingMessage* = object
|
||||
enrSeq*: uint64
|
||||
|
||||
PongPacket* = object
|
||||
PongMessage* = object
|
||||
enrSeq*: uint64
|
||||
ip*: seq[byte]
|
||||
port*: uint16
|
||||
|
||||
FindNodePacket* = object
|
||||
FindNodeMessage* = object
|
||||
distance*: uint32
|
||||
|
||||
NodesPacket* = object
|
||||
NodesMessage* = object
|
||||
total*: uint32
|
||||
enrs*: seq[Record]
|
||||
|
||||
SomePacket* = PingPacket or PongPacket or FindNodePacket or NodesPacket
|
||||
SomeMessage* = PingMessage or PongMessage or FindNodeMessage or NodesMessage
|
||||
|
||||
Packet* = object
|
||||
Message* = object
|
||||
reqId*: RequestId
|
||||
case kind*: PacketKind
|
||||
case kind*: MessageKind
|
||||
of ping:
|
||||
ping*: PingPacket
|
||||
ping*: PingMessage
|
||||
of pong:
|
||||
pong*: PongPacket
|
||||
pong*: PongMessage
|
||||
of findnode:
|
||||
findNode*: FindNodePacket
|
||||
findNode*: FindNodeMessage
|
||||
of nodes:
|
||||
nodes*: NodesPacket
|
||||
nodes*: NodesMessage
|
||||
else:
|
||||
# TODO: Define the rest
|
||||
discard
|
||||
|
||||
template packetKind*(T: typedesc[SomePacket]): PacketKind =
|
||||
when T is PingPacket: ping
|
||||
elif T is PongPacket: pong
|
||||
elif T is FindNodePacket: findNode
|
||||
elif T is NodesPacket: nodes
|
||||
template messageKind*(T: typedesc[SomeMessage]): MessageKind =
|
||||
when T is PingMessage: ping
|
||||
elif T is PongMessage: pong
|
||||
elif T is FindNodeMessage: findNode
|
||||
elif T is NodesMessage: nodes
|
||||
|
||||
method storeKeys*(db: Database, id: NodeId, address: Address, r, w: AesKey):
|
||||
bool {.base, raises: [Defect].} = discard
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import
|
||||
unittest, chronos, sequtils, chronicles, tables, stint,
|
||||
unittest, chronos, sequtils, chronicles, tables, stint, nimcrypto,
|
||||
eth/[keys, rlp], eth/p2p/enode, eth/trie/db,
|
||||
eth/p2p/discoveryv5/[discovery_db, enr, node, types, routing_table, encoding],
|
||||
eth/p2p/discoveryv5/protocol as discv5_protocol,
|
||||
|
@ -26,8 +26,8 @@ proc randomPacket(tag: PacketTag): seq[byte] =
|
|||
authTag: AuthTag
|
||||
msg: array[44, byte]
|
||||
|
||||
randomBytes2(authTag)
|
||||
randomBytes2(msg)
|
||||
require randomBytes(authTag) == authTag.len
|
||||
require randomBytes(msg) == msg.len
|
||||
result.add(tag)
|
||||
result.add(rlp.encode(authTag))
|
||||
result.add(msg)
|
||||
|
@ -35,7 +35,7 @@ proc randomPacket(tag: PacketTag): seq[byte] =
|
|||
proc generateNode(privKey = PrivateKey.random()[], port: int = 20302): Node =
|
||||
let port = Port(port)
|
||||
let enr = enr.Record.init(1, privKey, some(parseIpAddress("127.0.0.1")),
|
||||
port, port)
|
||||
port, port).expect("Properly intialized private key")
|
||||
result = newNode(enr)
|
||||
|
||||
proc nodeAtDistance(n: Node, d: uint32): Node =
|
||||
|
@ -98,7 +98,7 @@ suite "Discovery v5 Tests":
|
|||
let a = localAddress(20303)
|
||||
|
||||
for i in 0 ..< 5:
|
||||
randomBytes2(tag)
|
||||
require randomBytes(tag) == tag.len
|
||||
node.receive(a, randomPacket(tag))
|
||||
|
||||
# Checking different nodeIds but same address
|
||||
|
@ -344,7 +344,7 @@ suite "Discovery v5 Tests":
|
|||
# TODO: need to add some logic to update ENRs properly
|
||||
targetSeqNum.inc()
|
||||
let r = enr.Record.init(targetSeqNum, targetKey,
|
||||
some(targetAddress.ip), targetAddress.tcpPort, targetAddress.udpPort)
|
||||
some(targetAddress.ip), targetAddress.tcpPort, targetAddress.udpPort)[]
|
||||
targetNode.localNode.record = r
|
||||
targetNode.open()
|
||||
let n = await mainNode.resolve(targetId)
|
||||
|
@ -358,7 +358,7 @@ suite "Discovery v5 Tests":
|
|||
block:
|
||||
targetSeqNum.inc()
|
||||
let r = enr.Record.init(3, targetKey, some(targetAddress.ip),
|
||||
targetAddress.tcpPort, targetAddress.udpPort)
|
||||
targetAddress.tcpPort, targetAddress.udpPort)[]
|
||||
targetNode.localNode.record = r
|
||||
let pong = await targetNode.ping(lookupNode.localNode)
|
||||
require pong.isSome()
|
||||
|
|
|
@ -86,33 +86,33 @@ suite "Discovery v5 Packet Encodings":
|
|||
|
||||
suite "Discovery v5 Protocol Message Encodings":
|
||||
test "Ping Request":
|
||||
var p: PingPacket
|
||||
var p: PingMessage
|
||||
p.enrSeq = 1
|
||||
var reqId: RequestId = 1
|
||||
check encodePacket(p, reqId).toHex == "01c20101"
|
||||
check encodeMessage(p, reqId).toHex == "01c20101"
|
||||
|
||||
test "Pong Response":
|
||||
var p: PongPacket
|
||||
var p: PongMessage
|
||||
p.enrSeq = 1
|
||||
p.port = 5000
|
||||
p.ip = @[127.byte, 0, 0, 1]
|
||||
var reqId: RequestId = 1
|
||||
check encodePacket(p, reqId).toHex == "02ca0101847f000001821388"
|
||||
check encodeMessage(p, reqId).toHex == "02ca0101847f000001821388"
|
||||
|
||||
test "FindNode Request":
|
||||
var p: FindNodePacket
|
||||
var p: FindNodeMessage
|
||||
p.distance = 0x0100
|
||||
var reqId: RequestId = 1
|
||||
check encodePacket(p, reqId).toHex == "03c401820100"
|
||||
check encodeMessage(p, reqId).toHex == "03c401820100"
|
||||
|
||||
test "Nodes Response (empty)":
|
||||
var p: NodesPacket
|
||||
var p: NodesMessage
|
||||
p.total = 0x1
|
||||
var reqId: RequestId = 1
|
||||
check encodePacket(p, reqId).toHex == "04c30101c0"
|
||||
check encodeMessage(p, reqId).toHex == "04c30101c0"
|
||||
|
||||
test "Nodes Response (multiple)":
|
||||
var p: NodesPacket
|
||||
var p: NodesMessage
|
||||
p.total = 0x1
|
||||
var e1, e2: Record
|
||||
check e1.fromURI("enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg")
|
||||
|
@ -120,7 +120,7 @@ suite "Discovery v5 Protocol Message Encodings":
|
|||
|
||||
p.enrs = @[e1, e2]
|
||||
var reqId: RequestId = 1
|
||||
check encodePacket(p, reqId).toHex == "04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235"
|
||||
check encodeMessage(p, reqId).toHex == "04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235"
|
||||
|
||||
suite "Discovery v5 Cryptographic Primitives":
|
||||
test "ECDH":
|
||||
|
@ -166,10 +166,11 @@ suite "Discovery v5 Cryptographic Primitives":
|
|||
idNonceSig = "0xc5036e702a79902ad8aa147dabfe3958b523fd6fa36cc78e2889b912d682d8d35fdea142e141f690736d86f50b39746ba2d2fc510b46f82ee08f08fd55d133a4"
|
||||
|
||||
let
|
||||
c = Codec(privKey: PrivateKey.fromHex(localSecretKey)[])
|
||||
signature = signIDNonce(c, hexToByteArray[idNonceSize](idNonce),
|
||||
privKey = PrivateKey.fromHex(localSecretKey)[]
|
||||
signature = signIDNonce(privKey, hexToByteArray[idNonceSize](idNonce),
|
||||
hexToByteArray[64](ephemeralKey))
|
||||
check signature.toRaw() == hexToByteArray[64](idNonceSig)
|
||||
require signature.isOK()
|
||||
check signature[].toRaw() == hexToByteArray[64](idNonceSig)
|
||||
|
||||
test "Encryption/Decryption":
|
||||
const
|
||||
|
|
|
@ -7,7 +7,7 @@ suite "ENR":
|
|||
test "Serialization":
|
||||
var pk = PrivateKey.fromHex(
|
||||
"5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")[]
|
||||
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})
|
||||
var r = initRecord(123, pk, {"udp": 1234'u, "ip": [byte 5, 6, 7, 8]})[]
|
||||
doAssert($r == """(id: "v4", ip: 0x05060708, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
|
||||
let uri = r.toURI()
|
||||
var r2: Record
|
||||
|
@ -35,7 +35,7 @@ suite "ENR":
|
|||
let
|
||||
keys = KeyPair.random()[]
|
||||
ip = parseIpAddress("10.20.30.40")
|
||||
enr = Record.init(100, keys.seckey, some(ip), Port(9000), Port(9000), @[])
|
||||
enr = Record.init(100, keys.seckey, some(ip), Port(9000), Port(9000), @[])[]
|
||||
typedEnr = get enr.toTypedRecord()
|
||||
|
||||
check:
|
||||
|
@ -54,7 +54,7 @@ suite "ENR":
|
|||
test "ENR without address":
|
||||
let
|
||||
keys = KeyPair.random()[]
|
||||
enr = Record.init(100, keys.seckey, none(IpAddress), Port(9000), Port(9000))
|
||||
enr = Record.init(100, keys.seckey, none(IpAddress), Port(9000), Port(9000))[]
|
||||
typedEnr = get enr.toTypedRecord()
|
||||
|
||||
check:
|
||||
|
|
Loading…
Reference in New Issue