mirror of
https://github.com/waku-org/nwaku.git
synced 2025-01-28 15:46:33 +00:00
240 lines
7.3 KiB
Nim
240 lines
7.3 KiB
Nim
{.push raises: [Defect]}
|
|
|
|
import
|
|
std/[sequtils, sets, strformat],
|
|
chronicles,
|
|
chronos,
|
|
eth/keys,
|
|
nimcrypto/[hash, keccak],
|
|
stew/[base32, byteutils, results],
|
|
./tree
|
|
|
|
export
|
|
tree
|
|
|
|
## Implementation of DNS-based discovery client protocol, as specified
|
|
## in https://eips.ethereum.org/EIPS/eip-1459
|
|
##
|
|
## This implementation is loosely based on the Go implementation of EIP-1459
|
|
## at https://github.com/ethereum/go-ethereum/blob/master/p2p/dnsdisc
|
|
|
|
logScope:
|
|
topics = "dnsdisc.client"
|
|
|
|
type
|
|
Client* = object
|
|
## For now a client contains only a single tree in a single location
|
|
loc*: LinkEntry
|
|
tree*: Tree
|
|
|
|
## A Resolver proc takes a DNS domain as argument and
|
|
## returns the TXT record at that domain
|
|
Resolver* = proc(domain: string): Future[string] {.gcsafe.}
|
|
|
|
ResolveResult*[T] = Result[T, string]
|
|
|
|
const
|
|
ResolverTimeout* = 20.seconds # Maximum time to wait for DNS resolution
|
|
|
|
#######################
|
|
# Tree sync functions #
|
|
#######################
|
|
|
|
proc parseAndVerifySubtreeEntry(txtRecord: string, hashStr: string): EntryParseResult[SubtreeEntry] {.raises: [Defect, ValueError, Base32Error].} =
|
|
## Parses subtree TXT entry and verifies that it matches the hash
|
|
|
|
let res = parseSubtreeEntry(txtRecord)
|
|
|
|
if res.isErr():
|
|
# Return error result
|
|
trace "Failed to parse subtree entry", record=txtRecord
|
|
return res
|
|
|
|
let
|
|
subtreeEntry = res[]
|
|
checkHash = Base32.decode(hashStr)
|
|
entryHash = keccak256.digest(txtRecord.toBytes()).data
|
|
|
|
trace "Verifying parsed subtree entry", subtreeEntry=subtreeEntry
|
|
|
|
if entryHash[0..checkHash.len - 1] != checkHash:
|
|
# Check that entryHash starts with checkHash
|
|
trace "Failed to verify subdomain hash", subtreeEntry=subtreeEntry, hashStr=hashStr
|
|
return err("Could not verify subdomain hash")
|
|
|
|
ok(subtreeEntry)
|
|
|
|
proc resolveSubtreeEntry*(resolver: Resolver, loc: LinkEntry, subdomain: string): Future[ResolveResult[SubtreeEntry]] {.async, gcsafe, raises: [Defect, ValueError, Base32Error].} =
|
|
## Resolves subtree entry at given subdomain
|
|
## Follows EIP-1459 client protocol
|
|
|
|
let lookupFut = resolver(subdomain & "." & loc.domain)
|
|
|
|
if not await withTimeout(lookupFut, ResolverTimeout):
|
|
error "Failed to resolve DNS record", domain=subdomain
|
|
return err("Resolution failure: timeout")
|
|
|
|
let txtRecord = lookupFut.read()
|
|
|
|
trace "Resolving entry record", domain=subdomain, record=txtRecord
|
|
|
|
let res = parseAndVerifySubtreeEntry(txtRecord, subdomain)
|
|
|
|
if res.isErr():
|
|
error "Failed to parse and verify subtree entry", domain=loc.domain, record=txtRecord
|
|
return err("Resolution failure: " & res.error())
|
|
|
|
return ok(res[])
|
|
|
|
proc resolveAllEntries*(resolver: Resolver, loc: LinkEntry, rootEntry: RootEntry): Future[seq[SubtreeEntry]] {.async.} =
|
|
## Resolves all subtree entries at given root
|
|
## Follows EIP-1459 client protocol
|
|
|
|
var subtreeEntries: seq[SubtreeEntry]
|
|
|
|
var
|
|
# Initialise a hash set with the root hashes of ENR and link subtrees
|
|
hashes = toHashSet([rootEntry.eroot, rootEntry.lroot])
|
|
i = 1
|
|
|
|
while hashes.len > 0 and i <= 100:
|
|
# Recursively resolve leaf entries and add to return list.
|
|
# @TODO: Define a better depth limit. 100 was chosen arbitrarily.
|
|
inc(i)
|
|
|
|
let
|
|
# Resolve and remove random entry from subdomain hashes
|
|
nextHash = hashes.pop()
|
|
nextEntry = await resolveSubtreeEntry(resolver, loc, nextHash)
|
|
|
|
if nextEntry.isErr():
|
|
# @TODO metrics to track missing/failed entries
|
|
trace "Could not resolve next entry. Continuing.", subdomain=nextHash
|
|
continue
|
|
|
|
case nextEntry[].kind:
|
|
of Enr:
|
|
# Add to return list
|
|
subtreeEntries.add(nextEntry[])
|
|
of Link:
|
|
# Add to return list
|
|
subtreeEntries.add(nextEntry[])
|
|
of Branch:
|
|
# Add branch children to hashes, and continue resolving
|
|
hashes.incl(nextEntry[].branchEntry.children.toHashSet())
|
|
|
|
return subtreeEntries
|
|
|
|
proc verifySignature(rootEntry: RootEntry, pubKey: PublicKey): bool =
|
|
## Verifies the signature on the root against the public key
|
|
let sig = SignatureNR.fromRaw(rootEntry.signature)
|
|
|
|
var sigHash: seq[byte]
|
|
try:
|
|
sigHash = hashableContent(rootEntry)
|
|
except ValueError:
|
|
return false
|
|
|
|
if sig.isOk():
|
|
trace "Verifying signature", sig=repr(sig[]), msg=repr(sigHash), key=repr(pubKey)
|
|
return verify(sig = sig[],
|
|
msg = sigHash,
|
|
key = pubKey)
|
|
|
|
proc parseAndVerifyRoot(txtRecord: string, loc: LinkEntry): EntryParseResult[RootEntry] =
|
|
## Parses root TXT record and verifies signature
|
|
|
|
let res = parseRootEntry(txtRecord)
|
|
|
|
if res.isErr():
|
|
# Return error result
|
|
trace "Failed to parse root record", record=txtRecord
|
|
return res
|
|
|
|
let rootEntry = res[]
|
|
|
|
trace "Verifying parsed root entry", rootEntry=rootEntry
|
|
|
|
if not verifySignature(rootEntry, loc.pubKey):
|
|
trace "Failed to verify signature", rootEntry=rootEntry, pubKey=loc.pubKey
|
|
return err("Could not verify signature")
|
|
|
|
ok(rootEntry)
|
|
|
|
proc resolveRoot*(resolver: Resolver, loc: LinkEntry): Future[ResolveResult[RootEntry]] {.async.} =
|
|
## Resolves root entry at given location (LinkEntry)
|
|
## Also verifies the root signature and checks seq no
|
|
## Follows EIP-1459 client protocol
|
|
|
|
let lookupFut = resolver(loc.domain)
|
|
|
|
if not await withTimeout(lookupFut, ResolverTimeout):
|
|
error "Failed to resolve DNS record", domain=loc.domain
|
|
return err("Resolution failure: timeout")
|
|
|
|
let txtRecord = lookupFut.read()
|
|
|
|
info "Updating DNS discovery root", domain=loc.domain, record=txtRecord
|
|
|
|
let res = parseAndVerifyRoot(txtRecord, loc)
|
|
|
|
if res.isErr():
|
|
error "Failed to parse and verify root entry", domain=loc.domain, record=txtRecord
|
|
return err("Resolution failure: " & res.error())
|
|
|
|
return ok(res[])
|
|
|
|
proc syncTree(resolver: Resolver, rootLocation: LinkEntry): Future[Result[Tree, cstring]] {.async.} =
|
|
## Synchronises the client tree according to EIP-1459
|
|
|
|
let rootEntry = await resolveRoot(resolver, rootLocation)
|
|
|
|
if rootEntry.isErr:
|
|
return err("Failed to resolve root entry")
|
|
|
|
let
|
|
subtreeEntries = await resolveAllEntries(resolver, rootLocation, rootEntry.get())
|
|
tree = Tree(rootEntry: rootEntry.get(),
|
|
entries: subtreeEntries)
|
|
|
|
return ok(tree)
|
|
|
|
##############
|
|
# Client API #
|
|
##############
|
|
|
|
proc init*(T: type Client,
|
|
locationUrl: string): Result[T, cstring] =
|
|
## Initialise client from a DNS node list URL
|
|
## with format 'enrtree://<key>@<fqdn>'
|
|
|
|
let locLink = parseLinkEntry(locationUrl)
|
|
|
|
if locLink.isErr:
|
|
return err("Failed to create client")
|
|
|
|
return ok(Client(loc: locLink.get()))
|
|
|
|
proc getNodeRecords*(c: Client): seq[Record] =
|
|
## Returns a list of node records in the client tree
|
|
|
|
try:
|
|
return c.tree.getNodes().mapIt(it.record)
|
|
except ValueError:
|
|
return @[]
|
|
|
|
proc getTree*(c: var Client, resolver: Resolver): Tree {.raises: [Defect, CatchableError].} =
|
|
## Main entry point into the client
|
|
## Returns a synchronised copy of the tree
|
|
## at the configured client domain
|
|
##
|
|
## For now the client tree is (only) synchronised whenever accessed.
|
|
## Note that this is a blocking operation to maintain memory safety
|
|
## on var Client
|
|
##
|
|
## @TODO periodically sync client tree and return only locally cached version
|
|
|
|
c.tree = (waitFor syncTree(resolver, c.loc)).tryGet()
|
|
|
|
return c.tree
|