nim-eth/eth/p2p/discoveryv5/enr.nim

357 lines
9.3 KiB
Nim
Raw Normal View History

# ENR implemetation according to spec:
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-778.md
import
2020-06-05 14:10:15 +00:00
strutils, macros, algorithm, options,
stew/shims/net,
nimcrypto, stew/base64,
eth/[rlp, keys]
2019-12-10 18:34:57 +00:00
export options
{.push raises: [Defect].}
const
maxEnrSize = 300
minRlpListLen = 4 # for signature, seqId, "id" key, id
2019-12-10 18:34:57 +00:00
type
FieldPair* = (string, Field)
2019-12-10 18:34:57 +00:00
Record* = object
seqNum*: uint64
2019-12-10 18:34:57 +00:00
# signature: seq[byte]
raw*: seq[byte] # RLP encoded record
pairs: seq[FieldPair] # sorted list of all key/value pairs
2019-12-10 18:34:57 +00:00
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]
2019-12-10 18:34:57 +00:00
FieldKind = enum
kString,
kNum,
kBytes
Field = object
case kind: FieldKind
of kString:
str: string
of kNum:
num: BiggestUInt
2019-12-10 18:34:57 +00:00
of kBytes:
bytes: seq[byte]
EnrResult*[T] = Result[T, cstring]
2019-12-10 18:34:57 +00:00
template toField[T](v: T): Field =
when T is string:
Field(kind: kString, str: v)
elif T is array:
Field(kind: kBytes, bytes: @v)
2019-12-10 18:34:57 +00:00
elif T is seq[byte]:
Field(kind: kBytes, bytes: v)
elif T is SomeUnsignedInt:
Field(kind: kNum, num: BiggestUInt(v))
2019-12-10 18:34:57 +00:00
else:
{.error: "Unsupported field type".}
proc makeEnrAux(seqNum: uint64, pk: PrivateKey,
pairs: openarray[(string, Field)]): EnrResult[Record] =
var record: Record
record.pairs = @pairs
record.seqNum = seqNum
2019-12-10 18:34:57 +00:00
let pubkey = ? pk.toPublicKey()
2019-12-10 18:34:57 +00:00
record.pairs.add(("id", Field(kind: kString, str: "v4")))
record.pairs.add(("secp256k1",
Field(kind: kBytes, bytes: @(pubkey.toRawCompressed()))))
2019-12-10 18:34:57 +00:00
# Sort by key
record.pairs.sort() do(a, b: (string, Field)) -> int:
2019-12-10 18:34:57 +00:00
cmp(a[0], b[0])
proc append(w: var RlpWriter, seqNum: uint64,
2020-05-01 20:34:26 +00:00
pairs: openarray[(string, Field)]): seq[byte] =
w.append(seqNum)
2019-12-10 18:34:57 +00:00
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)
w.finish()
let toSign = block:
var w = initRlpList(record.pairs.len * 2 + 1)
w.append(seqNum, record.pairs)
let sig = ? signNR(pk, toSign)
2019-12-10 18:34:57 +00:00
record.raw = block:
var w = initRlpList(record.pairs.len * 2 + 2)
w.append(sig.toRaw())
w.append(seqNum, record.pairs)
2019-12-10 18:34:57 +00:00
ok(record)
2019-12-10 18:34:57 +00:00
macro initRecord*(seqNum: uint64, pk: PrivateKey,
pairs: untyped{nkTableConstr}): untyped =
2019-12-10 18:34:57 +00:00
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 init*(T: type Record, seqNum: uint64,
pk: PrivateKey,
2020-06-05 14:10:15 +00:00
ip: Option[ValidIpAddress],
tcpPort, udpPort: Port,
extraFields: openarray[FieldPair] = []):
EnrResult[T] =
var fields = newSeq[FieldPair]()
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))
fields.add(((if isV6: "tcp6" else: "tcp"), tcpPort.uint16.toField))
fields.add(((if isV6: "udp6" else: "udp"), udpPort.uint16.toField))
else:
fields.add(("tcp", tcpPort.uint16.toField))
fields.add(("udp", udpPort.uint16.toField))
fields.add extraFields
makeEnrAux(seqNum, pk, fields)
2019-12-10 18:34:57 +00:00
2020-05-01 20:34:26 +00:00
proc getField(r: Record, name: string, field: var Field): bool =
2019-12-10 18:34:57 +00:00
# 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) {.raises: [ValueError].} =
2019-12-10 18:34:57 +00:00
if f.kind != kind:
raise newException(ValueError, "Wrong field kind")
2020-05-01 20:34:26 +00:00
proc get*(r: Record, key: string, T: type): T {.raises: [ValueError, Defect].} =
2019-12-10 18:34:57 +00:00
var f: Field
if r.getField(key, f):
when T is SomeInteger:
2019-12-10 18:34:57 +00:00
requireKind(f, kNum)
return T(f.num)
elif T is seq[byte]:
2019-12-10 18:34:57 +00:00
requireKind(f, kBytes)
return f.bytes
elif T is string:
2019-12-10 18:34:57 +00:00
requireKind(f, kString)
return f.str
elif T is PublicKey:
requireKind(f, kBytes)
let pk = PublicKey.fromRaw(f.bytes)
if pk.isErr:
raise newException(ValueError, "Invalid public key")
return pk[]
elif T is array:
when type(result[0]) is byte:
requireKind(f, kBytes)
if f.bytes.len != result.len:
raise newException(ValueError, "Invalid byte blob length")
copyMem(addr result[0], addr f.bytes[0], result.len)
else:
{.fatal: "Unsupported output type in enr.get".}
else:
{.fatal: "Unsupported output type in enr.get".}
else:
raise newException(KeyError, "Key not found in ENR: " & key)
2019-12-10 18:34:57 +00:00
2020-05-01 20:34:26 +00:00
proc get*(r: Record, T: type PublicKey): Option[T] =
2019-12-10 18:34:57 +00:00
var pubkeyField: Field
if r.getField("secp256k1", pubkeyField) and pubkeyField.kind == kBytes:
let pk = PublicKey.fromRaw(pubkeyField.bytes)
if pk.isOk:
return some pk[]
2019-12-10 18:34:57 +00:00
2020-05-01 20:34:26 +00:00
proc tryGet*(r: Record, key: string, T: type): Option[T] =
try:
return some get(r, key, T)
except ValueError:
discard
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 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:
2019-12-10 18:34:57 +00:00
var h = keccak256.digest(content)
return verify(sig[], h, publicKey.get)
2019-12-10 18:34:57 +00:00
proc verifySignature(r: Record): bool {.raises: [RlpError, Defect].} =
var rlp = rlpFromBytes(r.raw)
2019-12-10 18:34:57 +00:00
let sz = rlp.listLen
2020-02-27 18:09:05 +00:00
if not rlp.enterList:
return false
let sigData = rlp.read(seq[byte])
2019-12-10 18:34:57 +00:00
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)
2020-02-27 18:09:05 +00:00
if not rlp.isList:
return false
2019-12-10 18:34:57 +00:00
let sz = rlp.listLen
if sz < minRlpListLen or sz mod 2 != 0:
2019-12-10 18:34:57 +00:00
# Wrong rlp object
return false
2020-02-27 18:09:05 +00:00
# We already know we are working with a list
doAssert rlp.enterList()
2019-12-10 18:34:57 +00:00
rlp.skipElem() # Skip signature
r.seqNum = rlp.read(uint64)
2019-12-10 18:34:57 +00:00
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)
2019-12-10 18:34:57 +00:00
r.pairs.add((k, Field(kind: kString, str: id)))
of "secp256k1":
let pubkeyData = rlp.read(seq[byte])
2019-12-10 18:34:57 +00:00
r.pairs.add((k, Field(kind: kBytes, bytes: pubkeyData)))
of "tcp", "udp", "tcp6", "udp6":
let v = rlp.read(uint16)
2019-12-10 18:34:57 +00:00
r.pairs.add((k, Field(kind: kNum, num: v)))
else:
r.pairs.add((k, Field(kind: kBytes, bytes: rlp.read(seq[byte]))))
2019-12-10 18:34:57 +00:00
verifySignature(r)
proc fromBytes*(r: var Record, s: openarray[byte]): bool =
## Loads ENR from rlp-encoded bytes, and validated the signature.
2019-12-10 18:34:57 +00:00
r.raw = @s
try:
result = fromBytesAux(r)
except RlpError:
2019-12-10 18:34:57 +00:00
discard
proc fromBase64*(r: var Record, s: string): bool =
## Loads ENR from base64-encoded rlp-encoded bytes, and validated the
## signature.
2019-12-10 18:34:57 +00:00
try:
r.raw = Base64Url.decode(s)
result = fromBytesAux(r)
except RlpError, Base64Error:
2019-12-10 18:34:57 +00:00
discard
proc fromURI*(r: var Record, s: string): bool =
## Loads ENR from its text encoding: base64-encoded rlp-encoded bytes,
## prefixed with "enr:".
2019-12-10 18:34:57 +00:00
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))
2020-05-01 20:34:26 +00:00
proc toBase64*(r: Record): string =
2019-12-10 18:34:57 +00:00
result = Base64Url.encode(r.raw)
2020-05-01 20:34:26 +00:00
proc toURI*(r: Record): string = "enr:" & r.toBase64
2019-12-10 18:34:57 +00:00
2020-05-01 20:34:26 +00:00
proc `$`(f: Field): string =
2019-12-10 18:34:57 +00:00
case f.kind
of kNum:
$f.num
of kBytes:
"0x" & f.bytes.toHex
of kString:
"\"" & f.str & "\""
2020-05-01 20:34:26 +00:00
proc `$`*(r: Record): string =
2019-12-10 18:34:57 +00:00
result = "("
var first = true
for (k, v) in r.pairs:
if first:
first = false
else:
result &= ", "
result &= k
result &= ": "
result &= $v
result &= ')'
2019-12-16 19:38:45 +00:00
proc `==`*(a, b: Record): bool = a.raw == b.raw
proc read*(rlp: var Rlp, T: typedesc[Record]):
T {.inline, raises:[RlpError, ValueError, Defect].} =
if 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.
2019-12-16 19:38:45 +00:00
raise newException(ValueError, "Could not deserialize")
rlp.skipElem()
2020-05-01 20:34:26 +00:00
proc append*(rlpWriter: var RlpWriter, value: Record) =
rlpWriter.appendRawBytes(value.raw)