Protocol poking
Part IV - Examining
- Hypothesis: The information is useful for a regular user.
- Methods: Use local Ethereum node as if it were a Portal Node, use the index to get transaction receipts relevant to the user.
- Conclusion: The receipts for an address are obtainable but not immediately useful without additional work.
(Continued from pokinng part 3)
The Practicality
So now there is a way to punch in an address and out comes a list of transaction identifiers.
0xabcd...1234
> block_number_a, transaction_index_x
> block_number_b, transaction_index_y
> block_number_c, transaction_index_z
What can be done with that? Is this a pretend solution that doesn’t really lead to anything tangible for a normal person?
We will try to use the information we have obtained from this new index. Think of this as a ~350MB thingy that was downloaded over IPFS for the user. Their wallet knows what their main address starts with, and initiated the download for the appropriate data.
Also imagine that they have a Portal Node. It holds some data, defined by the node’s ID and some resource settings like “don’t take up more than x space on my laptop”.
We will for now connect to a regular local node over http://localhost:8545
, and pretend
it is a Trin Portal Client by limiting our requests to those that the Portal Network
will support.
The Toolbox
There are many supported methods listed in the Portal Network spec, but these catch my eye for our intended purpose:
eth_getTransactionByBlockAndIndex(block_number, transaction_index)
- Returns tx_hash (then go to
eth_getTransactionReceipt
)
- Returns tx_hash (then go to
eth_getTransactionReceipt(tx_hash)
- Has logs, which has topics, data fields and contract contexts (then go to
eth_getCode
). - Has revert status and gas used.
- Has logs, which has topics, data fields and contract contexts (then go to
eth_getCode
- Returns the runtime bytecode which contains source code metadata, which can be used to get a link to the contract abi.
The Monotransaction
Given a single transaction eth_getTransactionByBlockAndIndex
delivers, we
can have a look and see if it is useful for a normal user.
I pick a random address that appears in a block that the sample data covers.
0x846be97d3bf1e3865f3caf55d749864d39e54cb9
In min-know I set up an example that extracts and prints the data.
cargo run --example user_0_find_transactions
It finds two transactions for this user:
Txs [
AppearanceTx {
block: 12387154,
index: 312,
},
AppearanceTx {
block: 12387161,
index: 166,
},
]
Another example picks the first transaction and ask the node about it:
cargo run --example user_1_transaction_receipt
It finds that there are 5 logs. Here they are below, feel free to skip past them. It is clear that a normal user is not going to be interested.
Transaction gas used: Some(123808)
Transaction logs: [
Log {
address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2,
topics: [
0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c,
0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d,
],
data: Bytes(
[
0,
0,
0,
...
142,
0,
0,
],
),
block_hash: Some(
0x6ef56e42a79d6732c770cbbbf2f00e2d05729c8b450351fff8912dc8191c4951,
),
block_number: Some(
12387154,
),
transaction_hash: Some(
0x1a8d94dda1694bad33384215bb3dc0a56652b7069c71d2b1afed35b24c9b54df,
),
transaction_index: Some(
312,
),
log_index: Some(
346,
),
transaction_log_index: None,
log_type: None,
removed: Some(
false,
),
},
Log {
address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2,
topics: [
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d,
0x0000000000000000000000001636a5dfcf7a21945c06d1bea40b52ce975ea614,
],
data: Bytes(
[
0,
0,
0,
...
142,
0,
0,
],
),
block_hash: Some(
0x6ef56e42a79d6732c770cbbbf2f00e2d05729c8b450351fff8912dc8191c4951,
),
block_number: Some(
12387154,
),
transaction_hash: Some(
0x1a8d94dda1694bad33384215bb3dc0a56652b7069c71d2b1afed35b24c9b54df,
),
transaction_index: Some(
312,
),
log_index: Some(
347,
),
transaction_log_index: None,
log_type: None,
removed: Some(
false,
),
},
Log {
address: 0x106d3c66d22d2dd0446df23d7f5960752994d600,
topics: [
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,
0x0000000000000000000000001636a5dfcf7a21945c06d1bea40b52ce975ea614,
0x000000000000000000000000846be97d3bf1e3865f3caf55d749864d39e54cb9,
],
data: Bytes(
[
0,
0,
0,
...
132,
118,
220,
],
),
block_hash: Some(
0x6ef56e42a79d6732c770cbbbf2f00e2d05729c8b450351fff8912dc8191c4951,
),
block_number: Some(
12387154,
),
transaction_hash: Some(
0x1a8d94dda1694bad33384215bb3dc0a56652b7069c71d2b1afed35b24c9b54df,
),
transaction_index: Some(
312,
),
log_index: Some(
348,
),
transaction_log_index: None,
log_type: None,
removed: Some(
false,
),
},
Log {
address: 0x1636a5dfcf7a21945c06d1bea40b52ce975ea614,
topics: [
0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1,
],
data: Bytes(
[
0,
0,
0,
...
251,
144,
101,
],
),
block_hash: Some(
0x6ef56e42a79d6732c770cbbbf2f00e2d05729c8b450351fff8912dc8191c4951,
),
block_number: Some(
12387154,
),
transaction_hash: Some(
0x1a8d94dda1694bad33384215bb3dc0a56652b7069c71d2b1afed35b24c9b54df,
),
transaction_index: Some(
312,
),
log_index: Some(
349,
),
transaction_log_index: None,
log_type: None,
removed: Some(
false,
),
},
Log {
address: 0x1636a5dfcf7a21945c06d1bea40b52ce975ea614,
topics: [
0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822,
0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d,
0x000000000000000000000000846be97d3bf1e3865f3caf55d749864d39e54cb9,
],
data: Bytes(
[
0,
0,
0,
...
0,
0,
0,
],
),
block_hash: Some(
0x6ef56e42a79d6732c770cbbbf2f00e2d05729c8b450351fff8912dc8191c4951,
),
block_number: Some(
12387154,
),
transaction_hash: Some(
0x1a8d94dda1694bad33384215bb3dc0a56652b7069c71d2b1afed35b24c9b54df,
),
transaction_index: Some(
312,
),
log_index: Some(
350,
),
transaction_log_index: None,
log_type: None,
removed: Some(
false,
),
},
]
The Implications
That receipt is pretty opaque. Can we do better? I think so.
What have have retrieved are Events
that have been hand crafted by the contract engineers. They are designed to fire off
when very important things happen. It is convention to use events to keep track of
things, and they are often specified.
Just take a look at the ERC-20 token standard. Under the events headig, It states that:
Transfer
MUST trigger when tokens are transferred, including zero value transfers.
A token contract which creates new tokens SHOULD trigger a Transfer event with the _from address set to 0x0 when tokens are created.
event Transfer(address indexed _from, address indexed _to, uint256 _value)
So events are going to be useful, and are probably at the right level that a user would be interested in. Something like: “You have been transferred x of y tokens from z address” is the goal here.
If standardised contracts MUST emit certain events, and we can access those events, then that is a reliable way to examine one aspect of chain history.
The LogJogger
The first useful thing that a user could want in a transaction is knowing the name of the events.
Keeping in mind their address may be the initiator, recipient, or some strange by-product during EVM execution.
So let us look at the first log
, and examine the first element in topics
. The value
in topic[0]
is always reserved for the “event signature”. It is the hash of the
string of the event.
Transaction logs: [
Log {
address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2,
topics: [
0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c,
0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d,
],
This is wonderful. Usually people only refer to these by the first four bytes:
0xe1fffcc4
The log also contains a contract address 0xc02a...6cc2
. This is the contract that
emitted the event in the log! This is the next piece of the puzzle.
The CodeRunner
We now call our node again with the contract address from any log we get
eth_getCode(contract_address)
That returns a blob of bytecode. This code is the “program for the transaction”, but the solidity compiler automatically includes additional information. This information is metadata that is tacked onto the end. Best seen in this playground (https://playground.sourcify.dev/) created by Sourcify.
What if we could snip off that metadata and go hunting for the contract? It might contain more useful information for us.
The SeaBorer
So by reading the length of the metadata, then snipping it off, we have the Concise Binary Object Representation (CBOR) which potentially has an identifier we can use to look for the contract ABI. For example an Interplanetary File System (IPFS) Content Identifier (CID).
Here’s a little test function that does that.
fn cid_extraction() {
let test_metadata = "a2646970667358221220c019e4614043d8adc295c3046ba5142c603ab309adeef171f330c51c38f1498964736f6c6343000804";
let bytes = hex::decode(test_metadata).unwrap();
let cid = ipfs_cid_from_metadata(&bytes).unwrap();
assert_eq!(
cid,
Some(String::from(
"QmbGXtNqvZYEcbjK6xELyBQGEmzqXPDqyJNoQYjJPrST9S"
))
);
}
CIDv0 all start with “Qm”, so that’s the good stuff.
The Pause
So far we have:
portal_network(0x84)
-> chapter_0x84 (~350MB of the index for address starting with0x84
)chapter_0x84
-> block_num, tx_indexeth_getTransactionByBlockAndIndex(block_num, tx_index)
-> tx_hasheth_getTransactionReceipt(tx_hash)
-> log_topic and contract_addresseth_getCode(contract_address)
-> bytecode, bytecode_metadata
The Interrogation
So a single transaction now can be interrogated against our portal node.
cargo run --example user_2_inspect_transaction_logs
Reveals:
Tx 0x1a8d94dda1694bad33384215bb3dc0a56652b7069c71d2b1afed35b24c9b54df has 5 logs
Contract: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
Topic logged: 0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c
Contract: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
Topic logged: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Contract: 0x106d3c66d22d2dd0446df23d7f5960752994d600
Topic logged: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
IPFS metadata CID: "QmZwxURkw5nD5ZCnrhqLdDFG1G52JYKXoXhvvQV2e6cmMH"
Contract: 0x1636a5dfcf7a21945c06d1bea40b52ce975ea614
Topic logged: 0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1
Contract: 0x1636a5dfcf7a21945c06d1bea40b52ce975ea614
Topic logged: 0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822
Not pretty to the human eye, but it is a start. We have the transactions for a user and some contracts and topics they emit. Plus, look at that nicely decoded IPFS hash! The other contracts did not have one - but I did not try for a swarm hash. There may be other creative ways to get ABIs too…
Missing: Human readable translation of these transactions.
Continue on to poking part 5
References
Ethereum
Portal Network
Lightweight protocol access by resource constrained devices.
https://github.com/ethereum/portal-network-specs/blob/master/README.md
Trin
Trin is a Rust implementation of a Portal Network client.
https://github.com/ethereum/trin
Sourcify
Sourcify enables transparent and human-readable smart contract interactions through automated Solidity contract verification, contract metadata, and NatSpec comments.
IPFS CID
Self-describing content-addressed identifiers for distributed systems.
https://github.com/multiformats/cid
Min-know
An implementation of the address-appearance-index-specs.