├── .eslintrc ├── .eslintrc.yml ├── .gitignore ├── .prettierrc ├── README.md ├── genesis.json ├── package.json ├── peer_nodes.sh ├── run.sh ├── sample_keystore_file ├── scripts ├── 1559-contract-bribe-demo.js ├── 1559-demo.js ├── contract-bribe-demo.ts ├── demo.ts ├── e2e-reverting-bundles.js ├── e2e-reverting-megabundle.js ├── helpers.js ├── private-tx-demo.js ├── ws-contract-bribe-demo.js └── ws-demo.js ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["prettier", "standard"], 4 | "plugins": ["prettier"], 5 | "rules": { 6 | "prettier/prettier": ["error"], 7 | "space-before-function-paren": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | es2021: true 5 | extends: eslint:recommended 6 | parserOptions: 7 | ecmaVersion: latest 8 | rules: {} 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.DS_Store 2 | node_modules/ 3 | build/ 4 | datadir/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MEV GETH Demo 2 | 3 | Launches an MEV GETH node, and shows how a miner may profit from it by accepting MEV 4 | bundles either via direct `block.coinbase` smart contract "bribes", or with extra transactions that pay 5 | the block's coinbase if it's known ahead of time. 6 | 7 | ## Quickstart 8 | 9 | ``` 10 | git clone https://github.com/flashbots/mev-geth 11 | cd mev-geth && make geth && cd .. 12 | git clone https://github.com/flashbots/mev-geth-demo 13 | cd mev-geth-demo 14 | yarn 15 | GETH=../mev-geth/build/bin/geth ./run.sh 16 | yarn run demo-simple 17 | yarn run demo-contract 18 | ``` 19 | 20 | ## Bundle Submission 21 | 22 | ### Direct miner transfer 23 | 24 | ```javascript 25 | import { ethers } from 'ethers' 26 | import { FlashbotsBundleProvider } from "ethers-flashbots"; 27 | 28 | // create the base provider 29 | let base = new ethers.providers.JsonRpcProvider("http://localhost:8545") 30 | // wrap it with the mev-geth provider, the Flashbots MEV-GETH node can be on a different host/port 31 | let provider = new FlashbotsBundleProvider(base, "http://mev-geth-api.com") 32 | 33 | const user = ethers.Wallet.createRandom().connect(provider) 34 | const nonce = await user.getTransactionCount() 35 | 36 | const COINBASE_ADDRESS = "0x2222222222222222222222222222222222222222" 37 | const bribe = ethers.utils.parseEther('0.042') 38 | const txs = [ 39 | { 40 | signer: user, 41 | transaction: { 42 | to: "0x1111111111111111111111111111111111111111", 43 | value: ethers.utils.parseEther('0.1'), 44 | nonce: nonce, 45 | }, 46 | }, 47 | { 48 | signer: user, 49 | transaction: { 50 | // The coinbase address of the mining pool of your choice 51 | to: COINBASE_ADDRESS, 52 | value: bribe, 53 | nonce: nonce + 1, 54 | } 55 | }, 56 | ] 57 | 58 | const blk = await provider.getBlockNumber() 59 | // `result` contains the tx hashes for all txs in the bundle 60 | const result = await provider.sendBundle(txs, blk + 1); 61 | await result.wait() 62 | ``` 63 | 64 | ### Contract Transfer 65 | 66 | ```javascript 67 | import { ethers } from 'ethers' 68 | import { FlashbotsBundleProvider } from "ethers-flashbots"; 69 | 70 | // create the base provider 71 | let base = new ethers.providers.JsonRpcProvider("http://localhost:8545") 72 | // wrap it with the mev-geth provider, the Flashbots MEV-GETH node can be on a different host/port 73 | let provider = new FlashbotsBundleProvider(base, "http://mev-geth-api.com") 74 | const user = ethers.Wallet.createRandom().connect(provider) 75 | 76 | // We assume the following contract is deployed: 77 | // 78 | // contract Bribe { 79 | // function bribe() payable public { 80 | // // do whatever else you want here. 81 | // block.coinbase.transfer(msg.value); 82 | // } 83 | // } 84 | const ADDRESS = "0x1111111111111111111111111111111111111111" 85 | const ABI = ["function bribe() payable"] 86 | const contract = new ethers.Contract(ADDRESS, ABI, user) 87 | 88 | const txs = [ 89 | { 90 | signer: user, 91 | transaction: await contract.populateTransaction.bribe({ 92 | value: ethers.utils.parseEther("0.216321768999"), 93 | }) 94 | }, 95 | ]; 96 | 97 | const blk = await provider.getBlockNumber() 98 | // `result` contains the tx hashes for all txs in the bundle 99 | const result = await provider.sendBundle(txs, blk + 1); 100 | await result.wait() 101 | ``` 102 | 103 | ## Expected Outputs 104 | 105 | The scripts should give you the following outputs (re-run if they fail, the test may be flaky due to timing): 106 | 107 | ### Simple 108 | 109 | ``` 110 | yarn run demo-simple 111 | yarn run v1.22.4 112 | $ ts-node scripts/demo.ts 113 | Faucet 0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8 114 | Funding account...this may take a while due to DAG generation in the PoW testnet 115 | OK 116 | Balance: 1000000000000000000 117 | Submitting bundle 118 | null 119 | { 120 | minimumNonceByAccount: { '0x203f54b5F444552447aC71e26EB5AC3f5e3dfaC9': 1 } 121 | } 122 | blockNumber: 16 123 | blockNumber: 17 124 | blockNumber: 18 125 | blockNumber: 19 126 | blockNumber: 20 127 | blockNumber: 21 128 | Bundle mined 129 | Transaction mined { 130 | to: '0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8', 131 | from: '0x203f54b5F444552447aC71e26EB5AC3f5e3dfaC9', 132 | contractAddress: null, 133 | transactionIndex: 1, 134 | gasUsed: BigNumber { _hex: '0x5208', _isBigNumber: true }, 135 | logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 136 | blockHash: '0x746e55600e4c8e99d086c9437a2029ddb5977c386cc9638de1e7734fe932108c', 137 | transactionHash: '0x2d78109fb01f205685049c5870d5ff5ccc3e7059757cc31a113ae23d5a0e692a', 138 | logs: [], 139 | blockNumber: 17, 140 | confirmations: 10, 141 | cumulativeGasUsed: BigNumber { _hex: '0xa410', _isBigNumber: true }, 142 | status: 1, 143 | byzantium: true 144 | } 145 | Miner before 1031000000000000000000 146 | Miner after 1033066666666660000000 147 | Profit (ETH) 0.06666666666 148 | Profit equals bribe? true 149 | ✨ Done in 15.48s. 150 | ``` 151 | 152 | ### Contract 153 | 154 | ``` 155 | yarn run demo-contract <<< 156 | yarn run v1.22.4 157 | $ ts-node scripts/contract-bribe-demo.ts 158 | Funding account...this may take a while due to DAG generation in the PoW testnet 159 | Deploying bribe contract... 160 | Deployed at: 0x8A7946D23E5096E8d7C81327d4608454B9c5CF8b 161 | Submitting bundle 162 | null 163 | { 164 | minimumNonceByAccount: { '0x23C9f032F8763a884e6ce6df838ebf5aEdc4B236': 1 } 165 | } 166 | blockNumber: 90 167 | blockNumber: 91 168 | blockNumber: 92 169 | blockNumber: 93 170 | blockNumber: 94 171 | Bundle mined 172 | Transaction mined { 173 | to: '0x8A7946D23E5096E8d7C81327d4608454B9c5CF8b', 174 | from: '0x23C9f032F8763a884e6ce6df838ebf5aEdc4B236', 175 | contractAddress: null, 176 | transactionIndex: 0, 177 | gasUsed: BigNumber { _hex: '0x70c8', _isBigNumber: true }, 178 | logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 179 | blockHash: '0x4e9a4c65b650dafaf4fb11856bbc6b74eb1050cba719d7a6d50518429035feb8', 180 | transactionHash: '0x7f9f615ec4dd49131d9d7722b036c7d7bf506983138e063a762a656b7e4c4346', 181 | logs: [], 182 | blockNumber: 91, 183 | confirmations: 9, 184 | cumulativeGasUsed: BigNumber { _hex: '0x70c8', _isBigNumber: true }, 185 | status: 1, 186 | byzantium: true 187 | } 188 | Miner before 1177066666666660000000 189 | Miner after 1179282988435659000000 190 | Profit (ETH) 0.216321768999 191 | Profit equals bribe? true 192 | ✨ Done in 24.23s. 193 | ``` 194 | -------------------------------------------------------------------------------- /genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config":{ 3 | "chainId":5465, 4 | "homesteadBlock":0, 5 | "byzantiumBlock":0, 6 | "constantinopleBlock":0, 7 | "petersburgBlock":0, 8 | "eip150Block":0, 9 | "eip155Block":0, 10 | "eip158Block":0, 11 | "eip160Block":0, 12 | "berlinBlock":0, 13 | "istanbulBlock":0, 14 | "MuirGlacierBlock":0, 15 | "londonBlock":0 16 | }, 17 | "nonce":"0x0012231231123042", 18 | "alloc":{ 19 | "0xd912aecb07e9f4e1ea8e6b4779e7fb6aa1c3e4d8":{ 20 | "balance":"100000000000000000000000" 21 | } 22 | }, 23 | "timestamp":"0x00", 24 | "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", 25 | "extraData":"0x353531111111AAAAAAAAAA352089901535353535353535353535353535353535", 26 | "gasLimit":"0x1000000", 27 | "difficulty":"0x493E0", 28 | "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000" 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flashbots-testnet", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:gakonst/eth-testnet.git", 6 | "author": "Georgios Konstantopoulos ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@ethereumjs/common": "^2.3.1", 10 | "@ethereumjs/tx": "^3.2.1", 11 | "@flashbots/ethers-provider-bundle": "^0.3.1", 12 | "@types/node": "^14.14.10", 13 | "ethereumjs-util": "^7.0.10", 14 | "ethers": "^5.0.23", 15 | "lodash": "^4.17.21", 16 | "node-fetch": "^2.6.7", 17 | "solc": "^0.7.5", 18 | "web3": "^1.3.6", 19 | "ws": "^7.5.0" 20 | }, 21 | "scripts": { 22 | "lint": "eslint .", 23 | "demo-simple-old": "ts-node scripts/demo.ts", 24 | "demo-contract-old": "ts-node scripts/contract-bribe-demo.ts", 25 | "demo-ws-simple": "node scripts/ws-demo.js", 26 | "demo-ws-contract": "node scripts/ws-contract-bribe-demo.js", 27 | "demo-simple": "node scripts/1559-demo.js", 28 | "demo-contract": "node scripts/1559-contract-bribe-demo.js", 29 | "demo-private-tx": "node scripts/private-tx-demo.js", 30 | "e2e-reverting-bundles": "node scripts/e2e-reverting-bundles.js", 31 | "e2e-reverting-megabundle": "node scripts/e2e-reverting-megabundle.js" 32 | }, 33 | "devDependencies": { 34 | "@types/node-fetch": "^2.5.10", 35 | "eslint": "^8.11.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /peer_nodes.sh: -------------------------------------------------------------------------------- 1 | n1_enode=`$GETH --datadir $DATADIR1 attach --exec "admin.nodeInfo.enode"` 2 | $GETH --datadir $DATADIR2 attach --exec "admin.addPeer($n1_enode)" 3 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | P2P_PORT="${P2P_PORT:-30301}" 2 | DATADIR="${DATADIR:-datadir}" 3 | HTTP_PORT="${HTTP_PORT:-8545}" 4 | 5 | MINER_ARGS="${MINER_ARGS:---miner.etherbase=0xd912aecb07e9f4e1ea8e6b4779e7fb6aa1c3e4d8 --miner.trustedrelays=0xfb11e78C4DaFec86237c2862441817701fdf197F --mine --miner.threads=2}" 6 | 7 | rm -rf $DATADIR 8 | 9 | $GETH init --datadir $DATADIR genesis.json 10 | $GETH --port $P2P_PORT --nodiscover --networkid 1234 --datadir $DATADIR --http --http.port $HTTP_PORT --http.api debug,personal,eth,net,web3,txpool,admin,miner $MINER_ARGS 11 | -------------------------------------------------------------------------------- /sample_keystore_file: -------------------------------------------------------------------------------- 1 | {"address":"908e8902bd2018d3bf4d5a0fb42a457e1e8f1a6e","crypto":{"cipher":"aes-128-ctr","ciphertext":"29a1fce09e2078467819f816172c7a0f2383522082aae26f3a3d6e83fff8e4c9","cipherparams":{"iv":"ca7d03430aeee9b29437e68cf399fe9c"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"bfb5a0f6f0374be1bb1f7c1afe5509fcf7f95da062cf8664c5593060a566b6a1"},"mac":"9903c30c52de40b11f4703695b3c00b512c1ed1188936c78ded507f104b0f544"},"id":"57940e74-0d0b-445f-9961-d8f6c63128c1","version":3} -------------------------------------------------------------------------------- /scripts/1559-contract-bribe-demo.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const fetch = require('node-fetch') 3 | const {signEIP1559Tx, generateRelaySignature} = require('./helpers') 4 | const ethers =require("ethers") 5 | const ethUtil = require('ethereumjs-util') 6 | const ContractFactory = require("ethers").ContractFactory 7 | const _ = require("lodash") 8 | const solc = require('solc') 9 | 10 | const localRPC = "http://localhost:8545/" 11 | const client = new Web3(new Web3.providers.HttpProvider(localRPC)) 12 | var BN = client.utils.BN 13 | const FAUCET_ADDRESS = '0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8' 14 | const testWallet = client.eth.accounts.create(); 15 | const TRUSTED_RELAY_PK = '0ceb0619ccbb1092e3d0e3874e4582abe5f9518262e465575ca837a7dad0703d' // 0xfb11e78C4DaFec86237c2862441817701fdf197F, see run.sh 16 | 17 | const CONTRACT = ` 18 | // SPDX-License-Identifier: UNLICENSED 19 | pragma solidity ^0.7.0; 20 | 21 | contract Bribe { 22 | function bribe() payable public { 23 | block.coinbase.transfer(msg.value); 24 | } 25 | } 26 | ` 27 | const INPUT = { 28 | language: 'Solidity', 29 | sources: { 30 | 'Bribe.sol': { 31 | content: CONTRACT 32 | } 33 | }, 34 | settings: { 35 | outputSelection: { 36 | '*': { 37 | '*': ['*'] 38 | } 39 | } 40 | } 41 | } 42 | const OUTPUT = JSON.parse(solc.compile(JSON.stringify(INPUT))) 43 | const COMPILED = OUTPUT.contracts['Bribe.sol'] 44 | const ABI = COMPILED.Bribe.abi 45 | const BIN = '0x' + COMPILED.Bribe.evm.bytecode.object 46 | // miner pk on the private network 47 | const FAUCET_PK = "0x133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791" 48 | // const DUMMY_RECEIVER = "0x1111111111111111111111111111111111111111" // address we'll send funds via bundles 49 | const simpleProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") 50 | const faucet = new ethers.Wallet(FAUCET_PK, simpleProvider) 51 | // we create a random user who will submit bundles 52 | const user = ethers.Wallet.createRandom().connect(simpleProvider) 53 | const MINER_BRIBE = 0.123 * 10 ** 18 54 | const BLOCK_REWARD = 2 * 10 ** 18 55 | 56 | // we use the miner as a faucet for testing 57 | 58 | 59 | const checkMegabundleStatus = async (hash) => { 60 | var timer = setInterval(async function() { 61 | const receipt = await client.eth.getTransactionReceipt(hash) 62 | if(receipt){ // If the tx has been mined, it returns null if pending 63 | clearInterval(timer) // stop the setInterval once we get a valid receipt 64 | const block = receipt.blockNumber 65 | // Given the base fee is burnt and priority fee is set to 0, miner balance shouldn't change 66 | 67 | const MinerBalanceBefore = await client.eth.getBalance(FAUCET_ADDRESS, block - 1) 68 | const MinerBalanceAfter = await client.eth.getBalance(FAUCET_ADDRESS, block) 69 | console.log("Miner before", MinerBalanceBefore.toString()) 70 | console.log("Miner after", MinerBalanceAfter.toString()) 71 | 72 | // balance before/after the block is mined, remove the block reward 73 | const minerProfit = new BN(MinerBalanceAfter).sub(new BN(MinerBalanceBefore)) 74 | const finalProfit = minerProfit.sub(new BN(BLOCK_REWARD.toString())).toString(); 75 | 76 | console.log("Profit (ETH)", finalProfit.toString()) 77 | console.log("Mega bundle Profit equals bribe?", parseInt(finalProfit)==MINER_BRIBE) 78 | } else{ 79 | console.log("Bundle tx has not been mined yet") 80 | } 81 | }, 5000); 82 | } 83 | 84 | 85 | const checkBundleStatus = async (hash, contractAddress) => { 86 | var timer = setInterval(async function() { 87 | const receipt = await client.eth.getTransactionReceipt(hash) 88 | if(receipt){ // If the tx has been mined, it returns null if pending 89 | clearInterval(timer) // stop the setInterval once we get a valid receipt 90 | const block = receipt.blockNumber 91 | // Given the base fee is burnt and priority fee is set to 0, miner balance shouldn't change 92 | 93 | const MinerBalanceBefore = await client.eth.getBalance(FAUCET_ADDRESS, block - 1) 94 | const MinerBalanceAfter = await client.eth.getBalance(FAUCET_ADDRESS, block) 95 | console.log("Miner before", MinerBalanceBefore.toString()) 96 | console.log("Miner after", MinerBalanceAfter.toString()) 97 | 98 | // balance before/after the block is mined, remove the block reward 99 | const minerProfit = new BN(MinerBalanceAfter).sub(new BN(MinerBalanceBefore)) 100 | const finalProfit = minerProfit.sub(new BN(BLOCK_REWARD.toString())).toString(); 101 | 102 | console.log("Profit (ETH)", finalProfit.toString()) 103 | console.log("Profit equals bribe?", parseInt(finalProfit)==MINER_BRIBE) 104 | 105 | // Now we test megabundles 106 | const blockNumber = await client.eth.getBlockNumber() 107 | console.log("Megabundle target block no: ", blockNumber + 15) 108 | const updatedNonce = await client.eth.getTransactionCount(testWallet.address) 109 | const bribeTxInput = { 110 | to: contractAddress, 111 | value: MINER_BRIBE, 112 | fromAddress: testWallet.address, 113 | data: "0x37d0208c", // bribe() 114 | gasLimit: 200000, 115 | priorityFee: 0, 116 | privateKey: testWallet.privateKey.substring(2), 117 | nonce: updatedNonce 118 | } 119 | const txs = [await signEIP1559Tx(bribeTxInput, client)] 120 | const unsignedMegaBundle = { 121 | txs: txs, 122 | blockNumber: blockNumber + 15, 123 | minTimestamp: 0, 124 | maxTimestamp: 0, 125 | revertingTxHashes: [] 126 | } 127 | const signedMegaBundle = await generateRelaySignature(unsignedMegaBundle, TRUSTED_RELAY_PK) 128 | const params = [ 129 | { 130 | txs, 131 | blockNumber: blockNumber + 15, 132 | minTimestamp: 0, 133 | maxTimestamp: 0, 134 | revertingTxHashes: [], 135 | relaySignature: signedMegaBundle 136 | } 137 | ] 138 | const body = { 139 | params, 140 | method: 'eth_sendMegabundle', 141 | id: '123' 142 | } 143 | const respRaw = await fetch('http://localhost:8545', { 144 | method: 'POST', 145 | body: JSON.stringify(body), 146 | headers: { 147 | 'Content-Type': 'application/json' 148 | } 149 | }) 150 | await checkMegabundleStatus(client.utils.keccak256(txs[0])) 151 | } else{ 152 | console.log("Bundle tx has not been mined yet") 153 | } 154 | }, 5000); 155 | } 156 | 157 | const main = async () => { 158 | const authSigner = ethers.Wallet.createRandom() 159 | console.log("Funding test account!") 160 | const fundAccountInput = { 161 | to: testWallet.address, 162 | value: 1 * 10 ** 18, // 1 ETH 163 | fromAddress: FAUCET_ADDRESS, 164 | data: "0x", // direct send 165 | gasLimit: 21000, 166 | priorityFee: 0, 167 | privateKey: FAUCET_PK.substring(2), 168 | nonce: await client.eth.getTransactionCount(FAUCET_ADDRESS) 169 | } 170 | const signedFundTx = await signEIP1559Tx(fundAccountInput, client) 171 | const fundTxReceipt = (await client.eth.sendSignedTransaction(signedFundTx)) 172 | console.log("Funding tx mined at block #", fundTxReceipt.blockNumber) 173 | const testWalletBalance = await client.eth.getBalance(testWallet.address) 174 | console.log('Balance: ', testWalletBalance) 175 | 176 | // deploy the bribe contract 177 | console.log('Deploying bribe contract...') 178 | const factory = new ContractFactory(ABI, BIN, user) 179 | const bytecode = factory.bytecode 180 | const deployInput = { 181 | to: '', 182 | value: 0, 183 | fromAddress: testWallet.address, 184 | data: bytecode, // contract creation 185 | gasLimit: 200000, 186 | priorityFee: 0, 187 | privateKey: testWallet.privateKey.substring(2), 188 | nonce: await client.eth.getTransactionCount(testWallet.address) 189 | } 190 | const signedDeployTx = await signEIP1559Tx(deployInput, client) 191 | const deployTxReceipt = (await client.eth.sendSignedTransaction(signedDeployTx)) 192 | const contractAddress = deployTxReceipt.contractAddress 193 | // sign the bribe tx 194 | const bribeTxInput = { 195 | to: contractAddress, 196 | value: MINER_BRIBE, 197 | fromAddress: testWallet.address, 198 | data: "0x37d0208c", // bribe() 199 | gasLimit: 200000, 200 | priorityFee: 0, 201 | privateKey: testWallet.privateKey.substring(2), 202 | nonce: await client.eth.getTransactionCount(testWallet.address) 203 | } 204 | const txs = [await signEIP1559Tx(bribeTxInput, client)] 205 | const blockNumber = await client.eth.getBlockNumber() 206 | console.log("Bundle target block no: ", blockNumber + 10) 207 | // generate bundle data 208 | const params = [ 209 | { 210 | txs, 211 | blockNumber: `0x${(blockNumber + 10).toString(16)}` 212 | } 213 | ] 214 | const body = { 215 | params, 216 | method: 'eth_sendBundle', 217 | id: '123' 218 | } 219 | const respRaw = await fetch('http://localhost:8545', { 220 | method: 'POST', 221 | body: JSON.stringify(body), 222 | headers: { 223 | 'Content-Type': 'application/json' 224 | } 225 | }) 226 | console.log("txHash of bundle tx #1 ", client.utils.keccak256(txs[0])) 227 | await checkBundleStatus(client.utils.keccak256(txs[0]), contractAddress) // to get hash 228 | } 229 | 230 | main() -------------------------------------------------------------------------------- /scripts/1559-demo.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const fetch = require('node-fetch') 3 | const {signEIP1559Tx, generateRelaySignature} = require('./helpers') 4 | 5 | const localRPC = "http://localhost:8545/" 6 | const client = new Web3(new Web3.providers.HttpProvider(localRPC)) 7 | var BN = client.utils.BN; 8 | 9 | const TRUSTED_RELAY_PK = '0ceb0619ccbb1092e3d0e3874e4582abe5f9518262e465575ca837a7dad0703d' // 0xfb11e78C4DaFec86237c2862441817701fdf197F, see run.sh 10 | const FAUCET_PK = '133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791' 11 | const FAUCET_ADDRESS = '0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8' 12 | const DUMMY_RECEIVER = '0x1111111111111111111111111111111111111111' 13 | const MINER_BRIBE = 0.123 * 10 ** 18 14 | const RECEIVER_VALUE = 0.321 * 10 ** 18 15 | const BLOCK_REWARD = 2 * 10 ** 18 16 | const testWallet = client.eth.accounts.create(); 17 | 18 | /* only for reference, not used elsewhere 19 | const sample1559TxInput = { 20 | to: '0x0000000000000000000000000000000000000000', 21 | value: 1 * 10 ** 18, // 1 ETH, 22 | fromAddress: "0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8", 23 | data: "0x", 24 | gasLimit: 21000, 25 | priorityFee: 0, 26 | privateKey: "133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791" 27 | } */ 28 | 29 | const checkMegabundleStatus = async (hash) => { 30 | var timer = setInterval(async function() { 31 | const receipt = await client.eth.getTransactionReceipt(hash) 32 | if (receipt) { // If the tx has been mined, it returns null if pending 33 | clearInterval(timer) // stop the setInterval once we get a valid receipt 34 | const block = receipt.blockNumber 35 | // Given the base fee is burnt and priority fee is set to 0, miner balance shouldn't change 36 | 37 | const MinerBalanceBefore = await client.eth.getBalance(FAUCET_ADDRESS, block - 1) 38 | const MinerBalanceAfter = await client.eth.getBalance(FAUCET_ADDRESS, block) 39 | console.log("Miner before", MinerBalanceBefore.toString()) 40 | console.log("Miner after", MinerBalanceAfter.toString()) 41 | 42 | // balance before/after the block is mined, remove the block reward 43 | const minerProfit = new BN(MinerBalanceAfter).sub(new BN(MinerBalanceBefore)) 44 | const finalProfit = minerProfit.sub(new BN(BLOCK_REWARD.toString())).toString(); 45 | 46 | console.log("Profit (ETH)", finalProfit.toString()) 47 | console.log("Mega bundle Profit equals bribe?", parseInt(finalProfit)==MINER_BRIBE) 48 | 49 | // 1st tx of our bundle should also be processed and the balance of receiver should increase 50 | const balanceBefore = await client.eth.getBalance(DUMMY_RECEIVER, block - 1) 51 | const balanceAfter = await client.eth.getBalance(DUMMY_RECEIVER, block) 52 | const receiverProfit = new BN(balanceAfter).sub(new BN(balanceBefore)).toString(); 53 | 54 | console.log("Receiver before", balanceBefore.toString()) 55 | console.log("Receiver after", balanceAfter.toString()) 56 | console.log("Received value?", parseInt(receiverProfit)==RECEIVER_VALUE) 57 | } else { 58 | console.log("Bundle tx has not been mined yet") 59 | } 60 | }, 5000); 61 | } 62 | 63 | const checkBundleStatus = async (hash) => { 64 | var timer = setInterval(async function() { 65 | const receipt = await client.eth.getTransactionReceipt(hash) 66 | if(receipt){ // If the tx has been mined, it returns null if pending 67 | clearInterval(timer) // stop the setInterval once we get a valid receipt 68 | const block = receipt.blockNumber 69 | // Given the base fee is burnt and priority fee is set to 0, miner balance shouldn't change 70 | 71 | const MinerBalanceBefore = await client.eth.getBalance(FAUCET_ADDRESS, block - 1) 72 | const MinerBalanceAfter = await client.eth.getBalance(FAUCET_ADDRESS, block) 73 | console.log("Miner before", MinerBalanceBefore.toString()) 74 | console.log("Miner after", MinerBalanceAfter.toString()) 75 | 76 | // balance before/after the block is mined, remove the block reward 77 | const minerProfit = new BN(MinerBalanceAfter).sub(new BN(MinerBalanceBefore)) 78 | const finalProfit = minerProfit.sub(new BN(BLOCK_REWARD.toString())).toString(); 79 | 80 | console.log("Profit (ETH)", finalProfit.toString()) 81 | console.log("Profit equals bribe?", parseInt(finalProfit)==MINER_BRIBE) 82 | 83 | // 1st tx of our bundle should also be processed and the balance of receiver should increase 84 | const balanceBefore = await client.eth.getBalance(DUMMY_RECEIVER, block - 1) 85 | const balanceAfter = await client.eth.getBalance(DUMMY_RECEIVER, block) 86 | const receiverProfit = new BN(balanceAfter).sub(new BN(balanceBefore)).toString(); 87 | 88 | console.log("Receiver before", balanceBefore.toString()) 89 | console.log("Receiver after", balanceAfter.toString()) 90 | console.log("Received value?", parseInt(receiverProfit)==RECEIVER_VALUE) 91 | 92 | // Now we test megabundles 93 | const blockNumber = await client.eth.getBlockNumber() 94 | console.log("Megabundle target block no: ", blockNumber + 15) 95 | const updatedNonce = await client.eth.getTransactionCount(testWallet.address) 96 | const txs = [ 97 | // random tx at bundle index 0 98 | await signEIP1559Tx({ 99 | to: DUMMY_RECEIVER, 100 | value: RECEIVER_VALUE, // ETH 101 | fromAddress: testWallet.address, 102 | data: "0x", // direct send 103 | gasLimit: 21000, 104 | priorityFee: 0, 105 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 106 | nonce: updatedNonce 107 | }, client), 108 | // miner bribe 109 | await signEIP1559Tx({ 110 | to: FAUCET_ADDRESS, 111 | value: MINER_BRIBE, // ETH 112 | fromAddress: testWallet.address, 113 | data: "0x", // direct send 114 | gasLimit: 21000, 115 | priorityFee: 0, 116 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 117 | nonce: updatedNonce + 1 118 | }, client) 119 | ] 120 | const unsignedMegaBundle = { 121 | txs: txs, 122 | blockNumber: blockNumber + 15, 123 | minTimestamp: 0, 124 | maxTimestamp: 0, 125 | revertingTxHashes: [] 126 | } 127 | const signedMegaBundle = await generateRelaySignature(unsignedMegaBundle, TRUSTED_RELAY_PK) 128 | const params = [ 129 | { 130 | txs, 131 | blockNumber: blockNumber + 15, 132 | minTimestamp: 0, 133 | maxTimestamp: 0, 134 | revertingTxHashes: [], 135 | relaySignature: signedMegaBundle 136 | } 137 | ] 138 | const body = { 139 | params, 140 | method: 'eth_sendMegabundle', 141 | id: '123' 142 | } 143 | await fetch('http://localhost:8545', { 144 | method: 'POST', 145 | body: JSON.stringify(body), 146 | headers: { 147 | 'Content-Type': 'application/json' 148 | } 149 | }) 150 | await checkMegabundleStatus(client.utils.keccak256(txs[1])) 151 | } else{ 152 | console.log("Bundle tx has not been mined yet") 153 | } 154 | }, 5000); 155 | } 156 | 157 | const main = async() => { 158 | // First we fund the random test wallet from the miner faucet 159 | console.log("Funding test account!") 160 | const fundAccountInput = { 161 | to: testWallet.address, 162 | value: 1 * 10 ** 18, // 1 ETH 163 | fromAddress: FAUCET_ADDRESS, 164 | data: "0x", // direct send 165 | gasLimit: 21000, 166 | priorityFee: 0, 167 | privateKey: FAUCET_PK, 168 | nonce: await client.eth.getTransactionCount(FAUCET_ADDRESS) 169 | } 170 | const signedFundTx = await signEIP1559Tx(fundAccountInput, client) 171 | const fundTxReceipt = (await client.eth.sendSignedTransaction(signedFundTx)) 172 | console.log("Funding tx mined at block #", fundTxReceipt.blockNumber) 173 | const testWalletBalance = await client.eth.getBalance(testWallet.address) 174 | console.log('Balance: ', testWalletBalance) 175 | 176 | // Now we create a bundle 177 | const blockNumber = await client.eth.getBlockNumber() 178 | console.log("Bundle target block no: ", blockNumber + 10) 179 | const nonce = await client.eth.getTransactionCount(testWallet.address); 180 | const txs = [ 181 | // random tx at bundle index 0 182 | await signEIP1559Tx({ 183 | to: DUMMY_RECEIVER, 184 | value: RECEIVER_VALUE, // ETH 185 | fromAddress: testWallet.address, 186 | data: "0x", // direct send 187 | gasLimit: 21000, 188 | priorityFee: 0, 189 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 190 | nonce: nonce 191 | }, client), 192 | // miner bribe 193 | await signEIP1559Tx({ 194 | to: FAUCET_ADDRESS, 195 | value: MINER_BRIBE, // ETH 196 | fromAddress: testWallet.address, 197 | data: "0x", // direct send 198 | gasLimit: 21000, 199 | priorityFee: 0, 200 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 201 | nonce: nonce + 1 202 | }, client) 203 | ] 204 | const params = [ 205 | { 206 | txs, 207 | blockNumber: `0x${(blockNumber + 10).toString(16)}` 208 | } 209 | ] 210 | const body = { 211 | params, 212 | method: 'eth_sendBundle', 213 | id: '123' 214 | } 215 | await fetch('http://localhost:8545', { 216 | method: 'POST', 217 | body: JSON.stringify(body), 218 | headers: { 219 | 'Content-Type': 'application/json' 220 | } 221 | }) 222 | console.log("txHash of bundle tx #1 ", client.utils.keccak256(txs[0])) 223 | console.log("txHash of bundle tx #2 ", client.utils.keccak256(txs[1])) 224 | await checkBundleStatus(client.utils.keccak256(txs[1])) 225 | } 226 | 227 | main() 228 | -------------------------------------------------------------------------------- /scripts/contract-bribe-demo.ts: -------------------------------------------------------------------------------- 1 | import { ethers, ContractFactory } from 'ethers' 2 | import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle' 3 | import fetch from 'node-fetch' 4 | // @ts-ignore 5 | import solc from 'solc' 6 | 7 | const CONTRACT = ` 8 | // SPDX-License-Identifier: UNLICENSED 9 | pragma solidity ^0.7.0; 10 | 11 | contract Bribe { 12 | function bribe() payable public { 13 | block.coinbase.transfer(msg.value); 14 | } 15 | } 16 | ` 17 | const INPUT = { 18 | language: 'Solidity', 19 | sources: { 20 | 'Bribe.sol': { 21 | content: CONTRACT 22 | } 23 | }, 24 | settings: { 25 | outputSelection: { 26 | '*': { 27 | '*': ['*'] 28 | } 29 | } 30 | } 31 | } 32 | 33 | const OUTPUT = JSON.parse(solc.compile(JSON.stringify(INPUT))) 34 | const COMPILED = OUTPUT.contracts['Bribe.sol'] 35 | const ABI = COMPILED.Bribe.abi 36 | const BIN = '0x' + COMPILED.Bribe.evm.bytecode.object 37 | 38 | const FAUCET = '0x133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791' 39 | // connect to the simple provider 40 | let provider = new ethers.providers.JsonRpcProvider('http://localhost:8545') 41 | 42 | // we use the miner as a faucet for testing 43 | const faucet = new ethers.Wallet(FAUCET, provider) 44 | // we create a random user who will submit bundles 45 | const user = ethers.Wallet.createRandom().connect(provider) 46 | 47 | ;(async () => { 48 | // wrap it with the mev-geth provider 49 | const authSigner = ethers.Wallet.createRandom() 50 | const flashbotsProvider = await FlashbotsBundleProvider.create(provider, authSigner, 'http://localhost:8545', 5465) 51 | 52 | // fund the user with some Ether from the coinbase address 53 | console.log('Funding account...this may take a while due to DAG generation in the PoW testnet') 54 | let tx = await faucet.sendTransaction({ 55 | to: user.address, 56 | value: ethers.utils.parseEther('1') 57 | }) 58 | await tx.wait() 59 | 60 | // deploy the bribe contract 61 | console.log('Deploying bribe contract...') 62 | const factory = new ContractFactory(ABI, BIN, user) 63 | const contract = await factory.deploy() 64 | await contract.deployTransaction.wait() 65 | console.log('Deployed at:', contract.address) 66 | 67 | const bribeTx = await contract.populateTransaction.bribe({ 68 | value: ethers.utils.parseEther('0.216321768999') 69 | }) 70 | const txs = [await user.signTransaction(bribeTx)] 71 | 72 | console.log('Submitting bundle') 73 | const blk = await provider.getBlockNumber() 74 | 75 | for (let i = 1; i <= 10; i++) { 76 | const params = [ 77 | { 78 | txs, 79 | blockNumber: `0x${(blk + i).toString(16)}` 80 | } 81 | ] 82 | const body = { 83 | params, 84 | method: 'eth_sendBundle', 85 | id: '123' 86 | } 87 | const respRaw = await fetch('http://localhost:8545', { 88 | method: 'POST', 89 | body: JSON.stringify(body), 90 | headers: { 91 | 'Content-Type': 'application/json' 92 | } 93 | }) 94 | if (respRaw.status >= 300) { 95 | console.error('error sending bundle') 96 | process.exit(1) 97 | } 98 | const json = await respRaw.json() 99 | if (json.error) { 100 | console.error('error sending bundle, error was', json.error) 101 | process.exit(1) 102 | } 103 | } 104 | while (true) { 105 | const newBlock = await provider.getBlockNumber() 106 | if (newBlock > blk + 10) break 107 | await new Promise((resolve) => setTimeout(resolve, 100)) // sleep 108 | } 109 | 110 | const balanceBefore = await provider.getBalance(faucet.address, blk) 111 | const balanceAfter = await provider.getBalance(faucet.address, blk + 10) 112 | 113 | console.log('Miner before', balanceBefore.toString()) 114 | console.log('Miner after', balanceAfter.toString()) 115 | // subtract 2 for block reward 116 | const profit = balanceAfter.sub(balanceBefore).sub(ethers.utils.parseEther('2')) 117 | console.log('Profit (ETH)', ethers.utils.formatEther(profit)) 118 | console.log('Profit equals bribe?', profit.eq(bribeTx.value!)) 119 | })().catch((err) => { 120 | console.error('error encountered in main loop', err) 121 | process.exit(1) 122 | }) 123 | -------------------------------------------------------------------------------- /scripts/demo.ts: -------------------------------------------------------------------------------- 1 | import { ethers, Wallet } from 'ethers' 2 | import fetch from 'node-fetch' 3 | 4 | const FAUCET = '0x133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791' 5 | const DUMMY_RECEIVER = '0x1111111111111111111111111111111111111111' 6 | // connect to the simple provider 7 | let provider = new ethers.providers.JsonRpcProvider('http://localhost:8545') 8 | // we use the miner as a faucet for testing 9 | const faucet = new ethers.Wallet(FAUCET, provider) 10 | // we create a random user who will submit bundles 11 | const user = ethers.Wallet.createRandom().connect(provider) 12 | 13 | ;(async () => { 14 | // wrap it with the mev-geth provider 15 | const authSigner = Wallet.createRandom() 16 | 17 | console.log('Faucet', faucet.address) 18 | // fund the user with some Ether from the coinbase address 19 | console.log('Funding account...this may take a while due to DAG generation in the PoW testnet') 20 | let tx = await faucet.sendTransaction({ 21 | to: user.address, 22 | value: ethers.utils.parseEther('1') 23 | }) 24 | await tx.wait() 25 | console.log('OK') 26 | const balance = await provider.getBalance(user.address) 27 | console.log('Balance:', balance.toString()) 28 | 29 | const nonce = await user.getTransactionCount() 30 | const bribe = ethers.utils.parseEther('0.06666666666') 31 | const txs = [ 32 | // some transaction 33 | await user.signTransaction({ 34 | to: DUMMY_RECEIVER, 35 | value: ethers.utils.parseEther('0.1'), 36 | nonce: nonce 37 | }), 38 | // the miner bribe 39 | await user.signTransaction({ 40 | to: faucet.address, 41 | value: bribe, 42 | nonce: nonce + 1 43 | }) 44 | ] 45 | 46 | console.log('Submitting bundle') 47 | const blk = await provider.getBlockNumber() 48 | 49 | for (let i = 1; i <= 10; i++) { 50 | const params = [ 51 | { 52 | txs, 53 | blockNumber: `0x${(blk + i).toString(16)}` 54 | } 55 | ] 56 | const body = { 57 | params, 58 | method: 'eth_sendBundle', 59 | id: '123' 60 | } 61 | const respRaw = await fetch('http://localhost:8545', { 62 | method: 'POST', 63 | body: JSON.stringify(body), 64 | headers: { 65 | 'Content-Type': 'application/json' 66 | } 67 | }) 68 | if (respRaw.status >= 300) { 69 | console.error('error sending bundle') 70 | process.exit(1) 71 | } 72 | const json = await respRaw.json() 73 | if (json.error) { 74 | console.error('error sending bundle, error was', json.error) 75 | process.exit(1) 76 | } 77 | } 78 | while (true) { 79 | const newBlock = await provider.getBlockNumber() 80 | if (newBlock > blk + 10) break 81 | await new Promise((resolve) => setTimeout(resolve, 100)) // sleep 82 | } 83 | 84 | const balanceBefore = await provider.getBalance(faucet.address, blk) 85 | const balanceAfter = await provider.getBalance(faucet.address, blk + 10) 86 | console.log('Miner before', balanceBefore.toString()) 87 | console.log('Miner after', balanceAfter.toString()) 88 | // subtract 2 for block reward 89 | const profit = balanceAfter.sub(balanceBefore).sub(ethers.utils.parseEther('2')) 90 | console.log('Profit (ETH)', ethers.utils.formatEther(profit)) 91 | console.log('Profit equals bribe?', profit.eq(bribe)) 92 | })().catch((err) => { 93 | console.error('error encountered in main loop', err) 94 | process.exit(1) 95 | }) 96 | -------------------------------------------------------------------------------- /scripts/e2e-reverting-bundles.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const fetch = require('node-fetch') 3 | const process = require('process') 4 | const {signEIP1559Tx, awaitBlock} = require('./helpers') 5 | 6 | const localRPC = "http://localhost:8545/" 7 | const client = new Web3(new Web3.providers.HttpProvider(localRPC)) 8 | 9 | const FAUCET_PK = '133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791' 10 | const FAUCET_ADDRESS = '0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8' 11 | const DUMMY_RECEIVER = '0x1111111111111111111111111111111111111111' 12 | const MINER_BRIBE = 0.123 * 10 ** 18 13 | const RECEIVER_VALUE = 0.321 * 10 ** 18 14 | const testWallet = client.eth.accounts.create(); 15 | 16 | const checkRevertingBundles = async () => { 17 | // Now we create a bundle 18 | const blockNumber = await client.eth.getBlockNumber() 19 | console.log("Bundle target block no: ", blockNumber + 10) 20 | const nonce = await client.eth.getTransactionCount(testWallet.address); 21 | const txs = [ 22 | // random tx at bundle index 0 23 | await signEIP1559Tx({ 24 | to: DUMMY_RECEIVER, 25 | value: RECEIVER_VALUE, // ETH 26 | fromAddress: testWallet.address, 27 | data: "0x", // direct send 28 | gasLimit: 21000, 29 | priorityFee: 0, 30 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 31 | nonce: nonce 32 | }, client), 33 | // miner bribe 34 | await signEIP1559Tx({ 35 | to: FAUCET_ADDRESS, 36 | value: MINER_BRIBE, // ETH 37 | fromAddress: testWallet.address, 38 | data: "0x", // direct send 39 | gasLimit: 21000 * 10**10, 40 | priorityFee: 0, 41 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 42 | nonce: nonce + 1 43 | }, client) 44 | ] 45 | const params = [ 46 | { 47 | txs, 48 | blockNumber: `0x${(blockNumber + 10).toString(16)}` 49 | } 50 | ] 51 | const body = { 52 | params, 53 | method: 'eth_sendBundle', 54 | id: '123' 55 | } 56 | await fetch('http://localhost:8545', { 57 | method: 'POST', 58 | body: JSON.stringify(body), 59 | headers: { 60 | 'Content-Type': 'application/json' 61 | } 62 | }) 63 | 64 | await awaitBlock(client, blockNumber + 15) 65 | 66 | for (let i = 0; i < 2; ++i) { 67 | const receipt = await client.eth.getTransactionReceipt(client.utils.keccak256(txs[i])) 68 | if (receipt) { 69 | console.log("transaction from a reverting bundle was inserted") 70 | process.exit(1) 71 | } 72 | } 73 | } 74 | 75 | const main = async() => { 76 | // First we fund the random test wallet from the miner faucet 77 | console.log("Funding test account!") 78 | const fundAccountInput = { 79 | to: testWallet.address, 80 | value: 1 * 10 ** 18, // 1 ETH 81 | fromAddress: FAUCET_ADDRESS, 82 | data: "0x", // direct send 83 | gasLimit: 21000, 84 | priorityFee: 0, 85 | privateKey: FAUCET_PK, 86 | nonce: await client.eth.getTransactionCount(FAUCET_ADDRESS) 87 | } 88 | const signedFundTx = await signEIP1559Tx(fundAccountInput, client) 89 | const fundTxReceipt = (await client.eth.sendSignedTransaction(signedFundTx)) 90 | console.log("Funding tx mined at block #", fundTxReceipt.blockNumber) 91 | const testWalletBalance = await client.eth.getBalance(testWallet.address) 92 | console.log('Balance: ', testWalletBalance) 93 | 94 | await checkRevertingBundles() 95 | } 96 | 97 | main() 98 | 99 | -------------------------------------------------------------------------------- /scripts/e2e-reverting-megabundle.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const fetch = require('node-fetch') 3 | const process = require('process') 4 | const {signEIP1559Tx, generateRelaySignature, awaitBlock} = require('./helpers') 5 | 6 | const localRPC = "http://localhost:8545/" 7 | const client = new Web3(new Web3.providers.HttpProvider(localRPC)) 8 | 9 | const TRUSTED_RELAY_PK = '0ceb0619ccbb1092e3d0e3874e4582abe5f9518262e465575ca837a7dad0703d' // 0xfb11e78C4DaFec86237c2862441817701fdf197F, see run.sh 10 | const FAUCET_PK = '133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791' 11 | const FAUCET_ADDRESS = '0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8' 12 | const DUMMY_RECEIVER = '0x1111111111111111111111111111111111111111' 13 | const MINER_BRIBE = 0.123 * 10 ** 18 14 | const RECEIVER_VALUE = 0.321 * 10 ** 18 15 | const testWallet = client.eth.accounts.create(); 16 | 17 | const checkRevertingMebagundle = async () => { 18 | const updatedNonce = await client.eth.getTransactionCount(testWallet.address) 19 | const txs = [ 20 | await signEIP1559Tx({ 21 | to: DUMMY_RECEIVER, 22 | value: RECEIVER_VALUE, // ETH 23 | fromAddress: testWallet.address, 24 | data: "0x", // direct send 25 | gasLimit: 21000, 26 | priorityFee: 0, 27 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 28 | nonce: updatedNonce 29 | }, client), 30 | // random tx at bundle index 0 31 | await signEIP1559Tx({ 32 | to: DUMMY_RECEIVER, 33 | value: RECEIVER_VALUE, // ETH 34 | fromAddress: testWallet.address, 35 | data: "0x", // direct send 36 | gasLimit: 21000 * 10**10, 37 | priorityFee: 0, 38 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 39 | nonce: updatedNonce + 1 40 | }, client), 41 | // miner bribe 42 | await signEIP1559Tx({ 43 | to: FAUCET_ADDRESS, 44 | value: MINER_BRIBE, // ETH 45 | fromAddress: testWallet.address, 46 | data: "0x", // direct send 47 | gasLimit: 21000, 48 | priorityFee: 0, 49 | privateKey: testWallet.privateKey.substring(2), // remove 0x in pk 50 | nonce: updatedNonce + 2 51 | }, client) 52 | ] 53 | const blockNumber = await client.eth.getBlockNumber() 54 | console.log("Megabundle target block no: ", blockNumber + 10) 55 | const unsignedMegaBundle = { 56 | txs: txs, 57 | blockNumber: blockNumber + 10, 58 | minTimestamp: 0, 59 | maxTimestamp: 0, 60 | revertingTxHashes: [] 61 | } 62 | const signedMegaBundle = await generateRelaySignature(unsignedMegaBundle, TRUSTED_RELAY_PK) 63 | const params = [ 64 | { 65 | txs, 66 | blockNumber: blockNumber + 10, 67 | minTimestamp: 0, 68 | maxTimestamp: 0, 69 | revertingTxHashes: [], 70 | relaySignature: signedMegaBundle 71 | } 72 | ] 73 | const body = { 74 | params, 75 | method: 'eth_sendMegabundle', 76 | id: '123' 77 | } 78 | await fetch('http://localhost:8545', { 79 | method: 'POST', 80 | body: JSON.stringify(body), 81 | headers: { 82 | 'Content-Type': 'application/json' 83 | } 84 | }) 85 | 86 | await awaitBlock(client, blockNumber + 15) 87 | 88 | for (let i = 0; i < 3; ++i) { 89 | const receipt = await client.eth.getTransactionReceipt(client.utils.keccak256(txs[i])) 90 | if (receipt) { 91 | console.log("transaction from a reverting megabundle was inserted") 92 | process.exit(1) 93 | } 94 | } 95 | } 96 | 97 | const main = async() => { 98 | // First we fund the random test wallet from the miner faucet 99 | console.log("Funding test account!") 100 | const fundAccountInput = { 101 | to: testWallet.address, 102 | value: 1 * 10 ** 18, // 1 ETH 103 | fromAddress: FAUCET_ADDRESS, 104 | data: "0x", // direct send 105 | gasLimit: 21000, 106 | priorityFee: 0, 107 | privateKey: FAUCET_PK, 108 | nonce: await client.eth.getTransactionCount(FAUCET_ADDRESS) 109 | } 110 | const signedFundTx = await signEIP1559Tx(fundAccountInput, client) 111 | const fundTxReceipt = (await client.eth.sendSignedTransaction(signedFundTx)) 112 | console.log("Funding tx mined at block #", fundTxReceipt.blockNumber) 113 | const testWalletBalance = await client.eth.getBalance(testWallet.address) 114 | console.log('Balance: ', testWalletBalance) 115 | 116 | await checkRevertingMebagundle() 117 | } 118 | 119 | main() 120 | 121 | -------------------------------------------------------------------------------- /scripts/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | const Common = require('@ethereumjs/common').default 3 | const ethTx = require('@ethereumjs/tx') 4 | const { Buffer } = require('buffer'); 5 | const Web3 = require('web3'); 6 | const web3 = new Web3(); 7 | const ethers = require('ethers') 8 | const localRPC = "http://localhost:8545/" 9 | const chainID = 5465 // from genesis.json file, for the common required for tx signing 10 | const client = new Web3(new Web3.providers.HttpProvider(localRPC)) 11 | 12 | function delay(ms) { 13 | return new Promise( resolve => setTimeout(resolve, ms) ); 14 | } 15 | 16 | const awaitBlock = async(client, blockNumber) => { 17 | while (await client.eth.getBlockNumber() < blockNumber) { 18 | await delay(1000) 19 | } 20 | } 21 | 22 | const getLatestBaseFee = async() => { 23 | const block = await client.eth.getBlock("latest") 24 | return parseInt(block.baseFeePerGas) 25 | } 26 | 27 | const signEIP1559Tx = async (input, client) => { 28 | const accountNonce = await client.eth.getTransactionCount(input.fromAddress); 29 | const tx = { 30 | to: input.to, 31 | data: input.data, 32 | value: Web3.utils.toHex(input.value), 33 | nonce: Web3.utils.toHex(input.nonce) || Web3.utils.toHex(accountNonce), 34 | gasLimit: Web3.utils.toHex(input.gasLimit), 35 | maxFeePerGas: Web3.utils.toHex(await getLatestBaseFee() + input.priorityFee), 36 | maxPriorityFeePerGas: Web3.utils.toHex(input.priorityFee), // 0 tip for now 37 | chainId: Web3.utils.toHex(await client.eth.getChainId()), 38 | accessList: [], 39 | type: "0x02" // ensures the tx isn't legacy type 40 | } 41 | // custom common for our private network 42 | const customCommon = Common.forCustomChain( 43 | 'mainnet', 44 | { 45 | name: 'mev-geth-with-1559', 46 | chainId: chainID, 47 | }, 48 | 'london', 49 | ); 50 | // sign and return 51 | const unsignedTx = new ethTx.FeeMarketEIP1559Transaction(tx, {customCommon}); 52 | const signedTx = unsignedTx.sign(Buffer.from(input.privateKey, 'hex')) 53 | return '0x' + signedTx.serialize().toString('hex'); 54 | } 55 | 56 | const generateRelaySignature = async(megabundle, relayPk) => { 57 | const formattedMegabundle = [ 58 | megabundle.txs, 59 | ethers.BigNumber.from(megabundle.blockNumber).toHexString(), 60 | (megabundle.minTimestamp == 0) ? '0x' : ethers.BigNumber.from(megabundle.minTimestamp).toHexString(), 61 | (megabundle.maxTimestamp == 0) ? '0x' : ethers.BigNumber.from(megabundle.maxTimestamp).toHexString(), 62 | megabundle.revertingTxHashes 63 | ] 64 | const encodedMegabundle = ethers.utils.RLP.encode(formattedMegabundle) 65 | const signedMegaBundle = web3.eth.accounts.sign(encodedMegabundle, relayPk) 66 | return signedMegaBundle.signature 67 | } 68 | 69 | exports.signEIP1559Tx = signEIP1559Tx 70 | exports.generateRelaySignature = generateRelaySignature 71 | exports.awaitBlock = awaitBlock 72 | exports.delay = delay 73 | -------------------------------------------------------------------------------- /scripts/private-tx-demo.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const fetch = require('node-fetch') 3 | const process = require('process') 4 | 5 | const {signEIP1559Tx, awaitBlock, delay} = require('./helpers') 6 | 7 | const FAUCET_PK = '133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791' 8 | const FAUCET_ADDRESS = '0xd912AeCb07E9F4e1eA8E6b4779e7Fb6Aa1c3e4D8' 9 | const DUMMY_RECEIVER = '0x1111111111111111111111111111111111111111' 10 | const RECEIVER_VALUE = 0.00321 * 10 ** 18 11 | const TEST_WALLET_RECEIVE_VALUE = 100*RECEIVER_VALUE 12 | 13 | const sendPrivateRawTransaction = async(rpc_address, client, from, pk, to, value) => { 14 | const tx = await signEIP1559Tx({ 15 | to: to, 16 | value: value, 17 | fromAddress: from, 18 | data: "0x", 19 | gasLimit: 21000, 20 | priorityFee: 150 * 10 ** 9, 21 | privateKey: pk, 22 | nonce: null 23 | }, client) 24 | 25 | return await sendRawTransaction(rpc_address, 'eth_sendPrivateRawTransaction', tx) 26 | } 27 | 28 | const sendRawTransaction = async(rpc_address, method, tx) => { 29 | const body = { 30 | params: [tx], 31 | method, 32 | id: '124' 33 | } 34 | const respRaw = await fetch(rpc_address, { 35 | method: 'POST', 36 | body: JSON.stringify(body), 37 | headers: { 38 | 'Content-Type': 'application/json' 39 | } 40 | }) 41 | return [tx, await respRaw.json()] 42 | } 43 | 44 | const main = async() => { 45 | const minerRPC = "http://localhost:8545/" 46 | const otherRPC = "http://localhost:8546/" 47 | const client = new Web3(new Web3.providers.HttpProvider(minerRPC)) 48 | const nonMiningClient = new Web3(new Web3.providers.HttpProvider(minerRPC)) 49 | 50 | const testWallet = client.eth.accounts.create(); 51 | 52 | /* Check that private transactions are mined */ 53 | const testWalletBalanceBefore = Number(await client.eth.getBalance(testWallet.address)) 54 | 55 | const [_, resp1] = await sendPrivateRawTransaction(minerRPC, client, FAUCET_ADDRESS, FAUCET_PK, testWallet.address, TEST_WALLET_RECEIVE_VALUE) 56 | if (resp1.error) { 57 | console.log("Incorrect response", resp1) 58 | process.exit(1) 59 | } 60 | 61 | let blockNumber = await client.eth.getBlockNumber() 62 | 63 | /* Wait until in sync */ 64 | await awaitBlock(nonMiningClient, blockNumber+2) 65 | const testWalletBalanceAfter = Number(await client.eth.getBalance(testWallet.address)) 66 | 67 | if (testWalletBalanceAfter !== testWalletBalanceBefore + TEST_WALLET_RECEIVE_VALUE) { 68 | console.log("incorrect balance, private tx was not mined") 69 | console.log("before", testWalletBalanceBefore, "after", testWalletBalanceAfter, "expected", testWalletBalanceBefore + TEST_WALLET_RECEIVE_VALUE, "diff", testWalletBalanceAfter - testWalletBalanceBefore + TEST_WALLET_RECEIVE_VALUE) 70 | process.exit(1) 71 | } 72 | 73 | const balanceBefore = Number(await client.eth.getBalance(DUMMY_RECEIVER)) 74 | 75 | const [privateTx, resp2] = await sendPrivateRawTransaction(otherRPC, nonMiningClient, testWallet.address, testWallet.privateKey.substring(2), DUMMY_RECEIVER, RECEIVER_VALUE) 76 | if (resp2.error) { 77 | console.log("Incorrect response", resp2) 78 | process.exit(1) 79 | } 80 | 81 | await delay(10 * 1000) 82 | 83 | const balanceAfterSubmission = Number(await client.eth.getBalance(DUMMY_RECEIVER)) 84 | if (balanceAfterSubmission !== balanceBefore) { 85 | console.log("incorrect balance, private tx was mined") 86 | process.exit(1) 87 | } 88 | 89 | await delay(20 * 1000) 90 | 91 | /* Wait two more blocks */ 92 | blockNumber = await client.eth.getBlockNumber() 93 | await awaitBlock(nonMiningClient, blockNumber+2) 94 | 95 | const balanceAfterMinute = Number(await client.eth.getBalance(DUMMY_RECEIVER)) 96 | 97 | if (balanceAfterMinute !== balanceBefore) { 98 | console.log("incorrect balance, private tx was mined") 99 | process.exit(1) 100 | } 101 | 102 | /* After one minute the tx should be dropped */ 103 | 104 | const [_2, resp3] = await sendRawTransaction(otherRPC, 'eth_sendRawTransaction', privateTx) 105 | if (resp3.error === null || resp3.error.message !== 'already known') { 106 | console.log("incorrect response on message replay as non-private", resp3) 107 | process.exit(1) 108 | } 109 | } 110 | 111 | main() 112 | -------------------------------------------------------------------------------- /scripts/ws-contract-bribe-demo.js: -------------------------------------------------------------------------------- 1 | // ws server - relay side 2 | const FlashbotsBundleProvider = require("@flashbots/ethers-provider-bundle").FlashbotsBundleProvider 3 | const ethers =require("ethers") 4 | const ethUtil = require('ethereumjs-util') 5 | const ContractFactory = require("ethers").ContractFactory 6 | const WebSocket = require('ws') 7 | const _ = require("lodash") 8 | const solc = require('solc') 9 | 10 | const CONTRACT = ` 11 | // SPDX-License-Identifier: UNLICENSED 12 | pragma solidity ^0.7.0; 13 | 14 | contract Bribe { 15 | function bribe() payable public { 16 | block.coinbase.transfer(msg.value); 17 | } 18 | } 19 | ` 20 | const INPUT = { 21 | language: 'Solidity', 22 | sources: { 23 | 'Bribe.sol': { 24 | content: CONTRACT 25 | } 26 | }, 27 | settings: { 28 | outputSelection: { 29 | '*': { 30 | '*': ['*'] 31 | } 32 | } 33 | } 34 | } 35 | const OUTPUT = JSON.parse(solc.compile(JSON.stringify(INPUT))) 36 | const COMPILED = OUTPUT.contracts['Bribe.sol'] 37 | const ABI = COMPILED.Bribe.abi 38 | const BIN = '0x' + COMPILED.Bribe.evm.bytecode.object 39 | // miner pk on the private network 40 | const FAUCET = "0x133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791" 41 | //const DUMMY_RECEIVER = "0x1111111111111111111111111111111111111111" // address we'll send funds via bundles 42 | const simpleProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") 43 | const faucet = new ethers.Wallet(FAUCET, simpleProvider) 44 | // we create a random user who will submit bundles 45 | const user = ethers.Wallet.createRandom().connect(simpleProvider) 46 | 47 | const wss = new WebSocket.Server({ port: 8080 }) 48 | const flashBotsProvider = new FlashbotsBundleProvider(simpleProvider, "http://localhost:8545") 49 | // we use the miner as a faucet for testing 50 | 51 | 52 | // Message sent to clients on first connection 53 | const initMessage = { 54 | data: "Successfully connected to relay WS", 55 | type: "success" 56 | } 57 | 58 | // Set heartbeat 59 | function heartbeat(){ 60 | console.log("received: pong") 61 | this.isAlive = true; 62 | } 63 | 64 | const whitelistedAddresses = ["0x908e8902bd2018d3bf4d5a0fb42a457e1e8f1a6e"] // EAO address, 0x trimmed 65 | 66 | // Helper functions 67 | const sleep = (ms) => { 68 | return new Promise(resolve => setTimeout(resolve, ms)); 69 | } 70 | 71 | const timeoutRange = 5 72 | const isValidTimestamp = (timestamp) => { 73 | const dateObj = new Date(timestamp) 74 | const currentTime = new Date() 75 | const lowerBound = new Date(currentTime.getTime() - timeoutRange *60000).getTime() // +- 5 mins UTC, to account for clock syncing 76 | const upperBound = new Date(currentTime.getTime() + timeoutRange *60000).getTime() // 60000 for mins => ms 77 | return dateObj.getTime() >= lowerBound && dateObj.getTime() <= upperBound 78 | } 79 | 80 | const isValidSignature = (signature, message) => { 81 | try{ 82 | const messageHash = ethers.utils.arrayify(ethers.utils.id(message)) 83 | const parsedSignature = ethUtil.fromRpcSig(signature) 84 | const recoveredAddress = "0x" + ethUtil.pubToAddress(ethUtil.ecrecover(messageHash, parsedSignature.v, parsedSignature.r, parsedSignature.s)).toString("hex"); 85 | console.log(recoveredAddress) 86 | if(_.includes(whitelistedAddresses, recoveredAddress) && isValidTimestamp(parseInt(message)* 1000)){ 87 | return true 88 | }else { 89 | return false 90 | } 91 | } catch (error){ 92 | console.log(error) 93 | return false 94 | } 95 | } 96 | const generateTestBundle = async () => { 97 | const authSigner = ethers.Wallet.createRandom() 98 | console.log("Funding account.....") 99 | let tx = await faucet.sendTransaction({ 100 | to: user.address, 101 | value: ethers.utils.parseEther('1') 102 | }) 103 | await tx.wait() 104 | 105 | // deploy the bribe contract 106 | console.log('Deploying bribe contract...') 107 | const factory = new ContractFactory(ABI, BIN, user) 108 | const contract = await factory.deploy() 109 | await contract.deployTransaction.wait() 110 | 111 | const bribeTx = await contract.populateTransaction.bribe({ 112 | value: ethers.utils.parseEther('0.216321768999') 113 | }) 114 | const txs = [ 115 | { 116 | signer: user, 117 | transaction: bribeTx 118 | } 119 | ] 120 | 121 | console.log("Submitting bundle"); 122 | const blk = await simpleProvider.getBlockNumber() 123 | 124 | const targetBlockNumber = blk + 10 125 | const payload = { 126 | data: { 127 | txs: await flashBotsProvider.signBundle(txs), 128 | blockNumber: `0x${targetBlockNumber.toString(16)}`, 129 | minTimestamp: 0, 130 | maxTimestamp: 0, 131 | revertingTxHashes: [] 132 | }, 133 | type: "bundle" 134 | } 135 | return payload 136 | } 137 | 138 | const checkBundle = async (payload) => { 139 | var timer = setInterval(async function() { 140 | const hash = ethers.utils.keccak256(payload.data.txs[0]) 141 | const receipt = await simpleProvider.getTransactionReceipt(hash) 142 | if(receipt){ // If the tx has been mined, it returns null if pending 143 | clearInterval(timer) // stop the setInterval once we get a valid receipt 144 | const block = receipt.blockNumber 145 | const balanceBefore = await simpleProvider.getBalance(faucet.address, block - 1) 146 | const balanceAfter = await simpleProvider.getBalance(faucet.address, block) 147 | console.log('Miner before', balanceBefore.toString()) 148 | console.log('Miner after', balanceAfter.toString()) 149 | // subtract 2 for block reward 150 | const profit = balanceAfter.sub(balanceBefore).sub(ethers.utils.parseEther('2')) 151 | console.log('Profit (ETH)', ethers.utils.formatEther(profit)) 152 | const checkProfit = (ethers.utils.formatEther(profit) === '0.216321768999') 153 | console.log('Profit equals bribe?', checkProfit) 154 | if(checkProfit){ 155 | wss.close() 156 | } 157 | } else{ 158 | console.log("Bundle tx has not been mined yet") 159 | } 160 | }, 5000); 161 | } 162 | 163 | wss.on('connection', async function connection(ws, req){ 164 | ws.isAlive = true; 165 | ws.on('pong', heartbeat) 166 | ws.on('message', message => { 167 | console.log("received message from ws client: " + message) 168 | }) 169 | 170 | if(req.headers['x-auth-message']){ 171 | const parsedAuthMessage = JSON.parse(req.headers['x-auth-message']) 172 | console.log(parsedAuthMessage) 173 | if(isValidSignature(parsedAuthMessage.signature, parsedAuthMessage.timestamp)){ 174 | await sleep(1000) 175 | const payload = await generateTestBundle() 176 | ws.send(JSON.stringify(payload)) 177 | await checkBundle(payload) 178 | }else{ 179 | console.log("auth failed") 180 | ws.terminate() 181 | } 182 | }else { 183 | ws.terminate() 184 | } 185 | 186 | ws.on("close", m => { 187 | console.log("client closed " + m) 188 | }) 189 | 190 | }) 191 | 192 | // Heartbeat test to see if connection is still alive every 10 seconds 193 | const interval = setInterval(function ping() { 194 | wss.clients.forEach(function each(ws) { 195 | if (ws.isAlive === false) {return ws.terminate()} 196 | ws.isAlive = false; 197 | ws.ping(()=> {console.log("sending: ping")}); 198 | }); 199 | }, 10000); 200 | 201 | wss.on('close', function close() { 202 | clearInterval(interval); 203 | }); 204 | -------------------------------------------------------------------------------- /scripts/ws-demo.js: -------------------------------------------------------------------------------- 1 | // ws server - relay side 2 | const FlashbotsBundleProvider = require("@flashbots/ethers-provider-bundle").FlashbotsBundleProvider 3 | const ethers =require("ethers") 4 | const ethUtil = require('ethereumjs-util') 5 | const WebSocket = require('ws') 6 | const _ = require("lodash") 7 | // miner pk on the private network 8 | const FAUCET = "0x133be114715e5fe528a1b8adf36792160601a2d63ab59d1fd454275b31328791" 9 | const DUMMY_RECEIVER = "0x1111111111111111111111111111111111111111" // address we'll send funds via bundles 10 | 11 | const wss = new WebSocket.Server({ port: 8080 }) 12 | const simpleProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545") 13 | const flashBotsProvider = new FlashbotsBundleProvider(simpleProvider, "http://localhost:8545") 14 | // we use the miner as a faucet for testing 15 | const faucet = new ethers.Wallet(FAUCET, simpleProvider) 16 | // we create a random user who will submit bundles 17 | const user = ethers.Wallet.createRandom().connect(simpleProvider) 18 | const bribe = ethers.utils.parseEther('0.02') 19 | 20 | // Message sent to clients on first connection 21 | const initMessage = { 22 | data: "Successfully connected to relay WS", 23 | type: "success" 24 | } 25 | 26 | // Set heartbeat 27 | function heartbeat(){ 28 | console.log("received: pong") 29 | this.isAlive = true; 30 | } 31 | 32 | const whitelistedAddresses = ["0x908e8902bd2018d3bf4d5a0fb42a457e1e8f1a6e"] // EAO address, 0x trimmed 33 | 34 | 35 | // Helper functions 36 | const sleep = (ms) => { 37 | return new Promise(resolve => setTimeout(resolve, ms)); 38 | } 39 | 40 | const timeoutRange = 5 41 | const isValidTimestamp = (timestamp) => { 42 | const dateObj = new Date(timestamp) 43 | const currentTime = new Date() 44 | const lowerBound = new Date(currentTime.getTime() - timeoutRange *60000).getTime() // +- 5 mins UTC, to account for clock syncing 45 | const upperBound = new Date(currentTime.getTime() + timeoutRange *60000).getTime() // 60000 for mins => ms 46 | return dateObj.getTime() >= lowerBound && dateObj.getTime() <= upperBound 47 | } 48 | 49 | const isValidSignature = (signature, message) => { 50 | try{ 51 | const messageHash = ethers.utils.arrayify(ethers.utils.id(message)) 52 | const parsedSignature = ethUtil.fromRpcSig(signature) 53 | const recoveredAddress = "0x" + ethUtil.pubToAddress(ethUtil.ecrecover(messageHash, parsedSignature.v, parsedSignature.r, parsedSignature.s)).toString("hex"); 54 | console.log(recoveredAddress) 55 | if(_.includes(whitelistedAddresses, recoveredAddress) && isValidTimestamp(parseInt(message)* 1000)){ 56 | return true 57 | }else { 58 | return false 59 | } 60 | } catch (error){ 61 | console.log(error) 62 | return false 63 | } 64 | } 65 | 66 | const generateTestBundle = async () => { 67 | console.log("Funding account.....") 68 | let tx = await faucet.sendTransaction({ 69 | to: user.address, 70 | value: ethers.utils.parseEther('1') 71 | }) 72 | await tx.wait() 73 | const balance = await simpleProvider.getBalance(user.address) 74 | console.log("Balance:", balance.toString()) 75 | const nonce = await user.getTransactionCount() 76 | const txs = [ 77 | // some transaction 78 | { 79 | signer: user, 80 | transaction: { 81 | to: DUMMY_RECEIVER, 82 | value: ethers.utils.parseEther('0.05'), 83 | nonce: nonce, 84 | }, 85 | }, 86 | // the miner bribe 87 | { 88 | signer: user, 89 | transaction: { 90 | to: faucet.address, 91 | value: bribe, 92 | nonce: nonce + 1, 93 | } 94 | }, 95 | ] 96 | console.log("Submitting bundle"); 97 | const blk = await simpleProvider.getBlockNumber() 98 | 99 | const targetBlockNumber = blk + 5 100 | const payload = { 101 | data: { 102 | txs: await flashBotsProvider.signBundle(txs), 103 | blockNumber: `0x${targetBlockNumber.toString(16)}`, 104 | minTimestamp: 0, 105 | maxTimestamp: 0, 106 | revertingTxHashes: [] 107 | }, 108 | type: "bundle" 109 | } 110 | return payload 111 | } 112 | 113 | const checkBundle = async (payload) => { 114 | var timer = setInterval(async function() { 115 | const hash = ethers.utils.keccak256(payload.data.txs[0]) 116 | const receipt = await simpleProvider.getTransactionReceipt(hash) 117 | if(receipt){ // If the tx has been mined, it returns null if pending 118 | clearInterval(timer) // stop the setInterval once we get a valid receipt 119 | const block = receipt.blockNumber 120 | const balanceBefore = await simpleProvider.getBalance(faucet.address, block - 1) 121 | const balanceAfter = await simpleProvider.getBalance(faucet.address, block) 122 | console.log("Miner before", balanceBefore.toString()) 123 | console.log("Miner after", balanceAfter.toString()) 124 | // subtract 2 for block reward 125 | const profit = balanceAfter.sub(balanceBefore).sub(ethers.utils.parseEther('2')) 126 | console.log("Profit (ETH)", ethers.utils.formatEther(profit)) 127 | console.log("Profit equals bribe?", profit.eq(bribe)) 128 | if(profit.eq(bribe)){ 129 | wss.close() 130 | } 131 | } else{ 132 | console.log("Bundle tx has not been mined yet") 133 | } 134 | }, 5000); 135 | } 136 | 137 | wss.on('connection', async function connection(ws, req){ 138 | ws.isAlive = true; 139 | ws.on('pong', heartbeat) 140 | ws.on('message', message => { 141 | console.log("received message from ws client: " + message) 142 | }) 143 | if(req.headers['x-auth-message']){ 144 | const parsedAuthMessage = JSON.parse(req.headers['x-auth-message']) 145 | console.log(parsedAuthMessage) 146 | if(isValidSignature(parsedAuthMessage.signature, parsedAuthMessage.timestamp)){ 147 | await sleep(1000) 148 | const payload = await generateTestBundle() 149 | ws.send(JSON.stringify(payload)) 150 | await checkBundle(payload) 151 | }else{ 152 | console.log("auth failed") 153 | ws.terminate() 154 | } 155 | }else { 156 | ws.terminate() 157 | } 158 | ws.on("close", m => { 159 | console.log("client closed " + m) 160 | }) 161 | 162 | }) 163 | 164 | // Heartbeat test to see if connection is still alive every 10 seconds 165 | const interval = setInterval(function ping() { 166 | wss.clients.forEach(function each(ws) { 167 | if (ws.isAlive === false) {return ws.terminate()} 168 | ws.isAlive = false; 169 | ws.ping(()=> {console.log("sending: ping")}); 170 | }); 171 | }, 10000); 172 | 173 | wss.on('close', function close() { 174 | clearInterval(interval); 175 | }); 176 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ "es2018" ], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "src/" /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------