mirror of
https://github.com/waku-org/nwaku.git
synced 2025-01-15 17:35:45 +00:00
230 lines
6.3 KiB
Nim
230 lines
6.3 KiB
Nim
{.push raises: [Defect]}
|
|
|
|
import
|
|
std/[strformat, strscans, strutils, sequtils],
|
|
stew/[base32, base64, byteutils, results],
|
|
eth/keys,
|
|
eth/p2p/discoveryv5/enr,
|
|
nimcrypto/[hash, keccak]
|
|
|
|
export keys, enr
|
|
|
|
## A collection of utilities for interacting with a list of ENR
|
|
## encoded as a Merkle Tree consisting of DNS TXT records.
|
|
##
|
|
## Discovery via DNS is based on https://eips.ethereum.org/EIPS/eip-1459
|
|
##
|
|
## This implementation is based on the Go implementation of EIP-1459
|
|
## at https://github.com/ethereum/go-ethereum/blob/master/p2p/dnsdisc
|
|
|
|
const
|
|
RootPrefix* = "enrtree-root:v1"
|
|
BranchPrefix* = "enrtree-branch:"
|
|
EnrPrefix* = "enr:"
|
|
LinkPrefix* = "enrtree://"
|
|
|
|
type
|
|
Tree* = object
|
|
## A tree consists of a root entry
|
|
## and a seq of subtree entries
|
|
rootEntry*: RootEntry
|
|
entries*: seq[SubtreeEntry]
|
|
|
|
EntryParseResult*[T] = Result[T, string]
|
|
|
|
# Entry types
|
|
|
|
RootEntry* = object
|
|
eroot*: string # Root of subtree containing node records
|
|
lroot*: string # Root of subtree containing links to other trees
|
|
seqNo*: uint32 # Sequence number, increased with every update
|
|
signature*: seq[byte] # Root entry signature
|
|
|
|
BranchEntry* = object
|
|
children*: seq[string] # Hashes pointing to the subdomains of other subtree entries
|
|
|
|
EnrEntry* = object
|
|
record*: enr.Record # Ethereum node record as per EIP-778
|
|
|
|
LinkEntry* = object
|
|
str*: string # String representation of subdomain, i.e. <key>@<domain>
|
|
pubKey*: PublicKey # Public key that signed the list at this link
|
|
domain*: string
|
|
|
|
SubtreeEntryKind* = enum
|
|
Branch
|
|
Enr
|
|
Link
|
|
|
|
SubtreeEntry* = object
|
|
case kind*: SubtreeEntryKind
|
|
of Branch:
|
|
branchEntry*: BranchEntry
|
|
of Enr:
|
|
enrEntry*: EnrEntry
|
|
of Link:
|
|
linkEntry*: LinkEntry
|
|
|
|
##################
|
|
# Util functions #
|
|
##################
|
|
|
|
proc isValidHash(hashStr: string): bool =
|
|
## Checks if a hash is valid. It should be the base32
|
|
## encoding of an abbreviated keccak256 hash.
|
|
let decodedLen = Base32.decodedLength(hashStr.len())
|
|
|
|
if (decodedLen > 32) or (hashStr.contains("\n\r")):
|
|
# @TODO: also check minimum hash size
|
|
return false
|
|
|
|
try:
|
|
discard Base32.decode(hashStr)
|
|
except Base32Error:
|
|
return false
|
|
|
|
return true
|
|
|
|
proc hashableContent*(rootEntry: RootEntry): seq[byte] {.raises: [Defect, ValueError].} =
|
|
# Returns the hashable content of a root entry, used to compute the `sig=` portion
|
|
return fmt"{RootPrefix} e={rootEntry.eroot} l={rootEntry.lroot} seq={rootEntry.seqNo}".toBytes()
|
|
|
|
#################
|
|
# Entry parsers #
|
|
#################
|
|
|
|
proc parseRootEntry*(entry: string): EntryParseResult[RootEntry] =
|
|
## Parses a root entry in the format
|
|
## 'enrtree-root:v1 e=<enr-root> l=<link-root> seq=<sequence-number> sig=<signature>'
|
|
|
|
var
|
|
eroot, lroot, sigstr: string
|
|
seqNo: int
|
|
signature: seq[byte]
|
|
|
|
try:
|
|
if not scanf(entry, RootPrefix & " e=$+ l=$+ seq=$i sig=$+", eroot, lroot, seqNo, sigstr):
|
|
# @TODO better error handling
|
|
return err("Invalid syntax")
|
|
except ValueError:
|
|
return err("Invalid syntax")
|
|
|
|
if (not isValidHash(eroot)) or (not isValidHash(lroot)):
|
|
return err("Invalid child")
|
|
|
|
try:
|
|
signature = Base64Url.decode(sigstr)
|
|
except Base64Error:
|
|
return err("Invalid signature")
|
|
|
|
if signature.len() != SkRawPublicKeySize:
|
|
return err("Invalid signature")
|
|
|
|
ok(RootEntry(eroot: eroot, lroot: lroot, seqNo: uint32(seqNo), signature: signature))
|
|
|
|
proc parseBranchEntry*(entry: string): EntryParseResult[BranchEntry] =
|
|
## Parses a branch entry in the format
|
|
## 'enrtree-branch:<h₁>,<h₂>,...,<hₙ>'
|
|
|
|
var
|
|
hashesSubstr: string
|
|
hashes: seq[string]
|
|
|
|
try:
|
|
if not scanf(entry, BranchPrefix & "$+", hashesSubstr):
|
|
# @TODO better error handling
|
|
return err("Invalid syntax")
|
|
except ValueError:
|
|
return err("Invalid syntax")
|
|
|
|
for hash in hashesSubstr.split(','):
|
|
if (not isValidHash(hash)):
|
|
return err("Invalid child")
|
|
|
|
hashes.add(hash)
|
|
|
|
ok(BranchEntry(children: hashes))
|
|
|
|
proc parseEnrEntry*(entry: string): EntryParseResult[EnrEntry] =
|
|
## Parses an enr entry in the format 'enr:<node-record>'.
|
|
## <node-record> is the EIP-1459 text encoding of the node record
|
|
|
|
var
|
|
nodeStr: string
|
|
record: Record
|
|
|
|
try:
|
|
if not scanf(entry, EnrPrefix & "$+", nodeStr):
|
|
# @TODO better error handling
|
|
return err("Invalid syntax")
|
|
except ValueError:
|
|
return err("Invalid syntax")
|
|
|
|
if (not record.fromBase64(nodeStr)):
|
|
return err("Invalid signature")
|
|
|
|
ok(EnrEntry(record: record))
|
|
|
|
proc parseLinkEntry*(entry: string): EntryParseResult[LinkEntry] =
|
|
## Parses a link entry in the format
|
|
## 'enrtree://<key>@<fqdn>'
|
|
|
|
var
|
|
keyStr, fqdnStr: string
|
|
rawKey: seq[byte]
|
|
key: PublicKey
|
|
|
|
try:
|
|
if not scanf(entry, LinkPrefix & "$+@$+", keyStr, fqdnStr):
|
|
# @TODO better error handling
|
|
return err("Invalid syntax")
|
|
except ValueError:
|
|
return err("Invalid syntax")
|
|
|
|
try:
|
|
rawKey = Base32.decode(keyStr)
|
|
except Base32Error:
|
|
return err("Invalid public key")
|
|
|
|
try:
|
|
key = PublicKey.fromRaw(rawKey).tryGet()
|
|
except CatchableError as e:
|
|
return err("Invalid public key: " & e.msg)
|
|
|
|
ok(LinkEntry(str: keyStr & "@" & fqdnStr,
|
|
domain: fqdnStr,
|
|
pubKey: key))
|
|
|
|
proc parseSubtreeEntry*(entry: string): EntryParseResult[SubtreeEntry] =
|
|
var subtreeEntry: SubtreeEntry
|
|
|
|
try:
|
|
if entry.startsWith(BranchPrefix):
|
|
subtreeEntry = SubtreeEntry(kind: Branch, branchEntry: parseBranchEntry(entry).tryGet())
|
|
elif entry.startsWith(EnrPrefix):
|
|
subtreeEntry = SubtreeEntry(kind: Enr, enrEntry: parseEnrEntry(entry).tryGet())
|
|
elif entry.startsWith(LinkPrefix):
|
|
subtreeEntry = SubtreeEntry(kind: Link, linkEntry: parseLinkEntry(entry).tryGet())
|
|
else:
|
|
return err("Unexpected subtree entry type")
|
|
except ValueError as e:
|
|
return err("Invalid syntax: " & e.msg)
|
|
|
|
ok(subtreeEntry)
|
|
|
|
##################
|
|
# Tree accessors #
|
|
##################
|
|
|
|
proc getNodes*(tree: Tree): seq[EnrEntry] {.raises: [Defect, ValueError]} =
|
|
## Returns a list of node entries in the tree
|
|
|
|
return tree.entries.filterIt(it.kind == Enr)
|
|
.mapIt(it.enrEntry)
|
|
|
|
proc getLinks*(tree: Tree): seq[LinkEntry] {.raises: [Defect, ValueError]} =
|
|
## Returns a list of link entries in the tree
|
|
|
|
return tree.entries.filterIt(it.kind == Link)
|
|
.mapIt(it.linkEntry)
|