mirror of https://github.com/status-im/nim-eth.git
ENR parsing and serialization
This commit is contained in:
parent
091239a710
commit
992aeecd29
|
@ -57,6 +57,7 @@ proc runP2pTests() =
|
|||
"test_waku_mail",
|
||||
"test_waku_mode",
|
||||
"test_protocol_handlers",
|
||||
"test_enr",
|
||||
]:
|
||||
runTest("tests/p2p/" & filename)
|
||||
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
import strutils, macros, algorithm
|
||||
import eth/[rlp, keys], nimcrypto, stew/base64
|
||||
|
||||
type
|
||||
Record* = object
|
||||
sequenceNumber*: uint64
|
||||
# signature: seq[byte]
|
||||
raw*: seq[byte] # RLP encoded record
|
||||
pairs: seq[(string, Field)] # sorted list of all key/value pairs
|
||||
|
||||
FieldKind = enum
|
||||
kString,
|
||||
kNum,
|
||||
kBytes
|
||||
|
||||
Field = object
|
||||
case kind: FieldKind
|
||||
of kString:
|
||||
str: string
|
||||
of kNum:
|
||||
num: int
|
||||
of kBytes:
|
||||
bytes: seq[byte]
|
||||
|
||||
template toField[T](v: T): Field =
|
||||
when T is string:
|
||||
Field(kind: kString, str: v)
|
||||
elif T is seq[byte]:
|
||||
Field(kind: kBytes, bytes: v)
|
||||
elif T is SomeInteger:
|
||||
Field(kind: kNum, num: v.int)
|
||||
else:
|
||||
{.error: "Unsupported field type".}
|
||||
|
||||
proc makeEnrAux(sequenceNumber: uint64, pk: PrivateKey, pairs: openarray[(string, Field)]): Record =
|
||||
result.pairs = @pairs
|
||||
result.sequenceNumber = sequenceNumber
|
||||
|
||||
let pubkey = pk.getPublicKey()
|
||||
|
||||
result.pairs.add(("id", Field(kind: kString, str: "v4")))
|
||||
result.pairs.add(("secp256k1", Field(kind: kBytes, bytes: @(pubkey.getRawCompressed()))))
|
||||
|
||||
# Sort by key
|
||||
result.pairs.sort() do(a, b: (string, Field)) -> int:
|
||||
cmp(a[0], b[0])
|
||||
|
||||
proc append(w: var RlpWriter, sequenceNumber: uint64, pairs: openarray[(string, Field)]): seq[byte] =
|
||||
w.append(sequenceNumber)
|
||||
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(result.pairs.len * 2 + 1)
|
||||
w.append(sequenceNumber, result.pairs)
|
||||
|
||||
var sig: SignatureNR
|
||||
if signRawMessage(keccak256.digest(toSign).data, pk, sig) != EthKeysStatus.Success:
|
||||
raise newException(Exception, "Could not sign ENR (internal error)")
|
||||
|
||||
result.raw = block:
|
||||
var w = initRlpList(result.pairs.len * 2 + 2)
|
||||
w.append(sig.getRaw())
|
||||
w.append(sequenceNumber, result.pairs)
|
||||
|
||||
macro initRecord*(sequenceNumber: uint64, pk: PrivateKey, pairs: untyped{nkTableConstr}): untyped =
|
||||
for c in pairs:
|
||||
c.expectKind(nnkExprColonExpr)
|
||||
c[1] = newCall(bindSym"toField", c[1])
|
||||
|
||||
result = quote do:
|
||||
makeEnrAux(`sequenceNumber`, `pk`, `pairs`)
|
||||
|
||||
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) =
|
||||
if f.kind != kind:
|
||||
raise newException(ValueError, "Wrong field kind")
|
||||
|
||||
proc get*[T: seq[byte] | string | SomeInteger](r: Record, key: string, typ: typedesc[T]): typ =
|
||||
var f: Field
|
||||
if r.getField(key, f):
|
||||
when typ is SomeInteger:
|
||||
requireKind(f, kNum)
|
||||
return f.num
|
||||
elif typ is seq[byte]:
|
||||
requireKind(f, kBytes)
|
||||
return f.bytes
|
||||
elif typ is string:
|
||||
requireKind(f, kString)
|
||||
return f.str
|
||||
|
||||
proc get*(r: Record, pubKey: var PublicKey): bool =
|
||||
var pubkeyField: Field
|
||||
if r.getField("secp256k1", pubkeyField) and pubkeyField.kind == kBytes:
|
||||
result = recoverPublicKey(pubkeyField.bytes, pubKey) == EthKeysStatus.Success
|
||||
|
||||
proc verifySignatureV4(r: Record, sigData: openarray[byte], content: seq[byte]): bool =
|
||||
var publicKey: PublicKey
|
||||
if r.get(publicKey):
|
||||
var sig: SignatureNR
|
||||
if sig.parseCompact(sigData) == EthKeysStatus.Success:
|
||||
var h = keccak256.digest(content)
|
||||
if verifySignatureRaw(sig, h.data, publicKey) == EthKeysStatus.Success:
|
||||
return true
|
||||
|
||||
proc verifySignature(r: Record): bool =
|
||||
var rlp = rlpFromBytes(r.raw.toRange)
|
||||
let sz = rlp.listLen
|
||||
rlp.enterList()
|
||||
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 =
|
||||
var rlp = rlpFromBytes(r.raw.toRange)
|
||||
let sz = rlp.listLen
|
||||
if sz < 5 or sz mod 2 != 0:
|
||||
# Wrong rlp object
|
||||
return false
|
||||
|
||||
rlp.enterList()
|
||||
rlp.skipElem() # Skip signature
|
||||
|
||||
r.sequenceNumber = rlp.read(uint64)
|
||||
|
||||
let numPairs = (sz - 2) div 2
|
||||
var
|
||||
id: string
|
||||
pubkeyData: seq[byte]
|
||||
|
||||
for i in 0 ..< numPairs:
|
||||
let k = rlp.read(string)
|
||||
case k
|
||||
of "id":
|
||||
id = rlp.read(string)
|
||||
r.pairs.add((k, Field(kind: kString, str: id)))
|
||||
of "secp256k1":
|
||||
pubkeyData = rlp.read(seq[byte])
|
||||
r.pairs.add((k, Field(kind: kBytes, bytes: pubkeyData)))
|
||||
of "tcp", "udp", "tcp6", "udp6", "ip":
|
||||
let v = rlp.read(int)
|
||||
r.pairs.add((k, Field(kind: kNum, num: v)))
|
||||
else:
|
||||
r.pairs.add((k, Field(kind: kBytes, bytes: rlp.rawData.toSeq)))
|
||||
rlp.skipElem
|
||||
|
||||
verifySignature(r)
|
||||
|
||||
proc fromBytes*(r: var Record, s: openarray[byte]): bool =
|
||||
# Loads ENR from rlp-encoded bytes, and validated the signature.
|
||||
r.raw = @s
|
||||
try:
|
||||
result = fromBytesAux(r)
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
proc fromBase64*(r: var Record, s: string): bool =
|
||||
# Loads ENR from base64-encoded rlp-encoded bytes, and validated the signature.
|
||||
try:
|
||||
r.raw = Base64Url.decode(s)
|
||||
result = fromBytesAux(r)
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
proc fromURI*(r: var Record, s: string): bool =
|
||||
# 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])
|
||||
|
||||
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 & "\""
|
||||
|
||||
proc `$`*(r: Record): string =
|
||||
result = "("
|
||||
var first = true
|
||||
for (k, v) in r.pairs:
|
||||
if first:
|
||||
first = false
|
||||
else:
|
||||
result &= ", "
|
||||
result &= k
|
||||
result &= ": "
|
||||
result &= $v
|
||||
result &= ')'
|
|
@ -0,0 +1,29 @@
|
|||
import unittest
|
||||
import eth/p2p/discoveryv5/enr, eth/keys
|
||||
|
||||
suite "ENR":
|
||||
test "Serialization":
|
||||
var pk = initPrivateKey("5d2908f3f09ea1ff2e327c3f623159639b00af406e9009de5fd4b910fc34049d")
|
||||
var r = initRecord(123, pk, {"udp": 1234, "ip": 12345})
|
||||
doAssert($r == """(id: "v4", ip: 12345, secp256k1: 0x02E51EFA66628CE09F689BC2B82F165A75A9DDECBB6A804BE15AC3FDF41F3B34E7, udp: 1234)""")
|
||||
let uri = r.toURI()
|
||||
var r2: Record
|
||||
let sigValid = r2.fromURI(uri)
|
||||
doAssert(sigValid)
|
||||
doAssert($r2 == $r)
|
||||
|
||||
test "Parsing":
|
||||
var r: Record
|
||||
let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
|
||||
doAssert(sigValid)
|
||||
doAssert($r == """(id: "v4", ip: 2130706433, secp256k1: 0x03CA634CAE0D49ACB401D8A4C6B6FE8C55B70D115BF400769CC1400F3258CD3138, udp: 30303)""")
|
||||
|
||||
test "Bad base64":
|
||||
var r: Record
|
||||
let sigValid = r.fromURI("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnhMHcBFZntXNFrdv*jX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
|
||||
doAssert(not sigValid)
|
||||
|
||||
test "Bad rlp":
|
||||
var r: Record
|
||||
let sigValid = r.fromBase64("-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOOnrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8")
|
||||
doAssert(not sigValid)
|
Loading…
Reference in New Issue