UPDATE: I was wrong, Infura doesn't censor RPC reads! This post caught the eyes of Infura, and they set out and wrote a blog post which you should check out. Although, the party isn't over yet. The option to censor is still there unless we use secure RPC. Which is why I ended up building out this, check it out- https://github.com/liamzebedee/eth-verifiable-rpc
Liam Zebedee (@liamzebedee).
This is a spec, a request for either (1) grants or (2) builders. Please reach out on the Twitter thread / over DM's if you're interested in either.
Recently, Ethereum node providers like Infura/Alchemy started censoring parts of the Ethereum database from being read via the JSON-RPC API's.
This proposal is to programatically detect this, by building a local EVM shim that verifiably loads state from a remote node during execution.
Example: the ENS entry for tornadocash.eth
On a censoring provider like Infura, the contenthash key for tornadocash.eth
returns 0, where in fact we know it to be nonzero.
You can verifty this simply using cast
from the Foundry toolbelt:
(base) ➜ lib git:(main) ✗ ETH_RPC_URL=https://mainnet.infura.io/v3/84842078b09946638c03157f83405213 cast call 0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8 "contenthash(bytes32 node)" tornadocash.eth
0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
As part of my work on Dappnet, I know that it's being censored. But this isn't being talked about.
What is worse, is that we don't know how to detect it. So I'm implementing an RPC provider marketplace, and I'm unable to tell which providers will at a moment's notice, block users from accessing their money/dapps.
This section outlines (1) how Ethereum works and then (2) how we can detect censorship.
What is happening when we call cast call 0x222... "contenthash(bytes32 node)" tornadocash.eth
?
cast call
translates to an eth_call
RPC, which translates to running the EVM with the following message (as EVM is a message-passing model):
(name => (key => value))
. To track this, we call a contract called the resolver. The resolver's address is 0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8
, which we'll call ENS_RESOLVER
.contenthash(bytes32 node)
(impl), a function on the resolver contract.cast abi-encode "contenthash(bytes32 node)(bytes memory)" $(cast --from-ascii "tornadocash.eth")
Message(from=0x0, to=$ENS_RESOLVER, data=0x746f726e61646f636173682e6574680000000000000000000000000000000000, value=0 ether)
,eth_call
is run, the EVM executes the bytecode of the contract, and returns data from the storage.How does storage work?
sstore
, sload
opcodes), no relational model (joins, etc). Smart contracts are like writing programs that natively use the database for storing their data structures.memory
aka RAM, and storage
aka disk.storage
, and other contracts cannot read it, they must use contract calls to interface with each other.How does consensus work?
How does the block hash represent?
state => (Accounts(address => balance), Code(address => bytes), Storage(contract_address => (bytes32 => bytes)))
Concept:
contenthash
for tornadocash.eth
is simply running a very small amount of EVM code, that interacts with a very small amount of state.
state.Code[ENSResolver]
state.Code[ContentHashResolver]
state.Storage[ContentHashResolver][hashes][tornadocash.eth]
eth_getStorageAt
, we can trivially verify if it was censored or not. How?
(block_hash, storage, ContentHashResolver, hashes, tornadocash.eth)
Ideation:
eth_getStorageAt
eth_call
client side (ie. something like Wei/FUCory's work), and lazily loading the storage from the remote execution node.
msg.to
contract's code.CALL
, load the corresponding contract's code.SLOAD
, load the corresponding storage key.contenthash(xx)
Next steps:
Why? Because while we know which nodes censor transactions to Tornado, we don't know which nodes censor read-access to Tornado.