Skip links
eth_logo

Exploiting eth_call for optimization purposes

The idea arises from the need to extract specific information from a diverse group of blocks while aiming to avoid consuming bandwidth, processing power, and money.

What’s eth_callabout?

When obtaining information from the blockchain, the standard method for a programmer is through an RPC node. The Ethereum RPC node offers many useful services. Here, Json RPC-API, you can see most of these services. Among them, for example, we find::

  • eth_getBalance: Returns the balance of ETH/basecoin associated with the specified account.
  • eth_getBlockByNumber: Retrieves information of a block given its block number.
  • eth_getBlockByHash: Retrieves information of a block given its hash.
  • eth_sendTransaction: Sends a transaction that will be recorded on the blockchain, paying the corresponding gas fees according to its type, etc. It probably alters the state of the blockchain in some way, e.g., changes some balance, changes some configuration, etc.
  • eth_call: Invokes a contract function locally without creating a transaction on the blockchain. This allows reading data from the blockchain without making any state changes or paying gas fees.

RPC node extra features

Additionally, the RPC node allows us two extra functionalities:

  1. For every invoked function that returns a value, we can specify the block from which we want to obtain that value. By default, if this parameter is not specified, the value is retrieved from the latest block. Examples:

  • eth_getBalance(0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97) => Retrieves the current balance of the given address.
  • eth_getBalance(0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97, ‘0x14e025f’) => Retrieves the balance of the given address at block 21,889,631.
  1. JSON-RPC nodes also allow executing multiple calls in batch mode. Instead of making five separate calls to invoke five different functions, we can make a single call and specify all five functions we want to invoke.

A standard call looks like this:

curl -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' https://node-rpc:8545/

Formatted in a more readable way, the query would be:

{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}

The expected response would be:

{
  "id":83,
  "jsonrpc": "2.0",
  "result": "0x4b7"
}

A batch call (which in this case is redundant as it repeats the same query) would be:

curl -X POST --data '[{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}, {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":2}, {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":3}]' https://node-rpc:8545/

Formatted in a more readable way, the query would be:

[{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}, 
 {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":2},
 {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":3}]

The response in this case would be:

[{
  "id":1,
  "jsonrpc": "2.0",
  "result": "0x4b7"
},{
  "id":2,
  "jsonrpc": "2.0",
  "result": "0x4b7"
},{
  "id":3,
  "jsonrpc": "2.0",
  "result": "0x4b7"
}]

A simple problem

Let’s say you need to find the block corresponding to a specific date, and you know it’s within a range of five possible blocks (or n blocks in a general case). You’d have to do something like this:

curl -X POST --data '[
 {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":[11098795, false],"id":1},
 {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":[11098796, false],"id":2},
 {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":[11098797, false],"id":3},
 {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":[11098798, false],"id":4},
 {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":[11098799, false],"id":5}
 ]'  https://node-rpc:8545/`

(This exactly: curl -X POST --data '[{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["11098795", false],"id":1},{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["11098796", false],"id":2},{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["11098797", false],"id":3},{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["11098798", false],"id":4},{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["11098799", false],"id":5}]' https://ethereum-rpc.publicnode.com )

This would return a JSON response of more than 50KB containing a lot of information about the requested blocks…

A Possible Alternative

If you had a smart contract with a Solidity function like this: function get(uint blocknr) returns (uint) { return block.timestamp } you could call it using eth_call specifying the block numbers in question. That way, instead of pulling massive amounts of unnecessary data, you’d extract just the timestamps for those blocks. However, this requires deploying a contract containing this function or finding an existing one that provides the same functionality.

The Good News

The good news is that eth_call is highly flexible and allows us to execute EVM bytecode directly on the blockchain. This means we can achieve the same result without needing a pre-deployed contract by executing the following:

curl -X POST --data '[
 {"jsonrpc":"2.0","method":"eth_call",
 "params":[{"data": "0x4260005260206000f3"},"0xa95aab"],
 "id":11098795},
 
 {"jsonrpc":"2.0","method":"eth_call",
 "params":[{"data": "0x4260005260206000f3"},"0xa95aac"],
 "id":11098796},
 
 {"jsonrpc":"2.0","method":"eth_call",
 "params":[{"data": "0x4260005260206000f3"},"0xa95aad"],
 "id":11098797},
 
 {"jsonrpc":"2.0","method":"eth_call",
 "params":[{"data": "0x4260005260206000f3"},"0xa95aae"],
 "id":11098798},
 
 {"jsonrpc":"2.0","method":"eth_call",
 "params":[{"data": "0x4260005260206000f3"},"0xa95aaf"],
 "id":11098799}
 ]'  https://node-rpc:8545/`

(This exactly: curl -X POST --data '[{"jsonrpc":"2.0","method":"eth_call","params":[{"data": "0x4260005260206000f3"},"0xa95aab"],"id":11098795},{"jsonrpc":"2.0","method":"eth_call","params":[{"data": "0x4260005260206000f3"},"0xa95aac"],"id":11098796},{"jsonrpc":"2.0","method":"eth_call","params":[{"data": "0x4260005260206000f3"},"0xa95aad"],"id":11098797},{"jsonrpc":"2.0","method":"eth_call","params":[{"data": "0x4260005260206000f3"},"0xa95aae"],"id":11098798},{"jsonrpc":"2.0","method":"eth_call","params":[{"data": "0x4260005260206000f3"},"0xa95aaf"],"id":11098799}]' https://ethereum-rpc.publicnode.com )

The response:

[{"jsonrpc":"2.0","id":11098795,"result":
"0x000000000000000000000000000000000000000000000000000000005f9004b9"},
{"jsonrpc":"2.0","id":11098796,"result":
"0x000000000000000000000000000000000000000000000000000000005f9004c7"},
{"jsonrpc":"2.0","id":11098797,"result":
"0x000000000000000000000000000000000000000000000000000000005f9004c9"},
{"jsonrpc":"2.0","id":11098798,"result":
"0x000000000000000000000000000000000000000000000000000000005f9004d5"},
{"jsonrpc":"2.0","id":11098799,"result":
"0x000000000000000000000000000000000000000000000000000000005f9004d8"}
]

Where we can clearly see the timestamp of each of the requested blocks:

...
11098797 -> 0x05f9004c9 -> 2020-10-21T09:52:21.000Z
11098798 -> 0x05f9004d5 -> 2020-10-21T09:52:09.000Z
..

What is: 0x4260005260206000f3 ?

This is raw EVM bytecode that retrieves the timestamp of the current block and returns it. You can find the complete list of EVM opcodes here.

Breaking it down:
 0x
   42         TIMESTAMP   // Pushes the current block's timestamp onto the stack 
   60 00      PUSH 0      // Pushes 0
   52         MSTORE      // Stores the timestamp at memory position 0 
   60 20      PUSH 32     // Pushes 32
   60 00      PUSH 0      // Pushes 0
   f3         RETURN      // Returns 32 bytes from memory position 0 (which contains the timestamp)

By executing this directly in the context of each block we’re searching for, we bypass the need to deploy a smart contract. Instead, we efficiently retrieve only the exact timestamp data we need, without any unnecessary overhead.