Polygon Gateway

Overview

The Polygon gateway enables validators to start receiving blocks and bundles from the Marlin network.

Architecture

 

There are 2 core components:

See here for setup instructions

In addition there are 2 optional components in order to send and receive blocks from the Marlin network. While they aren't needed for MEV extraction, they help in maximizing the time searchers have to create bundles. This increases competition among searchers and results in higher MEV extraction.

Core MEV setup

Step 1: Set up mev-bor in the validator node.

wget http://public.artifacts.marlin.pro/projects/mev-bor/2.0.4/bor-linux_amd64 -O /path/to/local/bor

Step 2: Expose the relevant RPC methods from mev-bor by adding the below cli flags.

--http  # Enable http rpc
--http.addr "0.0.0.0"  # Expose the RPC endpoint outside the instance so the gateway can access it
					   # IMPORTANT: Protect the RPC endpoint from external access using a firewall
                       # Especially if you are exposing the `eth` namespace, be sure to set up the firewall _prior_
                       # Only the gateway IP provided by us should have access to it
--http.api "mev"  # The RPC APIs used by the gateway live in the `mev` namespace, expose them
				  # Can expose other RPCs as per your requirement
--miner.recommit "700ms"  # Reduce the recommit time so blocks with bundles can be generated

Restart the bor service so all the above changes take effect.

Step 3: Contact us to set up a dedicated gateway for you.

Step 4: Add the gateway IP provided by us to your firewall to allow RPC requests from it.

That's it!

Contact us if you would like a hands-on guided walkthrough.

Core MEV setup - Manual gateway

This section assumes that marlinctl is installed already. See here if you have not installed it.

Step 1: Generate a client identity for the gateway by running the below command and entering a passphrase when prompted

sudo marlinctl gateway polygon bor keystore create

Step 2: Set up mev-bor in the validator node.

wget http://public.artifacts.marlin.pro/projects/mev-bor/2.0.4/bor-linux_amd64 -O /path/to/local/bor

Step 3: Expose the RPC methods from mev-bor by adding the below cli flags.

--http  # Enable http rpc
--http.addr "0.0.0.0"  # Expose the RPC endpoint outside the instance so the gateway can access it
					   # IMPORTANT: Protect the RPC endpoint from external access using a firewall
                       # Especially if you are exposing the `eth` namespace, be sure to set up the firewall _prior_
                       # Only the gateway should have access to it
--http.api "mev"  # The RPC APIs used by the gateway live in the `mev` namespace, expose them
				  # Can expose other RPCs as per your requirement
--miner.recommit "700ms"  # Reduce the recommit time so blocks with bundles can be generated

Restart the bor service so all the above changes take effect.

Step 4: Create the gateway, providing the above RPC endpoint (default RPC port in bor is 8545).

sudo marlinctl gateway polygon bor create --bootstrap-addr "127.0.0.1:8002" --spamcheck-addr "rpc_ip:rpc_port" --bundle-addr "http://rpc_ip:rpc_port/"

Step 5: The gateway needs to accept connections on port 18545, ensure it is open to the internet.

That's it!

Contact us if you would like a hands-on guided walkthrough.

Loki testnet

The Loki testnet features a simpler architecture where Marlin runs a mock Heimdall layer with a permissioned validator list which doesn't need the validator to stake any tokens on any chain. This enables the validator to focus their efforts on solely the Bor layer.

This guide assumes you have the bor binary ready to use.

Step 1: Download the genesis file.

wget http://public.artifacts.marlin.pro/projects/mev-bor/1.0.1/genesis.json

Step 2: Initialize bor.

/path/to/bor --datadir /path/to/datadir init genesis.json

Step 3: (Only for mining nodes) Generate a sealing key.

/path/to/bor --datadir /path/to/datadir account new --password /path/to/pass.txt

Step 4: Start bor.

Without mining enabled:

/path/to/bor --datadir /path/to/datadir --syncmode 'full' --networkid 1137 --bor.heimdall 'http://13.57.231.182:8000' --bootnodes "enode://edf8c774a442603c497989529940b92489b98d0382ba6e9a01f5b1587e09f1a9d40faff5396a77e39ffc89ae0a0b424c5e933379f4dc34da41dcc3618e190e20@13.229.200.159:30303"

With mining enabled:

/path/to/bor --datadir /path/to/datadir --syncmode 'full' --networkid 1137 --miner.gaslimit '20000000' --miner.gastarget '20000000' --mine --unlock 0xSEALINGKEY --password /path/to/pass.txt --allow-insecure-unlock --bor.heimdall 'http://13.57.231.182:8000' --bootnodes "enode://edf8c774a442603c497989529940b92489b98d0382ba6e9a01f5b1587e09f1a9d40faff5396a77e39ffc89ae0a0b424c5e933379f4dc34da41dcc3618e190e20@13.229.200.159:30303"

That's it! Once the node is synced, see here to setup the gateway.

Split validator setup

This page describes a split validator setup that minimizes missed checkpoints (and thus maximize staking rewards) and makes running mev-bor virtually risk-free to the point where it might be a good idea to use this setup regardless of Marlin.

A validator node consists of two components, Heimdall and Bor working in tandem. While a validator usually has both of them set up to sign, it is important to note that they are independent layers and can work even when only one of them is signing.

Anatomy of checkpoints

 

 

On receiving a checkpoint, Heimdall communicates with Bor to verify the blocks in the checkpoint and signs it if everything is verified. Note that this process only needs Bor to be receiving blocks, not producing them.

Anatomy of block production

 

Bor continuously communicates with Heimdall to fetch details of the next span and events to commit on-chain. With this data, it can figure out when its turn is going to come and produce valid blocks. Note that this process only needs Heimdall to be reading stake data from Ethereum, not signing/writing checkpoints.

Split setup

The above facts lead naturally to a split validator setup to run mev-bor while not running the risk of missing checkpoints:

 

The first node runs vanilla bor + heimdall in the existing validator setup except, the bor is not creating any blocks. This is sufficient to make Heimdall sign checkpoints and earn staking rewards. The second node runs Heimdall in non-signing mode and mev-bor. This isolates mev-bor from the checkpoint signing node and gives freedom to customize bor without affecting staking rewards.

For searchers

Participating validators

Endpoint

Searchers can use the following relay endpoint for sending bundles:

Searchers will want to run their nodes near Germany to get good latency to the relay and the validator.

mev-bor spec

mev-bor follows the Flashbots v0.3 spec (https://docs.flashbots.net/flashbots-auction/miners/mev-geth-spec/v03-rpc) with the following changes:

mev relay spec

The MEV relay is designed to work with the flashbots bundle formats, RPC calls and the flashbots ethers provider (https://github.com/flashbots/ethers-provider-flashbots-bundle) with the following notable changes:

Note that bundle delivery and inclusion is best effort, and searchers still have to make sure that bundles do not spam the endpoints and are economically enticing for validators to include, moreso than transactions from other bundles and public mempool.

Example usage

import { providers, Wallet } from "ethers";
import { FlashbotsBundleProvider } from "@flashbots/ethers-provider-bundle";

async function main() {
  // Standard json rpc provider directly from ethers.js (NOT Flashbots)
  // create the base provider
  let base = new ethers.providers.JsonRpcProvider({url: "http://<polygonbor-ip>:8545"}, 137)
  await base.ready
  const user = new ethers.Wallet('a2..<privkey>..40', base)
  // wrap it with the marlin relay provider
  let provider = new FlashbotsBundleProvider(base, user, {url: 'http://bor.txrelay.marlin.org/'}, 137)


  const CONTRACT_ADDRESS = "0x0a..<contract-address>"
  const ABI = ["function coinbasetransfer() payable"]
  const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, user)

  const txs = [
    {
      signer: user,
      transaction: await contract.populateTransaction.coinbasetransfer({
        value: ethers.utils.parseEther("0.1"),
        gasPrice: "31000000000",
      })
    },
    {
      signer: user,
      transaction: await contract.populateTransaction.coinbasetransfer({
        value: ethers.utils.parseEther("0.2"),
        gasPrice: "31000000000",
      })
    },
  ];

  const blk = await base.getBlockNumber()

  // send bundle to marlin relay
  const result = await provider.sendBundle(txs, blk + 1);
  console.log(result)
}

main().catch(console.error)