When discussing the best approaches for delivering transactions from searchers to validators (by the way, can we find a common way of referring to both a miner and validator?) for Ethereum 2 Jason Paryani suggested to encrypt the MEV transactions with a validator public key and use the public devp2p (or libp2p or whatever replaces devp2p) to deliver the transactions to recipients.

While the idea of skipping the relay by using public mempool and at the same time ensuring the privacy of communication seemed very tempting there were some questions to ask.

  1. how do we convince devp2p to deliver such transactions?
  2. what format of transactions do we use? would EIP-2718 be required with a new transaction type (this would require an EIP and modification of all of the clients' codebases)
  3. how do we ensure that the validator does not steal the MEV from the transaction

Below I address 1) and 2), presenting devp2p 'carrier transactions'.

(3 has been answered in questions below)

Let us have a look at the part of the current transaction RLP decoder in one of the Eth1 implementations (Nethermind).

Every transaction can have a non-empty Data field which is used for calldata when calling smart contracts or init code when deploying smart contracts.

private static void DecodeEip1559PayloadWithoutSig(
    Transaction transaction, RlpStream rlpStream)
{
    transaction.ChainId = rlpStream.DecodeULong();
    transaction.Nonce = rlpStream.DecodeUInt256();
    transaction.GasPrice = rlpStream.DecodeUInt256(); // gas premium
    transaction.DecodedMaxFeePerGas = rlpStream.DecodeUInt256();
    transaction.GasLimit = rlpStream.DecodeLong();
    transaction.To = rlpStream.DecodeAddress();
    transaction.Value = rlpStream.DecodeUInt256();
    transaction.Data = rlpStream.DecodeByteArray();
    transaction.AccessList = _accessListDecoder.Decode(rlpStream);
}

In the usual mempool processing in Ethereum clients, after the transaction is decoded, it is added to the mempool and then some basic checks can be done, like:

Generally, clients will not distribute transactions that do not satisfy all of these requirements but will deliver transaction that look correct and have high enough gas price to be closer to the top of the mempool (are likely to be included in some blocks).

Some of the fields above can be critical for recognizing / detecting MEV attempts → To, Value, Data fields may carry information that would allow to recognize the MEV opportunity. We may want to encrypt these fields to avoid detection of the MEV opportunity by mempool analysers (remember that we assume now that we use the public devp2p and not a relay).

To encrypt some of the fields only we would need a new transaction type conformant with the EIP-2718 requirements.

We could imagine that a transaction like this would only reveal its signature to the validator (signature would be encrypted).

Possible problems with this solution:

Now let us propose a carrier transaction construction. It is called a carrier because it masks itself as a totally normal transaction that is happily distributed by the devp2p while in fact the normal transaction is just a carrier of the true MEV-bundle DNA encrypted using ECIES or other scheme.

private static void DecodeEip1559PayloadWithoutSig(
    Transaction transaction, RlpStream rlpStream)
{
    transaction.ChainId = rlpStream.DecodeULong();
    transaction.Nonce = rlpStream.DecodeUInt256();
    transaction.GasPrice = rlpStream.DecodeUInt256();
    transaction.DecodedMaxFeePerGas = rlpStream.DecodeUInt256();
    transaction.GasLimit = rlpStream.DecodeLong();
    [transaction.To](<http://transaction.to/>) = rlpStream.DecodeAddress();
    transaction.Value = rlpStream.DecodeUInt256();
    // data is MEV_Prefix|val_pub_key|Encrypt(val_pub_key, mev_bundle))
    // MEV_Prefix to quickly recognize the encrypted MEV content
    // val_pub_keyis the public key of the target validator so there is no
    // unnecessary decryption happening
    // transaction is a totally normal transaction with a valid nonce, signature, etc
    // transaction has an MEV opportunity that can only be decrypted by the validator
    transaction.Data = rlpStream.DecodeByteArray();
    transaction.AccessList = _accessListDecoder.Decode(rlpStream);
}

The carrier transaction should be executable - with a valid nonce and gas price so it is possibly included in one of the previous blocks - such transaction will be distributed in mempools just fine and sometimes (not always) included in one of the preceding blocks. In such case, the higher the gas premium, the more likely the transaction would be distributed and included in the block. This is preferable solution as the searcher pays not only for the MEV extracted but also the 'delivery fee'

Now, as long as the transaction gets delivered to validator (whether it was included in one of the previous blocks or just stuck in the mempool) validator is able to decrypt it and execute when their turn arrives.


Carrier transaction initially is created by constructing it inside an ethers.js wrapper for flashbots:

https://github.com/flashbots/ethers-provider-flashbots-bundle

We are leaving it to the implementer to verify whether a new provider is needed or the existing provider should be extended.

Flashbots provider should allow to specify the carrier transaction information (sender, signature) and be able to encrypt the calldata with a provided validator public key using ECIES on secp256k1.

Calldata should be a bundle serialized using RLP

https://eth.wiki/fundamentals/rlp

You do not have to write RLP code, you can use modules / libraries: https://www.npmjs.com/package/@ethersproject/rlp

Some prototype example can be found here: https://github.com/NethermindEth/nethermind/blob/e0e5d47c477988f11588b8247b840c1532633f76/src/Nethermind/Nethermind.Mev/MevRpcModule.cs#L130


In both cases the implementation is similar. Whenever a transaction is added to the mempool, if such transaction has a MEV-prefix in the data field (calldata) then the internal carrier transactions decoder should use the assigned validator private key to decrypt the bundle and then add the bundle to the bundles list alongside any bundles received via JSON RPC.

It is also possible that a transaction has been included in a block but never reached the current node mempool (very unlikely scenario, but possible if the node was offline during the last few minutes). In such cases we may also analyse the processed blocks for any transaction that has a MEV prefix and similarly to the mempool. decrypt the bundles from the calldata add them to the bundles list / pool.

(Nethermind prototype can be found here: https://github.com/NethermindEth/nethermind/tree/carriers inside the MEV plugin inside classes like NutCracker)