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.
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
:
to
: this is the address of the ENS resolver contractdata
: this is a hash of the function we’re calling and its arguments
How this hash for data
is calculated is a little
complicated but just
for the addr
function it is easier:
-
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 textaddr(bytes32)
, -
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 marker59 d1 d4 3c
: istext(bytes32,string)
hashed and truncated to 4 bytes05a67c0ee82964c4f7394cdd47fee7f4d9503a23c09c38341779ea012afe6e00
- 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)
- “url” (
And the same for the result:
{"id":1,
"jsonrpc":"2.0",
"result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001468747470733a2f2f656e732e646f6d61696e732f000000000000000000000000"}
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000014
68747470733a2f2f656e732e646f6d61696e732f000000000000000000000000
20
: offset to string data14
: 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.
- Erik Winkels <aerique@xs4all.nl>
- https://www.aerique.net/
https://twitter.com/aerique- aerique.eth