Skip to main content

Cutting Out the Middle Man

(The repository with code is here: https://gitlab.com/aerique/dnsdist-ens/. The links to code have not been fixed.)

Introduction

This article is an update to an article I wrote about one-and-half years ago. My intention was to continue with it sooner but life got in the way.

The previous article can be found here: Querying the Ethereum Name Service from DNS

One of the main issues I had with the article was the dependency on a JavaScript proxy between DNSdist and ENS. Since DNSdist has Lua (LuaJit) as scripting language the search was on for a web3 library written in it and, lo and behold, one actually exists now: Web3.Lua !

The library helped me get started but eventually was not used directly because it does not support LuaJit. I did nick a few functions from it though: expand, fromhex and tohex.

The most relevant function from Web3.Lua was Ethereum Keccak (‘SHA3’) and that function was also found in another awesome library which did support LuaJit: pure_lua_SHA.

This library did need one small tweak because it only supported modern SHA3 and not the older one Ethereum uses. It took me several hours to figure out but eventually it came down to changing a 6 to a 1 on a single line: https://github.com/Egor-Skriptunoff/pure_lua_SHA/blob/master/sha2.lua#L4618

I have tried to contact both authors but unfortunately the Web3.Lua author is from Ukraine and the pure_lua_SHA from Russia. I hope they’re both okay.

XKCD 2347: Dependency

Direct Interaction with the ENS Resolver Contract

So in true web3 fashion we’re cutting out the middle man!

Compared to the last article the Lua code has hopefully improved a little. We’re not polluting the global namespace anymore which, according to my esteemed colleague Peter van Dijk, “was a little gross”.

The function resolve_ens is mostly similar to the earlier lua_ens and the main function in resolve-ens.lua is ens_text:

local function ens_text (name, field)
    local data = '0x59d1d43c' .. namehash(name) ..
                 expand_left(tohex(64))     ..
                 expand_left(tohex(#field)) ..
                 expand_right(tohex(field))
    local code, status, response = ens_resolver_call(data)
    if (status == 200) then
        local result = string.sub(json.decode(response[1])['result'], 3)
        local length = tonumber(string.sub(result, 65, 128), 16)
        local content = fromhex(string.sub(result, 129, 129 + length * 2 - 1))
        return content
    else
        return nil
    end
end

This is not production-ready code since it still has magic values, hardcoded offsets, hardcoded lengths and almost no error checking but it gets the job done (most of the time).

The see what the function does lets explore calls to the ENS resolver contract with two curl commands, first the more basic addr function and second the text function.

Call to ENS Resolver Function addr(bytes32 node)

(I have asked Pocket Network for permission for using their gateway in these examples.)

A lot of the following command should be readable except for data inside params.

$ curl -X POST -H "Content-Type: application/json" \
       --data '{"jsonrpc":"2.0",
                "method":"eth_call",
                "params":[{"to":"0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41",
                           "data":"0x3b3b57dede9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"},
                          "latest"],
                "id":1}' \
       https://eth-rpc.gateway.pokt.network/

The curl command sends a JSON-RPC request to the Pocket Network gateway eth-rpc.gateway.pokt.network. The method used is eth_call, which is what one uses to call functions in Ethereum contracts.

The two most interesting items in this command are inside params:

How this hash for data is calculated is a little complicated but just for the addr function it is easier:

  1. If we skip 0x the next four bytes (3b 3b 57 de) are created by taking the first 8 characters of the hash of the text addr(bytes32),

  2. Since the only argument for that function is bytes32 and since we know from the specification the argument is the namehash of an ENS name we add that to the data we already have.

0x3b3b57dede9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f
  ^^^^^^^^
     ||   ^^^^^^^^^^^^^^^^^^^^ namehash of foo.eth ^^^^^^^^^^^^^^^^^^^^^^^
  function

The result of the curl command is:

{"id":1,
 "jsonrpc":"2.0",
 "result":"0x000000000000000000000000ce1e62f71bc7d7bb593ec2540e62c870dc7187bc"}

and again from the documentation we know this is a 20 byte Ethereum address padded to 32 bytes so: 0xce1e62f71bc7d7bb593ec2540e62c870dc7187bc.

A look at ENS tells us this is correct: https://app.ens.domains/name/foo.eth/details

The same happens in the ens_addr function in resolve-ens.lua.

Call to ENS Resolver Function text(bytes32 node, string key)

The hashing in the text function is a little more complicated because the second argument is a string which is “dynamic data” and requires supplying an offset and a length in the data of params.

$ curl -X POST -H "Content-Type: application/json" \
       --data '{"jsonrpc":"2.0",
                "method":"eth_call",
                "params":[{"to":"0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41",
                           "data":"0x59d1d43c05a67c0ee82964c4f7394cdd47fee7f4d9503a23c09c38341779ea012afe6e000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000375726c0000000000000000000000000000000000000000000000000000000000"},
                          "latest"],
                "id":1}' \
       https://eth-rpc.gateway.pokt.network/

As you can see the RPC data is more complicated. Lets partition it to make it more clear:

0x
59d1d43c
05a67c0ee82964c4f7394cdd47fee7f4d9503a23c09c38341779ea012afe6e00
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000003
75726c0000000000000000000000000000000000000000000000000000000000
  • 0x: hex marker
  • 59 d1 d4 3c: is text(bytes32,string) hashed and truncated to 4 bytes
  • 05a67c0ee82964c4f7394cdd47fee7f4d9503a23c09c38341779ea012afe6e00
    • namehash of “nick.eth”
  • 0000000000000000000000000000000000000000000000000000000000000040
    • offset to string data in bytes
  • 0000000000000000000000000000000000000000000000000000000000000003
    • length of string data: 3 ⇒ “url”
  • 75726c0000000000000000000000000000000000000000000000000000000000
    • “url” (75 72 6c), padded on the right this time (don’t ask me why)

And the same for the result:

{"id":1,
 "jsonrpc":"2.0",
 "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001468747470733a2f2f656e732e646f6d61696e732f000000000000000000000000"}
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000014
68747470733a2f2f656e732e646f6d61696e732f000000000000000000000000
  • 20: offset to string data
  • 14: length of string data in hex, so 20 in decimal
68 74 74 70 73 3a 2f 2f 65 6e 73 2e 64 6f 6d 61 69 6e 73 2f
h  t  t  p  s  :  /  /  e  n  s  .  d  o  m  a  i  n  s  /

Which can be verified at: https://app.ens.domains/name/nick.eth/details

Running it Yourself

This time we’re using Docker so we don’t pollute the OS. Check out this repository and then:

  • bin/build.sh
  • bin/run.sh
  • cd ens
  • dnsdist --conf dnsdist.conf

Now we can do DNS queries against the running DNSdist with:

  • docker exec -it ens-lua dig @localhost -p 5200 nick.eth

ENS Experiments

There’s also an ens-experiments.lua file in the ens directory. This is the file that I build while working on this topic and figuring things out. It is more verbose than what is used for DNSdist and can be run by issuing luajit ens-experiments.lua.

Outro

I hope this explained the data sections of JSON-RPC requests to Ethereum a little more. Myself, I missed such an explanation when looking into ENS calls.