From 2d448241fdb8f8e806089ef4dc978d0eff211117 Mon Sep 17 00:00:00 2001 From: Hanno Cornelius <68783915+jm-clius@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:24:47 +0200 Subject: [PATCH] Add tree creation utility (#9) * Improve client error handling and API * Basic creator API * Add tree creator utility * Add tree creation basic tutorial * Minor improvements --- .gitmodules | 25 ++ Makefile | 4 + discovery/dnsdisc/builder.nim | 43 ++- discovery/dnsdisc/creator/tree_creator.nim | 211 +++++++++++++ .../dnsdisc/creator/tree_creator_conf.nim | 85 +++++ .../dnsdisc/creator/tree_creator_rpc.nim | 57 ++++ dnsdisc.nimble | 11 + docs/tutorial.md | 295 ++++++++++++++++++ tests/test_builder.nim | 2 +- vendor/nim-confutils | 1 + vendor/nim-http-utils | 1 + vendor/nim-json-rpc | 1 + vendor/nim-websock | 1 + vendor/nim-zlib | 1 + 14 files changed, 727 insertions(+), 11 deletions(-) create mode 100644 discovery/dnsdisc/creator/tree_creator.nim create mode 100644 discovery/dnsdisc/creator/tree_creator_conf.nim create mode 100644 discovery/dnsdisc/creator/tree_creator_rpc.nim create mode 100644 docs/tutorial.md create mode 160000 vendor/nim-confutils create mode 160000 vendor/nim-http-utils create mode 160000 vendor/nim-json-rpc create mode 160000 vendor/nim-websock create mode 160000 vendor/nim-zlib diff --git a/.gitmodules b/.gitmodules index 0d791b8..6d896c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -63,3 +63,28 @@ url = https://github.com/status-im/nim-serialization.git ignore = untracked branch = master +[submodule "vendor/nim-confutils"] + path = vendor/nim-confutils + url = https://github.com/status-im/nim-confutils.git + ignore = untracked + branch = master +[submodule "vendor/nim-json-rpc"] + path = vendor/nim-json-rpc + url = https://github.com/status-im/nim-json-rpc.git + ignore = untracked + branch = master +[submodule "vendor/nim-http-utils"] + path = vendor/nim-http-utils + url = https://github.com/status-im/nim-http-utils.git + ignore = untracked + branch = master +[submodule "vendor/nim-websock"] + path = vendor/nim-websock + url = https://github.com/status-im/nim-websock.git + ignore = untracked + branch = master +[submodule "vendor/nim-zlib"] + path = vendor/nim-zlib + url = https://github.com/status-im/nim-zlib.git + ignore = untracked + branch = master diff --git a/Makefile b/Makefile index a354d04..839a6bd 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,10 @@ update: | update-common rm -rf dnsdisc.nims && \ $(MAKE) dnsdisc.nims $(HANDLE_OUTPUT) +creator: | build deps + echo -e $(BUILD_MSG) "build/$@" && \ + $(ENV_SCRIPT) nim creator $(NIM_PARAMS) dnsdisc.nims + test: | deps echo -e "Running: $@" && \ $(ENV_SCRIPT) nim test $(NIM_PARAMS) dnsdisc.nims diff --git a/discovery/dnsdisc/builder.nim b/discovery/dnsdisc/builder.nim index ae6aede..3b8917c 100644 --- a/discovery/dnsdisc/builder.nim +++ b/discovery/dnsdisc/builder.nim @@ -17,9 +17,39 @@ export tree ## This implementation is based on the Go implementation of EIP-1459 ## at https://github.com/ethereum/go-ethereum/blob/master/p2p/dnsdisc + +## How we determine MaxChildren: (Adapted from go-ethereum) +## https://github.com/ethereum/go-ethereum/blob/4d88974864c3ee84a24b1088064162b8dbab8ad5/p2p/dnsdisc/tree.go#L116-L146 +## +## We want to keep the UDP size below 512 bytes. The UDP size is roughly: +## UDP length = 8 + UDP payload length ( 229 ) +## UPD Payload length: +## - dns.id 2 +## - dns.flags 2 +## - dns.count.queries 2 +## - dns.count.answers 2 +## - dns.count.auth_rr 2 +## - dns.count.add_rr 2 +## - queries (query-size + 6) +## - answers : +## - dns.resp.name 2 +## - dns.resp.type 2 +## - dns.resp.class 2 +## - dns.resp.ttl 4 +## - dns.resp.len 2 +## - dns.txt.length 1 +## - dns.txt resp_data_size +## So the total size is roughly a fixed overhead of `39`, and the size of the +## query (domain name) and response. +## The query size is, for example, FVY6INQ6LZ33WLCHO3BPR3FH6Y.snap.mainnet.ethdisco.net (52) +## We also have some static data in the response, such as `enrtree-branch:`, and potentially +## splitting the response up with `" "`, leaving us with a size of roughly `400` that we need +## to stay below. +## The number `370` is used to have some margin for extra overhead (for example, the dns query +## may be larger - more subdomains). const HashAbbrevSize = 1 + (16*13)/8 # Size of an encoded hash (plus comma) - MaxChildren* = 370 div toInt(HashAbbrevSize) # 13 children + MaxChildren* = 370 div toInt(HashAbbrevSize) # 13 children. See comment above for explanation. type BuilderResult*[T] = Result[T, string] @@ -196,7 +226,7 @@ proc signTree*(tree: var Tree, privateKey: PrivateKey): BuilderResult[void] = proc buildTree*(seqNo: uint32, enrRecords: seq[Record], - links: seq[string]): BuilderResult[Tree] = + links: seq[LinkEntry]): BuilderResult[Tree] = ## Builds a tree from given lists of ENR and links. var tree: Tree @@ -209,14 +239,7 @@ proc buildTree*(seqNo: uint32, linkEntries: seq[SubtreeEntry] enrEntries = enrRecords.mapIt(SubtreeEntry(kind: Enr, enrEntry: EnrEntry(record: it))) - - for link in links: - let parsedLinkRes = parseLinkEntry(link) - - if parsedLinkRes.isErr: - return err("Failed to parse link entry: " & parsedLinkRes.error) - - linkEntries.add(SubtreeEntry(kind: Link, linkEntry: parsedLinkRes.get())) + linkEntries = links.mapIt(SubtreeEntry(kind: Link, linkEntry: it)) # Build ENR and link subtrees diff --git a/discovery/dnsdisc/creator/tree_creator.nim b/discovery/dnsdisc/creator/tree_creator.nim new file mode 100644 index 0000000..70fa112 --- /dev/null +++ b/discovery/dnsdisc/creator/tree_creator.nim @@ -0,0 +1,211 @@ +{.push raises: [Defect]} + +## A utility module to create a Merkle tree encoding +## a list of ENR and link entries. This is a standalone +## module that can be run as a CLI application. It exposes +## a JSON-RPC API. + +import + std/[options, tables], + chronos, + chronicles, + stew/base32, + stew/results, + ../builder + +logScope: + topics = "tree.creator" + +type + TreeCreator* = object + privateKey: PrivateKey + domain: Option[string] + seqNo: uint32 + enrRecords: seq[Record] + links: seq[LinkEntry] + tree: Option[Tree] + isUpdated: bool + + CreatorResult*[T] = Result[T, string] + +############## +# Public API # +############## + +proc setDomain*(tc: var TreeCreator, domain: string) = + tc.domain = some(domain) + +proc getDomain*(tc: TreeCreator): Option[string] = + tc.domain + +proc addEnrEntries*(tc: var TreeCreator, enrRecords: seq[string]): bool = + debug "adding enr entries" + + var isSuccess = true + + for enrRecord in enrRecords: + # Attempt to create ENR from strings + var enr: Record + if enr.fromURI(enrRecord): + tc.enrRecords.add(enr) + tc.isUpdated = true + else: + debug "Failed to parse ENR entry", enrRecord=enrRecord + isSuccess = false + + return isSuccess + +proc getEnrEntries*(tc: TreeCreator): seq[Record] = + tc.enrRecords + +proc addLinkEntries*(tc: var TreeCreator, links: seq[string]): bool = + debug "adding link entries" + + var isSuccess = true + + for link in links: + # Attempt to parse link entries + let parsedLinkRes = parseLinkEntry(link) + + if parsedLinkRes.isOk: + tc.links.add(parsedLinkRes.get()) + tc.isUpdated = true + else: + debug "Failed to parse link entry", link=link + isSuccess = false + + return isSuccess + +proc getLinkEntries*(tc: TreeCreator): seq[LinkEntry] = + tc.links + +proc buildTree*(tc: var TreeCreator): CreatorResult[Tree] = + ## Attempts to build the tree, if it has not been built yet + ## or if it has since been updated + + debug "attempting to build tree" + + if tc.tree.isSome() and not tc.isUpdated: + # We've already built a tree and there has been no update since. + debug "no update. returning tree." + return ok(tc.tree.get()) + + var tree: Tree + + if tc.enrRecords.len == 0 and tc.links.len == 0: + # No entries to encode + return err("no enr or link entries configured") + + # Build tree from existing entries. Increase seq no as per EIP-1459. + tc.seqNo = tc.seqNo + 1 + let treeRes = buildTree(tc.seqNo, tc.enrRecords, tc.links) + + if treeRes.isErr(): + return err(treeRes.error) + + # Sign tree + tree = treeRes[] + + let signRes = tree.signTree(tc.privateKey) + + if signRes.isErr(): + return err(signRes.error) + + # Cache signed tree on creator and reset isUpdated state + tc.tree = some(tree) + tc.isUpdated = false + + return ok(tree) + +proc getTXTs*(tc: var TreeCreator): CreatorResult[Table[string, string]] = + debug "getting TXT records" + + if tc.domain.isNone(): + return err("Failed to create: no domain configured") + + # Attempt to build tree, if necessary + let buildRes = tc.buildTree() + + if buildRes.isErr(): + return err("Failed to create: " & buildRes.error) + + # Extract TXT records + let txtRes = tc.tree.get().buildTXT(tc.domain.get()) + + if txtRes.isErr(): + return err("Failed to create: " & txtRes.error) + + return ok(txtRes[]) + +proc getPublicKey*(tc: TreeCreator): string = + ## Returns the compressed 32 byte public key + ## in base32 encoding. This forms the "username" + ## part of the tree location URL as per + ## https://eips.ethereum.org/EIPS/eip-1459 + + Base32.encode(tc.privateKey.toPublicKey().toRawCompressed()) + +proc getURL*(tc: TreeCreator): CreatorResult[string] = + ## Returns the tree URL in the format + ## 'enrtree://@' as per + ## https://eips.ethereum.org/EIPS/eip-1459 + + if tc.domain.isNone(): + return err("Failed to create: no domain configured") + + return ok(LinkPrefix & tc.getPublicKey & "@" & tc.domain.get()) + +########################## +# Creator initialization # +########################## + +proc init*(T: type TreeCreator, + privateKey: PrivateKey, + domain = none(string), + enrRecords: seq[Record] = @[], + links: seq[LinkEntry] = @[]): T = + + let treeCreator = TreeCreator( + privateKey: privateKey, + domain: domain, + seqNo: 0, # No sequence no yet, as tree has not been built + enrRecords: enrRecords, + links: links, + isUpdated: true # Indicates that tree requires a build + ) + + return treeCreator + +{.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError +when isMainModule: + import + confutils, + stew/shims/net as stewNet, + ./tree_creator_conf, + ./tree_creator_rpc + + logScope: + topics = "tree.creator.setup" + + let + conf = TreeCreatorConf.load() + + # 1/2 Initialise TreeCreator + debug "1/2 initialising" + + let domain = if conf.domain == "": none(string) + else: some(conf.domain) + + var treeCreator = TreeCreator.init(conf.privateKey, + domain, + conf.enrRecords, + conf.links) + + # 2/2 Install JSON-RPC API handlers + debug "2/2 starting RPC API" + + treeCreator.startRpc(conf.rpcAddress, Port(conf.rpcPort)) + + debug "setup complete" + + runForever() diff --git a/discovery/dnsdisc/creator/tree_creator_conf.nim b/discovery/dnsdisc/creator/tree_creator_conf.nim new file mode 100644 index 0000000..832417a --- /dev/null +++ b/discovery/dnsdisc/creator/tree_creator_conf.nim @@ -0,0 +1,85 @@ +import + confutils, confutils/defs, confutils/std/net, + eth/p2p/discoveryv5/enr, + eth/keys, + stew/results, + ../tree + +type + TreeCreatorConf* = object + # General config + + privateKey* {. + desc: "Tree creator private key as 64 char hex string.", + defaultValue: PrivateKey.random(newRng()[]) + name: "private-key" }: PrivateKey + + domain* {. + desc: "Fully qualified domain name for the tree root entry", + defaultValue: "" + name: "domain" }: string + + enrRecords* {. + desc: "Discoverable ENR entry in ENR text encoded format. Argument may be repeated." + name: "enr-record" }: seq[Record] + + links* {. + desc: "Discoverable link entry in link entry format. Argument may be repeated." + name: "link" }: seq[LinkEntry] + + ## JSON-RPC config + + rpcAddress* {. + desc: "Listening address of the JSON-RPC server.", + defaultValue: ValidIpAddress.init("127.0.0.1") + name: "rpc-address" }: ValidIpAddress + + rpcPort* {. + desc: "Listening port of the JSON-RPC server.", + defaultValue: 8545 + name: "rpc-port" }: uint16 + +################## +# Config parsing # +################## + +proc parseCmdArg*(T: type PrivateKey, p: TaintedString): T = + try: + let pk = PrivateKey.fromHex(string(p)).tryGet() + return pk + except CatchableError: + raise newException(ConfigurationError, "Invalid private key") + +proc completeCmdArg*(T: type PrivateKey, val: TaintedString): seq[string] = + return @[] + +proc parseCmdArg*(T: type enr.Record, p: TaintedString): T = + var enr: enr.Record + + if not fromURI(enr, p): + raise newException(ConfigurationError, "Invalid ENR") + + return enr + +proc completeCmdArg*(T: type enr.Record, val: TaintedString): seq[string] = + return @[] + +proc parseCmdArg*(T: type LinkEntry, p: TaintedString): T = + try: + let linkEntry = parseLinkEntry(string(p)).tryGet() + return linkEntry + except CatchableError: + raise newException(ConfigurationError, "Invalid link entry") + +proc completeCmdArg*(T: type LinkEntry, val: TaintedString): seq[string] = + return @[] + +proc parseCmdArg*(T: type ValidIpAddress, p: TaintedString): T = + try: + let ipAddr = ValidIpAddress.init(p) + return ipAddr + except CatchableError as e: + raise newException(ConfigurationError, "Invalid IP address") + +proc completeCmdArg*(T: type ValidIpAddress, val: TaintedString): seq[string] = + return @[] \ No newline at end of file diff --git a/discovery/dnsdisc/creator/tree_creator_rpc.nim b/discovery/dnsdisc/creator/tree_creator_rpc.nim new file mode 100644 index 0000000..b8b2b77 --- /dev/null +++ b/discovery/dnsdisc/creator/tree_creator_rpc.nim @@ -0,0 +1,57 @@ +{.push raises: [Defect, CatchableError].} + +import + std/tables, + chronicles, + json_rpc/rpcserver, + stew/shims/net, + stew/results, + ./tree_creator + +logScope: + topics = "tree.creator.rpc" + +proc installRpcApiHandlers(initTc: TreeCreator, rpcsrv: RpcServer) {.gcsafe.} = + var tc = initTc # Create a mutable copy, to maintain memory safety + + rpcsrv.rpc("post_domain") do(domain: string) -> bool: + debug "post_domain" + tc.setDomain(domain) + return true + + rpcsrv.rpc("get_domain") do() -> Option[string]: + debug "get_domain" + return tc.getDomain() + + rpcsrv.rpc("post_enr_entries") do(enrRecords: seq[string]) -> bool: + debug "post_enr_entries" + return tc.addEnrEntries(enrRecords) + + rpcsrv.rpc("post_link_entries") do(links: seq[string]) -> bool: + debug "post_link_entries" + return tc.addLinkEntries(links) + + rpcsrv.rpc("get_txt_records") do() -> Table[string, string]: + debug "get_txt_records" + let txts = tc.getTXTs().tryGet() + return txts + + rpcsrv.rpc("get_public_key") do() -> string: + debug "get_public_key" + return tc.getPublicKey() + + rpcsrv.rpc("get_url") do() -> string: + debug "get_url" + let url = tc.getURL().tryGet() + return url + +proc startRpc*(tc: var TreeCreator, rpcIp: ValidIpAddress, rpcPort: Port) = + info "Starting RPC server" + let + ta = initTAddress(rpcIp, rpcPort) + rpcServer = newRpcHttpServer([ta]) + + installRpcApiHandlers(tc, rpcServer) + + rpcServer.start() + info "RPC Server started", ta diff --git a/dnsdisc.nimble b/dnsdisc.nimble index d9f48d7..aa428f9 100644 --- a/dnsdisc.nimble +++ b/dnsdisc.nimble @@ -20,6 +20,14 @@ requires "nim >= 1.2.0", "nimcrypto" # Helper functions +proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = + if not dirExists "build": + mkDir "build" + # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" + var extra_params = params + for i in 2.. NIM_PARAMS="-d:chronicles_log_level=INFO" make test2` @@ -27,5 +35,8 @@ proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = # specified here. exec "nim " & lang & " -r " & params & " tests/" & name & ".nim" +task creator, "Build DNS discovery tree creator": + buildBinary "tree_creator", "discovery/dnsdisc/creator/", "-d:chronicles_log_level=DEBUG -d:chronosStrictException" + task test, "Build & run all DNS discovery tests": test "all_tests" diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..e8015e0 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,295 @@ +# DNS-based discovery Tree Creation: Basic Tutorial + +## Background + +The `tree_creator` is a command line utility used to create and update [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459) compliant Merkle trees. +It takes as input a list of node records (in ENR text encoding) and link entries pointing to other trees. +The `tree_creator` utility will keep track of the tree sequence number and increase it whenever the encoded links or ENR entries are updated. +The root domain is configurable. +The most useful output is a map of subdomains to TXT records that can easily be converted to a zone file and deployed to a DNS name server, +from where the encoded lists of links and ENR can be retrieved by clients. + +## How to build and run + +To build and run `tree_creator` using its default configuration: + +```bash +# Build `tree_creator` utility +make creator + +# Run tree creator utility with default configuration +./build/tree_creator +``` + +To initialise the tree with some entries when running the utility, +specify `--enr-record` or `--link` command line options. +Both arguments may be repeated as many times as necessary. + +```bash +./build/tree_creator \ +--link=enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@never.gonna.give.you.up \ +--enr-record=enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA \ +--enr-record=enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI +``` + +The domain can be specified when running the utility with the `--domain` option. +This can later be modified using the JSON-RPC API. + +```bash +./build/tree_creator --domain=mydomain.example.org +``` + +For a full list of available command line options, +run the utility with the `--help` option: + +```bash +./build/tree_creator --help +``` + +## Using the JSON-RPC API + +The JSON-RPC API is exposed by default on the localhost (`127.0.0.1`) at port `8545`. +It is possible to change this configuration by setting the `rpc-address` and `rpc-port` options when running the node: + +```bash +./build/tree_creator --rpc-address:127.0.1.1 --rpc-port:8546 +``` + +The following JSON-RPC API methods are defined for the `tree_creator`: + +--- + +### 1. `post_domain` + +The `post_domain` method sets the fully qualified root domain name for the tree. + +#### Parameters + +| Field | Type | Inclusion | Description | +| ----: | :---: | :---: |----------- | +| `domain` | `String` | mandatory | The fully qualified domain name to set for the tree root entry | + +#### Response + +- **`Bool`** - `true` on success or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +--- + +### 2. `get_domain` + +The `get_domain` method returns the domain currently configured for this tree. + +#### Parameters + +none + +#### Response + +- The domain or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +--- + +### 3. `post_enr_entries` + +The `post_enr_entries` method adds a sequence of node records to the `tree_creator` to be encoded. +The node records must be ENR text encoded as per [EIP-778](https://eips.ethereum.org/EIPS/eip-778#text-encoding) + +#### Parameters + +| Field | Type | Inclusion | Description | +| ----: | :---: | :---: |----------- | +| `enrRecords` | `Array`[`String`] | mandatory | A list of ENR records to add to the tree | + +#### Response + +- **`Bool`** - `true` on success or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +--- + +### 4. `post_link_entries` + +The `post_link_entries` method adds a sequence of links referencing other trees to the `tree_creator` to be encoded. +The links must formatted according to the the `enrtree` scheme defined in [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459#dns-record-structure) + +#### Parameters + +| Field | Type | Inclusion | Description | +| ----: | :---: | :---: |----------- | +| `links` | `Array`[`String`] | mandatory | A list of links to other trees to add to the tree | + +#### Response + +- **`Bool`** - `true` on success or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +--- + +### 5. `get_txt_records` + +The `get_txt_records` method returns a map of subdomains to TXT records for the encoded Merkle tree. +This can easily be converted to a zone file and deployed to a DNS name server. + +#### Parameters + +none + +#### Response + +- A map of subdomain to TXT record or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +--- + +### 6. `get_public_key` + +The `get_public_key` method returns the compressed 32 byte public key in base32 encoding. +This forms the "username" part of the tree location URL as per [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459) + +#### Parameters + +none + +#### Response + +- The public key or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +--- + +### 7. `get_url` + +The `get_url` method returns the tree URL in the format `enrtree://@` as per [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459) + +#### Parameters + +none + +#### Response + +- The tree URL or an [error](https://www.jsonrpc.org/specification#error_object) on failure. + +## JSON-RPC API example + +One way to access JSON-RPC methods is by using the `cURL` command line tool. + +For example: + +```bash +curl -d '{"jsonrpc":"2.0","id":"id","method":"", "params":[]}' --header "Content-Type: application/json" http://localhost:8545 +``` + +where `` is the name of the JSON-RPC method to call and `` is a comma-separated `Array` of parameters to pass as arguments to the selected method. + +This example assumes that the `tree_creator` is running and the API is exposed on the `localhost` at port `8545` (the default configuration). + +### Setting the domain + +#### Example request: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "method": "post_domain", + "params": [ + "mynodes.example.org" + ] +} +``` + +#### Example response: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "result": true +} +``` + +### Adding ENR and link entries + +#### Example request - adding ENR: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "method": "post_enr_entries", + "params": [ + [ + "enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA", + "enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o" + ] + ] +} +``` + +#### Example request - adding links: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "method": "post_link_entries", + "params": [ + [ + "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@never.gonna.let.you.down" + ] + ] +} +``` + +### Retrieving encoded TXT records + +#### Example request: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "method": "get_txt_records", + "params": [] +} +``` + +#### Example response: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "result": { + "mynodes.example.org": "enrtree-root:v1 e=DHTTG472H5RVLIHOIQSPLVMGGA l=T7J7RURX6U73I7N4DKNIJYUOUU seq=1 sig=9e4E1Yw2cdPjuLvwjhfmBvjDKepAFow0x5BfVy8JzG56RDSTErOFxOz8eUzBO5l_acE-VHQLc9TFB8muSbZH6QE", + "T7J7RURX6U73I7N4DKNIJYUOUU.mynodes.example.org": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@never.gonna.let.you.down", + "DHTTG472H5RVLIHOIQSPLVMGGA.mynodes.example.org": "enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,MHTDO6TMUBRIA2XWG5LUDACK24", + "MHTDO6TMUBRIA2XWG5LUDACK24.mynodes.example.org": "enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o", + "2XS2367YHAXJFGLZHVAWLQD4ZY.mynodes.example.org": "enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA" + } +} +``` + +### Retrieving the tree URL + +#### Example request: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "method": "get_url", + "params": [] +} +``` + +#### Example response: + +```json +{ + "jsonrpc": "2.0", + "id": "id", + "result": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@mynodes.example.org" +} +``` + +## References + +1. [EIP-778](https://eips.ethereum.org/EIPS/eip-778) +2. [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459) diff --git a/tests/test_builder.nim b/tests/test_builder.nim index 36a3f03..9d2a670 100644 --- a/tests/test_builder.nim +++ b/tests/test_builder.nim @@ -71,7 +71,7 @@ procSuite "Test DNS Discovery: Merkle Tree builder": enr1: Record enr2: Record enr3: Record - link = "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org" + link = parseLinkEntry("enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org").get() seqNo = 1.uint32 check: diff --git a/vendor/nim-confutils b/vendor/nim-confutils new file mode 160000 index 0000000..ab4ba1c --- /dev/null +++ b/vendor/nim-confutils @@ -0,0 +1 @@ +Subproject commit ab4ba1cbfdccdb8c0398894ffc25169bc61faeed diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils new file mode 160000 index 0000000..689da19 --- /dev/null +++ b/vendor/nim-http-utils @@ -0,0 +1 @@ +Subproject commit 689da19e9e9cfff4ced85e2b25c6b2b5598ed079 diff --git a/vendor/nim-json-rpc b/vendor/nim-json-rpc new file mode 160000 index 0000000..b2417fc --- /dev/null +++ b/vendor/nim-json-rpc @@ -0,0 +1 @@ +Subproject commit b2417fc0719a6d5069437a3097645d1fae6954d6 diff --git a/vendor/nim-websock b/vendor/nim-websock new file mode 160000 index 0000000..f354dfe --- /dev/null +++ b/vendor/nim-websock @@ -0,0 +1 @@ +Subproject commit f354dfebe9f45ba33e6ad3b6f3163d516c9d0727 diff --git a/vendor/nim-zlib b/vendor/nim-zlib new file mode 160000 index 0000000..6bbc67d --- /dev/null +++ b/vendor/nim-zlib @@ -0,0 +1 @@ +Subproject commit 6bbc67d09e624d69052d62b2353878f491186942