Are you a TCR token TCR holder?

Convert 1:1 to TCR token MYC here.

Need more information?
Read the announcement

The Journey of an Ethereum Transaction

Nick Crow, James Olsen, Ben Bassa, Douglas Atherton

 - 12 min read
The Journey of an Ethereum Transaction bannner

Understanding how transactions are created, communicated across a network, executed, and used to support complex operations is essential to understanding how decentralised finance is possible. Throughout this article the information needed to observe the events and logs emitted by transactions are also shared, allowing for comprehension of what the data that exists on Ethereum is really saying. The life-cycle of a transaction is extensive, with many checks for validation and other processes that must be undergone - all of which is comprehensively explained in the following article.

For a concise explanation of the process of including a transaction on the blockchain, feel free to skip ahead to the ‘The Journey of a Transaction’ section.

Contents:

FAQ
What is an Ethereum Transaction
The Journey of a Transaction
The Block Header
Example Transaction Events
Transaction Receipts and Logs

FAQ

What are Ethereum transactions?

Transactions in Ethereum are similar to those in the traditional banking system. One user can send a transaction, an object containing data, that specifies who they are (the sender), who the receiver is, what asset is being exchanged, and the quantity of said asset being exchanged. This is not the only form a transaction can be used for, but it is the most typical, and it allows for users to tell the network to send assets (such as ETH the currency of Ethereum) from one account to another.

How is a transaction determined to be valid?

There are a variety of steps required in validation of a transaction, but perhaps the most important stage comes when it is decided if a transaction can be admitted to the mempool (a storage for all transactions awaiting addition to a block). Ten criteria must be ticked off, including: observing an authentic signature from sender, checking that the sender has sufficient balance, and seeing that gas use isn’t too high for the transaction. The prior list is not exhaustive (only covering three of ten criteria), but provides an example of the verifications made.

What are Ethereum blocks?

To execute transactions in Ethereum said transactions must be included in a block that is appended to the chain (though it must be noted that a block doesn’t necessarily have to have transactions). Any block, much like transactions, must pass several validation checks to be allowed on the chain. Blocks will often include information about itself in a header (i.e. timestamp, index) and will often require some test of resources possessed by the proposer, either computational or monetary. In this manner a block can be thought of as a container for transactions, an object containing objects that contain data.

Do Ethereum transactions create receipts and logs?

All transactions will produce a transaction receipt object upon their execution that serves as a record of said execution much like a receipt serves as a record of a sale. Some receipts, but not all, will also create transaction logs. These logs are records of important events that were emitted by the execution of a transaction such as a swap between two assets or currencies.

What information does an Ethereum transaction log give?

Transaction logs can contain any data, and do not necessarily need to abide by any standards. The exact information communicated by a log (and if there any logs) can be determined by the creator of the transaction emitting said logs. Said creator may choose to include information about the value of a given currency when a swap was performed for instance. There is however a field in each log that contains a maximum of four elements that can be filled with data, such as the value of a given currency. Users may choose to place their data in this field instead of the variable length field because it is these four elements that are indexed and easily search through at a later date (much like how your OS may index the names or dates of creation for files to be searched, but not their content).

When is a block considered ‘final’?

There is no strict definition as to when a block can be considered final on Ethereum, but it is standard to consider a block final once six blocks have been executed since said block. This standard is dependent on the notion of ‘statistical finality’. To reorganise a chain (replace or reorder blocks in the chain) by a single block, a majority of computational or monetary resources on the network is required. Therefore, at such great expense, it is unlikely any malicious actor will attempt even a single block reorganisation. To reorganise six blocks typically requires a supermajority of resources of >95%, a task that is feasibly expected to be impossible for any collective of malicious actors.

How can an Ethereum smart contract be created?

While transactions are often used as a means to organise the transfer of assets between accounts their applications are far more diverse. Where it is considered that transactions are simply objects that contain data Ethereum has allowed for transactions to include code that can be compiled into smart contracts on the chain. Once a smart contract has been created it operates much like an account allowing further transactions to be sent to it to transfer resources between the contract and other accounts.

How does a node verify the origins of a transaction?

When verifying that a transaction has truly originated from the sender it claims to have been sent by, verifiers must observe how the transaction has been encrypted. Ethereum uses a public-key infrastructure where each account is associated with a public key and a private key (the latter of which isn’t revealed to anyone). Whatever is encrypted with one key can be decrypted with the other, so if an account signs a transaction with their private key (that it assumed to be known by no one else) it can be decrypted with their public key. If decryption with the account’s public key is successful, then it can be assumed that the transaction originated from the claimed sender.

What is an Ethereum Transaction

A transaction is a serialised binary message that encapsulates information that represents a state change on the Ethereum blockchain. Overtime, Ethereum Improvement Proposals (EIPs) have changed what a valid transaction looks like, whilst maintaining a high level of backwards compatibility with other transaction types. Today, the main types of transactions are legacy transactions and typed transactions (i.e. Type 1, Type 2).

NameStructure
Legacyrlp([nonce, gasPrice, gasLimit, to, value, data, init, vrs])
Type 10x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS])
Type 20x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])

As can be seen above, the specific fields that are required for a transaction object to be valid depends on the type of transaction. The primary differences between the legacy transactions and the typed transactions involve the inclusion of: access lists, priority gas fees and max gas fees - the latter two being introduce to help reduce gas costs. Construction of any transaction requires a wallet address or a smart contract to be the creator.

The transaction fields are defined as follows:

nonce

Referred to as the transaction nonce to avoid confusion with the nonce used in a block. It is a number used to indicate how many transactions have been sent by the sender (i.e. each transaction an entity sends will increase their nonce on future transactions by 1). This prevents replay attacks.

gasPrice

The amount of ETH (expressed in Gwei) that each unit of gas costs for the transaction.

gasLimit

The amount of ETH (expressed in Gwei) that an entity is willing to pay to have their transaction included. If the cost of the transaction exceeds this limit, the sender will still be required to pay the gas up to the limit but their transaction will not be included.

to

The address of the destination of the transaction, either of a wallet or a smart contract, in 160-bit form.

value

The amount of ETH (expressed in Wei) that will be transferred from the sender to the recipient. If the transaction is creating a smart contract this will be the balance the contract begins with.

data

If the transaction recipient is a smart contract, this field will be filled with the information necessary to execute code on the smart contract (can be thought of as a function call).

vrs

These values are necessary for the verification of the ECDSA (elliptic curve digital signature algorithm) used by the sending wallet address to ensure that they are who they say they are (similar to an RSA Digital Signature in purpose).

init

If a smart contract is created by this transaction, this is the code that will be used to create said contract.

Most fields described above do not require much intervention by a given user, as most UI’s are designed to abstract away these complex considerations.

All transactions must be signed using the private key of the address constructing the transaction. This private key is one of a pair of keys that are generated when an address is first constructed using Elliptic Curve Cryptography. The vrs field described above allows the address to provide the minimum information required for nodes to verify that only the owner of the private key could have signed the transaction. The exact details of this entire cryptographic process is beyond the scope of this article. It is therefore only necessary to understand that, given current cryptographic assumptions, it is impossible for a valid transaction to be attributed to an address that didn’t sign the transaction.

Encoding

The transaction is broadcast to a node on the Ethereum network - who then in turn broadcast it to their peers (this process is detailed later in this article). To ensure that transactions are propagated across the network without failure, forms of encoding are utilised to reduce the sizes of payloads and to prevent common forms of error (especially those caused by faulty or poor network connections). There are two commonly described forms of such encoding used in Ethereum to varying degrees: RLP and SSZ.

RLP

As defined in the Ethereum Yellow Paper, Recursive Length Prefix (RLP) is a serialisation method used to encode “arbitrarily structured binary data”. It was created specifically for Ethereum, to guarantee that two important characteristics were provided by the serialisation method. These characteristics are “simplicity to implement” and “guaranteed absolute byte-perfect consistency”. RLP provides simplicity by only defining data types of bytes and arrays and leaving higher level protocols to define other data types. RLP is deterministic, by having explicit ordering of items within arrays, to ensure that input data will always result in the same encoding, important for the consistency of hashes.

An example of an RLP encoded legacy transaction.

An example of an RLP encoded legacy transaction.

With knowledge of EIP-2718, the first thing that can be inferred from the transaction above is that this is a legacy transaction, as the first byte 0xf8 is not in the range 0x00 to 0xbf and instead is in the range of 0xc0 to 0xfe. This range is reserved to specify legacy transactions, whereas the remaining range is reserved for typed transactions. Referring to the following list, each byte in hexadecimal form can be decoded to assess the purpose it serves and what the message as a whole might mean.

[0x00, 0x7f]

A byte whose value is in this range becomes its own RLP encoding.

Example: A nonce that is equal to or less than 127 (0x7f). In the example transaction, the 0x13 near the beginning decoded is simply 0x13, or 19 in decimal.

[0x80, 0xb7]

A string (array of bytes) between 0 and 55 bytes long is: 
0x80 + length(string) || string

Example: The decimal gas price value, 50000000000, RLP encoded.

  1. 50000000000 in hexadecimal is 0B A4 3B 74 00
  2. 0x80 + 0x05 = 0x85 || 0ba43b7400
  3. Therefore the RLP encoding is 0x850ba43b7400

This is the RLP encoding of the gas price seen in the example transaction.

[0xb8, 0xbf]

A string (array of bytes) greater than 55 bytes long is: 
0xb7 + length(length(string)) || length(string) || string

Example: A 400 byte long string would have the prefix 0xb90190. It has a length of 190 in hex, which itself is 2 bytes long.  0xb7 + 2 || 0190 || (400 byte long string)

[0xb8, 0xbf]

A string (array of bytes) greater than 55 bytes long is: 
0xb7 + length(length(string)) || length(string) || string

Example: A 400 byte long string would have the prefix 0xb90190. It has a length of 190 in hex, which itself is 2 bytes long.  0xb7 + 2 || 0190 || (400 byte long string)

[0xc0, 0xf7]

An array of strings (list) between 0 and 55 bytes long is:
0xc0 + length(RLP(list)) || RLP(list)

Example: An array that is 10 bytes long (a in hex) would have the prefix 0xc0 + a = 0xca

[0xf8, 0xff]

An array of strings (list) greater than 55 bytes long is: 
0xf7 + length(length(RLP(list))) || length(RLP(list)) || RLP(list)

Example: Starting to decode the example transaction with this in mind, you can see that 0xf8 - 0xf7 = 0x01 = 1 byte. The next byte 6b is therefore the length of the list, 6b = 107 bytes.

SSZ

In contrast SSZ is a more recent adaptation of RLP, generally seen by developers across Ethereum as an improvement. In the consensus layer, all data types are serialized into bytes using a standard format called SimpleSeralize (SSZ).

Compared to the former RLP serialisation format used in the execution layer, SSZ provides two important functions for ease of transmission and storage of data: including encoding/decoding and merkelisation. Additionally, it introduces types for different kinds of data structures. Based on years of experience gained from engineering the Ethereum protocol, developers incorporated the following design goals in SSZ to improve its utility and effectiveness:

  • Simple: All kinds of commonly used primitive data types can be serialised.
  • Bijective: The result of encoding the same value of a single type can only have one representation. Likewise, two different values of different types cannot have the same representation.
  • Compact: For the serialisation of types that are fixed or dynamic in length, the resulting size is relatively compact. The merkelisation of data structures is more efficient than a typical Merkle-Patricia Tree due to a lower branching factor (i.e. more than two branches per a node as is typically seen).
  • Merkle-first: A value of any type has a sound generalized Merkle-root, enabling the creation of flexible proofs that are small in size and avoid merkelisation complexity.
  • Efficient to traverse: By having four-byte offsets, traversal through the fields of the encoded data structure becomes relatively quick.

It can be seen that the simple and bijective properties of SSZ encoding is similar to those of RLP encoding. The primary differences are found to be the way that efficient Merkle Trees are used to store the encoding of any data, meaning significantly less storage is required (at the cost of slightly more computation to be performed).

The SSZ specification defines two kinds of data types, such as basic and composite:

Basic

  • Unsigned integers: Integers of $n$ bytes, where $n$ $∈$ $[ 8, 16, 32, 64, 128, 256 ]$
  • Boolean values: True or false.

Composite

  • Container: An ordered heterogeneous collection of values. E.g. a class containing arbitrary number of properties.
  • Vector: An ordered fixed-length homogenous collection with $n$ values. E.g. an array of $n$ elements.
  • List: An ordered variable-length homogenous collection limited to $n$ values. E.g. a list of $n$ elements.
  • Bit-vector: An ordered fixed-length collection of boolean values, with $n$ bits. E.g. an array of $n$ bit values.
  • Bitlist: An ordered variable-length collection of boolean values, limited to $n$ bits. E.g. a list of $n$ bit values.
  • Union: Contains one of the given subtypes e.g. Union[none, uint64, uint32].

To highlight the distinction between these types, suppose there is a vector type of an array containing $n$ boolean values, and there is a bit-vector type containing $n$ values. While these two types are similar, they will have different representations when serialised. The reason is that each type undergoes a different process for serialising into bytes, hence, conforming to the design goal of being bijective.

The Journey of a transaction

Once a transaction has been constructed - and its validity has been locally verified - the entity that created the transaction will send it to a node on the network. The node that received the transaction will send the transaction to its network of peers.

As the transaction is propagated through the network it will be picked up by nodes who will add it to their list of pending transactions. This list of transactions stored by a node is known as that node’s mempool.

When transactions are in a node’s mempool, they are publicly visible, this is a necessary condition of the peer to peer transaction propagation method Ethereum employs.

A transaction must be validated before it can be added to a node’s mempool. This validation process can differ between implementations, but the algorithms must ultimately comply with the formal rules agreed upon by the network in order to reach consensus across the network. The validation that transactions go through in Geth, before they are included in the mempool is described below.

10 checks for the mempool

  1. The transaction’s signature is correct and valid.
  2. The byte length of the transaction in RLP form doesn’t exceed the maximum size of 128KB (Line 586 & Line 53). This limitation is implemented to prevent DoS attacks.
  3. The value of the transaction cannot be negative. While this cannot happen with RLP, it may occur when using the Geth RPC.
  4. The amount of gas used by the transaction must be less than the current block gas limit. This block gas limit controls the amount of computation that a single block can do. The block gas limit has an average target size of 15M, but can be varied by a miner, within limits (up to a multiple of 2 of the gas target currently).
  5. The transaction gas fee and gas tip is no greater than 256 bits long. This is a sanity check to stop extremely large values from being accepted.
  6. The transaction gas fee is greater than or equal to the gas tip cap.
  7. The amount of gas used by the transaction must be above the minimum set by the node.
  8. The nonce is not in the past. In cryptography, a nonce is an arbitrary number used just once in cryptographic communication. This transaction nonce is the number of transactions sent from your address in sequential order. For instance, 0 is the first position, 1 is the next position and so on and so forth. The node will keep the transaction if the nonce is current, as well as if the nonce is in the future, as a node assumes the transactions have been received out of order and anticipates that the transactions to “fill the gap” will soon arrive.
  9. The address sending the transaction has sufficient balance to cover the fee cost on top of the value transferred. This can be calculated as seen below.

$\mathrm{balance} >= \mathrm{gas \ limit} * \mathrm{gas \ price} + \mathrm{value}$

  1. The transaction has more gas than the intrinsic gas calculated for the transaction. This intrinsic gas is the gas required outside of code execution. This is 21,000 if the transaction is not creating a contract and 53,000 if the transaction is creating a contract plus an additional fee for every byte of data included in the transaction. Non-zero bytes cost 16 gas and zero bytes cost 4 gas. There are also 2 fees associated with access lists, 2400 gas per access list address and 1900 per access list storage key.

Selection by Node

Each node will select transactions from their mempool which will generally be done based on the gas price of each transaction, as the node (acting as a block producer) is looking to maximise the amount that it will get paid for creating a valid block.

Once the node has selected the transactions that it wants to include in its block, it will compile them and validate them to ensure they don't break the rules of the Ethereum network. Following this, the miner will begin the Proof of Work process where they attempt to find a block nonce that hashes to a value below the threshold currently required by the network difficulty.

The reason why this selection process exists for transactions is because of the block 'gas limit' imposed by the Ethereum network. This sets a limit on what the cumulative value of all included transaction gas limit values can be.

Eventually one node will find an appropriate nonce and have found a valid block. The next step is for the miner to propagate the completed block through its network of peers. The nodes that receive the block will check its PoW validity and validate the transactions inside, before adding it to their version of the chain.

The transaction has now been included on Ethereum.

The Block Header

For a given block to be valid, the header of the block must include several fields. These fields are used by miners and the EVM itself to judge the validity of the block. Any transactions included in the block also contribute to the data contained in said fields (which is used to prevent nodes from later claiming that a block contained a transaction it never did).

Building a block involves both a block header and transactions.

Building a block involves both a block header and transactions.

The fields that make up a block are described below:

parentHash

Most commonly referred to as prevHash. This is the 256-bit hash belonging to the parent of the current block.

ommersHash

A 256-bit hash for each of the headers of the (up to) two ommer/uncle blocks referred to by the current block.

beneficiary

A 160-bit address that all transaction fees will be dedicated to, given that it is a) mined/validated, or b) referred to as an ommer/uncle block.

stateRoot

  • Head node of the state trie (otherwise referred to as the world state trie) which keeps track of all accounts (e.g. wallet addresses) and their state after all transactions in and preceding the current block have been executed. Is stored in 256-bits by use of a Merkle-Patricia tree that can consolidate all this information into one root.

transactionsRoot

The head node of the transaction trie which keeps track of all transactions in the current block. It is stored in 256-bits by use of a Merkle-Patricia tree that can consolidate all information into one root.

receiptsRoot

The head node of the transaction receipt trie (otherwise referred to as the receipt trie) which keeps track of all outcomes of the execution of transactions (e.g. which accounts have had changes in balance) within the current block. Is stored in 256-bits by use of a Merkle-Patricia tree that can consolidate all information into one root.

logsBloom

Information about the transactions stored within a block in a manner that can quickly be searched through (i.e. to discover if there are any transactions of interest within a block) while requiring only 256-bits worth of space. Relies on the bloom filters data structure.

difficulty

A scalar value representing the difficulty of mining the current block - dependent on the difficulty of the parent block and the timestamp. Often used to indicate the number of "leading zeros" that must be found in the current block's hash to be valid.

number

Count of ancestor blocks for the current block.

gasLimit

The limit of the total gas that may be used by the current block.

gasUsed

The total gas that was used by the current block.

timestamp

A reasonable estimation of the time that the block was created using the output of the UNIX time() function.

extraData

A field that can be used by the creator of the current block to include information relevant to said block; up to a maximum of 256-bits in size.

mixHash

Most commonly referred to as the hash. The 256-bit hash belonging to the current block. Must meet the difficulty constraints of the current block to ensure that the block is valid (i.e. this proves that the sufficient "work" has been done).

nonce

Value chosen by a miner to attempt to modify the mixHash to meet the difficulty constraints of the current block; represented with 64-bits.

Statistical Finality

The journey of a transaction theoretically never ends, as the decentralised nature of the blockchain means there can never be certainty that a transaction or block is valid. Instead of a centralised authority dictating the validity of certain actions, the blockchain uses a method called statistical finality to judge when a transaction can be considered final. In general, an Ethereum transaction can be considered final after six blocks (~1-2 minutes). Most nodes consider that an appropriate amount of work has been performed by this point, and as such that there is a statistically insignificant chance that the transaction is invalid.

Example Transaction Events

When recording the journey of a transaction, specific events and occurrences can be observed. Many of these events follow similar patterns of operations, but are beyond the technical specifications of Ethereum and must be described separately. Examples of these patterns and interactions with accounts such as smart contracts are detailed in the following sections.

Answers lie in transaction data, and the subsequent events and logs generated. To simplify, an initial summary of the swap of one token for another is provided; more specifically coinA tokens to coinB tokens.

Simplified Swap

As an additional step of specification, most interactions with a swap smart contract will instead be an interaction with a router contract that directs transactions along the most liquid pathways. As a result the example transaction will be routed through an additional smart contract to represent this common instance of reality. The end result remains identical though.

It is not often distinctly clear how the end result is arrived at - the answer lies in the input 'data' field mentioned in the payload of the transaction section. This data stipulates the function to be called and provides the arguments needed by the contract to complete that function. An example of the (RLP encoded) data field from a transaction is provided below.

code block 1

The first 4 bytes (8 hexadecimals) is the function identifier - describing the function to be called from the smart contract. Following that every 32 bytes (64 hexadecimals) is a separate string representing the function arguments, also known as input data. In the below image these characteristics can be observed with a MethodID (the function identifier) and nine arguments required by that function.

code block 2

For individuals to understand what functions to call and which arguments to provide, they must refer to the smart contract’s ABI, or Application Binary Interface. This is a specification that smart contracts must make available in order to allow other smart contracts or wallet addresses to know how to interact with them. For instance the MethodID is the Keccak-256 output of the function name and its parameters’ types. If the function signature is displayed in ASCII format, removing any spaces and argument names (leaving only the data types of said arguments), and the Keccack-256 hash is found of this modified signature the first 4 bytes of this output becomes the MethodID.

keccack256('swapExactTokensForTokens(uint256,uint256,address[],address,uint256)');
// Output: 38ed1739ee07daf49933f1800d1a9bc8d39a6876ea11e643f9c4c39c66df0ee8

It can be observed that the MethodID indeed does match the first 4 bytes of the output of the hash above. Any function that takes upon a different name, takes a different number of arguments, or takes arguments with different data types, will produce a different MethodID - ensuring that there is no confusion about which function a given user intends to call. In languages such as Solidity any function with an identical signature will cause an error or lead to one overwriting the other (in other words there can never be two functions in a smart contract with an identical signature).

Taking the unmodified signature with the names of the arguments included for the example function, the arguments that the user passed to this method in the original transaction can be explained. There is no specific standards for the naming of arguments in Solidity, so smart contracts that are interacted with outside of this example may use the same arguments for different purposes than those described below.

// Swaps coinA tokens for coinB tokens
swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline);

amountIn

Describes the amount of coinA being sent by the user which will be swapped for the appropriate amount of coinB.

// 0x4a817c800 == 20000000000

amountOutMin

The minimum amount of coinB the user is expecting to receive without the transaction reverting and returning all of the USDT.

// 0xa6d80ac3f321453784de9ca1 == 51635690445679324510067530913

path

As as array data type, this argument details what position the array containing the data intended for this argument begins (where the position is indicated in bytes since the beginning of the arguments list).

// 0xa0 == 160

to

Address in 160 bit (20 byte) format of the account that the coinB will be sent to in exchange for the USDT.

deadline

Time in milliseconds specified by the UNIX time() function before the transaction reverts if not yet finalised.

// 0x62048815 == 1644464149

Despite covering all the arguments required for the described function, there still remains four arguments that were provided in the original transaction that have not been detailed yet. It can be noted that these remaining arguments begin at byte position 160 as previously identified in the to argument. The first of these arguments specify the number of array arguments provided (in this case 3). The remaining three arguments (as expected following the last argument) are all 160 bit (20 byte) addresses specifying the accounts that form the pathway that the swap of coinA for coinB will take.

Data limitations in transactions

While there is no hard limit on the amount of information that can be contained in the data field of a transaction, there is a soft limit in the form of the block’s gas limit. Each zero byte of data (0x00) consumes 4 gas, and each non-zero byte of data consumes 16 gas.

Actioning swap

It can be explained that the example function being discussed above was an example of what a Uniswap router contract may be and the transaction describes what said function may be receiving. More specifically this function was identified from a Uniswap V2 router contract, though it would likely be similar for similar protocols. The function called that is called will take the coinA as specified by the user sending the transaction, performing multiple transfers/swaps along the path specified, and return the coinB token to the provided address.

The 'swapExactTokensForTokens()' function will look at these token addresses and determine the optimal route among Uniswap's trading pools in order to receive those tokens in that order. It is likely that some smart contract identified that the most optimal pathway (often the most liquid pathway) to transfer coinA to coinB was to transfer it through Wrapped Ethereum.

Slippage

The final thing to note is that the user specified a minimum amount of tokens they would be expecting to receive in the amountOutMin argument. If the end transaction resulted in less tokens than this, then the transaction would revert and return all coinA back to the user.

Transaction Receipts and Logs

The actions that smart contracts make in fulfilling their purpose leaves traces that can be analysed. Such analysis is often conducted in the form of transaction receipt event logs. These logs are emitted onto the blockchain as a record of all EVM interactions a broader transaction will cause. It can be noted that it is these transaction receipts that are in fact aggregated in a Merkle-Patricia Trie to form a root known as the receiptsRoot, which is recorded in the header of the block. The logs contained within these transaction receipts are also themselves aggregated using a bloom filter data structure to make them easier to search through (as each transaction receipt could have many logs). Observing once more the original transaction described above, one of the logs emitted by this transaction will be explored.

All transaction receipts contain the following fields to describe themselves in a similar form to the fields contained by transactions and blocks described previously.

status

A boolean value indicating if the transaction executed.

transactionHash

The hash of the transaction the receipt describes, 256 bits (32 bytes) in length.

transactionIndex

Equal to the transactionNonce of the transaction that the receipt describes.

blockHash

Equal to the mixedHash of the block that the transaction is located in.

blockNumber

Equal to the number of the block that the transaction is located in.

contractAddress

If the transaction deployed a smart contract this field describes the address said smart contract now exists at, 160 bits (20 bytes) in length.

gasUsed

Total gas required by execution of transaction, and as such the amount of gas to be paid to any miners of the block.

cumulativeGasUsed

Total gas required of the block transaction is contained within up to the point of execution of said transaction.

from

The address of the account that sent the transaction the receipt describes, 160 bits (20 bytes) in length.

to

Equal to the to of the transaction the receipt describes.

root

Can be envisioned as the stateRoot of the block up to the point of the described transaction, 256 bits (32 bytes) in length.

logsBloom

Much like the logBloom for a block, but it only allows for easier querying of logs associated with the described transaction, 256 bits (32 bytes) in length.

logs

An array containing all log objects describing said transaction.

These log objects are particularly important to track, as they often reveal additional information about complex transactions. Using the logsBloom field nodes can query details about transactions quickly without necessarily having to unload every log object one by one. In the example transaction seven log objects were generated, which will be detailed more in the next subsection. The fields these logs contain, and as such the data that can be queried from them, are described in the following.

address

The to field of the transaction receipt said log object is contained within.

blockHash

The blockHash of the transaction receipt that said log object is contained within.

blockNumber

The blockNumber of the transaction receipt that said log object is contained within.

data

Represented as a scalar value and serves no direct purpose apart from allowing for the inclusion of additional data describing said log object (though this field cannot be searched through using the log bloom data structure in the transaction receipt).

logIndex

Describes the ordering of all log objects within the transactions of a particular block.

removed

Boolean value describing if the log object remains on the canonical chain or not (i.e. if the block it is contained within has not become orphaned, or the transaction has not been reverted).

topics

Similar to the data field but is only capable of holding up to four array elements each of 256 bits (32 bytes) length and additionally only allows for querying using the log bloom data structure.

transactionHash

The transactionHash of the transaction receipt that said log object is contained within.

transactionIndex

The transactionIndex of the transaction receipt that said log object is contained within.

id

An identification of the log object itself in the format of “log_” followed by four bytes.

The topics field is often where the most queried data will be stored. Up to four array elements are allowed in this field, and the first element will usually be occupied with the hash of the function that emitted the log. The remaining elements are typically arguments that were provided to the function that emitted the log, assuming that these arguments were listed as “indexed” in the function signature. Storing data in the topics field instead of the data field allows it to be queried, but it is significantly more expensive from a gas costs perspective and has a limit on the total data that can be stored.

Ordering

Each of the seven logs generated by the example transaction are referred to in the following section to illustrate the process the UniSwap V2 router contract takes to perform the functionality requested by the user. The functionality to be precise is to transfer coinA for coinB.

  1. Transfer (Initiator -> coinA/WETH pool)
  2. Transfer (coinA/WETH pool -> WETH/coinB pool)
  3. Sync (coinA/WETH pool)
  4. Swap (coinA/WETH pool)
  5. Transfer (WETH/coinB pool -> Final Recipient)
  6. Sync (WETH/coinB pool)
  7. Swap (WETH/coinB pool)

Ordering of the log events are not necessarily logical or chronological, and are instead based on how exactly the functions called by the router contract emit events. Expanding upon the information above into terms more understandable, the reason for this particular ordering becomes clearer.

  1. The initial transfer of tokens from the swap initiator to the pool.
  2. The transfer of funds to the exit address.
  3. The sync log, this is a Uniswap function that aligns the 'on paper' reserves held by the contract in storage with its actual reserves.
  4. The swap log, this is emitted once the above computations have been completed as it is declaring the completion of the entire swap function.

An example of the consequences of this is that the 'sync' log actually marks the token balances after the swap has been conducted.

Understanding event logs

These logs can only be interpreted with the help of the contract ABI - for most events however this is not an issue because of the establishment of different ERC standards. The ERC-20 token standard describes the common event types of 'Transfer' and 'Approve' while the most significant events caused by other contracts like DEX's publish their ABI's in order to facilitate other contracts interacting with them. In fact since a lot of decentralised exchanges are forks of larger applications like Uniswap there exists more or less a common ABI across many smart contracts to interpret their events. However, there exists bespoke contracts like MEV bots that often do not publish their ABI.

Understanding bespoke smart contracts

It is impossible to determine function signatures, and as such which functions to call and which arguments to provide, without the ABI for a given smart contract. There are however less deterministic ways a user may be able to guess the existence of function signatures, and as such interact with the smart contract themselves.

All smart contracts can emit event logs, therefore by looking at the emissions cause by a transaction a determined actor could interpret the actions of a smart contract by its interactions with other accounts and smart contracts with known ABI's. To expand on this a brief theoretical example is considered. If there exists a smart contract of which its origins (and as such ABI) are unknown, its unique functionality cannot be directly understood in absence of input data or in the case of emitted event logs.

Conclusion

This article has intended to serve as an overview of both the broader transaction process and the deeper transaction details that can be derived from the blockchain. The aim of it was to familiarise individuals with how transaction data that block explorers display was actually derived as well as providing a guide to help design blockchain analytic tools that gain a faster understanding of the data available.

Swaps logo

More Leverage. Less Overhead