Commit 73009b68 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #1334 from ethereum-optimism/develop

Merge develop into master.
parents 05ace3ae 5588c241
---
'@eth-optimism/l2geth': patch
---
Style fix to the ovm state manager precompile
---
'@eth-optimism/l2geth': patch
---
Small fixes to miner codepath
---
'@eth-optimism/l2geth': patch
---
Remove an unnecessary use of `reflect` in l2geth
---
'@eth-optimism/l2geth': patch
---
Remove layer of indirection in `callStateManager`
---
'@eth-optimism/l2geth': patch
---
Update the start script to work with the latest regenesis, `0.4.0`
---
'@eth-optimism/l2geth': patch
---
Return correct value in L2 Geth fee too high error message
---
'@eth-optimism/contracts': patch
---
Add hardhat task for whitelisting addresses
---
'@eth-optimism/contracts': patch
---
Add a hardhat task to withdraw ETH fees from L2 to L1
---
'@eth-optimism/l2geth': patch
---
Delete stateobjects in the miner as blocks are produced to prevent a build up of memory
---
'@eth-optimism/batch-submitter': patch
---
Fix tx resubmission estimateGas bug in batch submitter
---
'@eth-optimism/l2geth': patch
---
Remove diffdb
---
'@eth-optimism/l2geth': patch
---
Quick syntax fix in the sync service
---
'@eth-optimism/replica-healthcheck': minor
---
Add replica-healthcheck to monorepo
---
'@eth-optimism/l2geth': patch
---
Make the extradata deterministic for deterministic block hashes
......@@ -15,4 +15,5 @@
# packages/message-relayer/ @K-Ho
# packages/batch-submitter/ @annieke @karlfloersch
# packages/data-transport-layer/ @annieke
# packages/replica-healthcheck/ @annieke
# integration-tests/ @tynes
......@@ -32,6 +32,7 @@ Extensive documentation is available [here](http://community.optimism.io/docs/).
* [`data-transport-layer`](./packages/data-transport-layer): Event indexer, allowing the `l2geth` node to access L1 data
* [`batch-submitter`](./packages/batch-submitter): Daemon for submitting L2 transaction and state root batches to L1
* [`message-relayer`](./packages/message-relayer): Service for relaying L2 messages to L1
* [`replica-healthcheck`](./packages/replica-healthcheck): Service to monitor the health of different replica deployments
* [`l2geth`](./l2geth): Fork of [go-ethereum v1.9.10](https://github.com/ethereum/go-ethereum/tree/v1.9.10) implementing the [OVM](https://research.paradigm.xyz/optimism#optimistic-geth).
* [`integration-tests`](./integration-tests): Integration tests between a L1 testnet, `l2geth`,
* [`ops`](./ops): Contains Dockerfiles for containerizing each service involved in the protocol,
......
......@@ -8,4 +8,6 @@ require (
github.com/sirupsen/logrus v1.4.2
github.com/ybbus/jsonrpc v2.1.2+incompatible
gopkg.in/alecthomas/kingpin.v2 v2.2.6
k8s.io/apimachinery v0.21.2 // indirect
k8s.io/client-go v0.21.2
)
This diff is collapsed.
package k8sClient
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
func Newk8sClient() (client *kubernetes.Clientset, err error) {
// creates the in-cluster config
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}
// creates the clientset
client, err = kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
return client, nil
}
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"os"
"sync"
"time"
"github.com/ethereum-optimism/optimism/go/op_exporter/k8sClient"
"github.com/ethereum-optimism/optimism/go/op_exporter/version"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"github.com/ybbus/jsonrpc"
"gopkg.in/alecthomas/kingpin.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
var (
......@@ -36,7 +41,10 @@ var (
"wait.minutes",
"Number of minutes to wait for the next block before marking provider unhealthy.",
).Default("10").Int()
//unhealthyTimePeriod = time.Minute * 10
enableK8sQuery = kingpin.Flag(
"k8s.enable",
"Enable kubernetes info lookup.",
).Default("true").Bool()
)
type healthCheck struct {
......@@ -44,13 +52,15 @@ type healthCheck struct {
height uint64
healthy bool
updateTime time.Time
allowedMethods []string
version *string
}
func healthHandler(health *healthCheck) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
health.mu.RLock()
defer health.mu.RUnlock()
w.Write([]byte(fmt.Sprintf(`{ "healthy": "%t" }`, health.healthy)))
w.Write([]byte(fmt.Sprintf(`{ "healthy": "%t", "version": "%s" }`, health.healthy, *health.version)))
}
}
......@@ -71,6 +81,8 @@ func main() {
height: 0,
healthy: false,
updateTime: time.Now(),
allowedMethods: nil,
version: nil,
}
http.Handle("/metrics", promhttp.Handler())
http.Handle("/health", healthHandler(&health))
......@@ -86,6 +98,13 @@ func main() {
})
go getRollupGasPrices()
go getBlockNumber(&health)
if *enableK8sQuery {
client, err := k8sClient.Newk8sClient()
if err != nil {
log.Fatal(err)
}
go getSequencerVersion(&health, client)
}
log.Infoln("Listening on", *listenAddress)
if err := http.ListenAndServe(*listenAddress, nil); err != nil {
log.Fatal(err)
......@@ -93,6 +112,39 @@ func main() {
}
func getSequencerVersion(health *healthCheck, client *kubernetes.Clientset) {
ns, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
if err != nil {
log.Fatalf("Unable to read namespace file: %s", err)
}
ticker := time.NewTicker(30 * time.Second)
for {
<-ticker.C
getOpts := metav1.GetOptions{
TypeMeta: metav1.TypeMeta{},
ResourceVersion: "",
}
sequencerStatefulSet, err := client.AppsV1().StatefulSets(string(ns)).Get(context.TODO(), "sequencer", getOpts)
if err != nil {
unknownStatus := "UNKNOWN"
health.version = &unknownStatus
log.Errorf("Unable to retrieve a sequencer StatefulSet: %s", err)
continue
}
for _, c := range sequencerStatefulSet.Spec.Template.Spec.Containers {
log.Infof("Checking container %s", c.Name)
switch {
case c.Name == "sequencer":
log.Infof("The sequencer version is: %s", c.Image)
health.version = &c.Image
default:
log.Infof("Unable to find the sequencer container in the statefulset?!?")
}
}
}
}
func getBlockNumber(health *healthCheck) {
rpcClient := jsonrpc.NewClientWithOpts(*rpcProvider, &jsonrpc.RPCClientOpts{})
var blockNumberResponse *string
......
......@@ -162,7 +162,6 @@ var (
utils.RollupTimstampRefreshFlag,
utils.RollupPollIntervalFlag,
utils.RollupStateDumpPathFlag,
utils.RollupDiffDbFlag,
utils.RollupMaxCalldataSizeFlag,
utils.RollupBackendFlag,
utils.RollupEnforceFeesFlag,
......
......@@ -77,7 +77,6 @@ var AppHelpFlagGroups = []flagGroup{
utils.RollupTimstampRefreshFlag,
utils.RollupPollIntervalFlag,
utils.RollupStateDumpPathFlag,
utils.RollupDiffDbFlag,
utils.RollupMaxCalldataSizeFlag,
utils.RollupBackendFlag,
utils.RollupEnforceFeesFlag,
......
......@@ -881,12 +881,6 @@ var (
Value: eth.DefaultConfig.Rollup.StateDumpPath,
EnvVar: "ROLLUP_STATE_DUMP_PATH",
}
RollupDiffDbFlag = cli.Uint64Flag{
Name: "rollup.diffdbcache",
Usage: "Number of diffdb batch updates",
Value: eth.DefaultConfig.DiffDbCache,
EnvVar: "ROLLUP_DIFFDB_CACHE",
}
RollupMaxCalldataSizeFlag = cli.IntFlag{
Name: "rollup.maxcalldatasize",
Usage: "Maximum allowed calldata size for Queue Origin Sequencer Txs",
......@@ -1676,9 +1670,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
setEth1(ctx, &cfg.Rollup)
setRollup(ctx, &cfg.Rollup)
if ctx.GlobalIsSet(RollupDiffDbFlag.Name) {
cfg.DiffDbCache = ctx.GlobalUint64(RollupDiffDbFlag.Name)
}
if ctx.GlobalIsSet(SyncModeFlag.Name) {
cfg.SyncMode = *GlobalTextMarshaler(ctx, SyncModeFlag.Name).(*downloader.SyncMode)
}
......
......@@ -36,7 +36,6 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
......@@ -165,8 +164,6 @@ type BlockChain struct {
txLookupCache *lru.Cache // Cache for the most recent transaction lookup data.
futureBlocks *lru.Cache // future blocks are blocks added for later processing
diffdb state.DiffDB // Diff
quit chan struct{} // blockchain quit channel
running int32 // running must be called atomically
// procInterrupt must be atomically called
......@@ -184,20 +181,6 @@ type BlockChain struct {
terminateInsert func(common.Hash, uint64) bool // Testing hook used to terminate ancient receipt chain insertion.
}
func NewBlockChainWithDiffDb(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(block *types.Block) bool, path string, cache uint64) (*BlockChain, error) {
diff, err := diffdb.NewDiffDb(path, cache)
if err != nil {
return nil, err
}
bc, err := NewBlockChain(db, cacheConfig, chainConfig, engine, vmConfig, shouldPreserve)
if err != nil {
return nil, err
}
bc.diffdb = diff
return bc, nil
}
// NewBlockChain returns a fully initialised block chain using information
// available in the database. It initialises the default Ethereum Validator and
// Processor.
......@@ -365,7 +348,7 @@ func (bc *BlockChain) loadLastState() error {
return bc.Reset()
}
// Make sure the state associated with the block is available
if _, err := state.NewWithDiffDb(currentBlock.Root(), bc.stateCache, bc.diffdb); err != nil {
if _, err := state.New(currentBlock.Root(), bc.stateCache); err != nil {
// Dangling block without a state associated, init from scratch
log.Warn("Head state missing, repairing chain", "number", currentBlock.Number(), "hash", currentBlock.Hash())
if err := bc.repair(&currentBlock); err != nil {
......@@ -427,7 +410,7 @@ func (bc *BlockChain) SetHead(head uint64) error {
if newHeadBlock == nil {
newHeadBlock = bc.genesisBlock
} else {
if _, err := state.NewWithDiffDb(newHeadBlock.Root(), bc.stateCache, bc.diffdb); err != nil {
if _, err := state.New(newHeadBlock.Root(), bc.stateCache); err != nil {
// Rewound state missing, rolled back to before pivot, reset to genesis
newHeadBlock = bc.genesisBlock
}
......@@ -543,11 +526,6 @@ func (bc *BlockChain) SetCurrentBlock(block *types.Block) {
bc.currentBlock.Store(block)
}
// GetDiff retrieves the diffdb's state diff keys for a block
func (bc *BlockChain) GetDiff(block *big.Int) (diffdb.Diff, error) {
return bc.diffdb.GetDiff(block)
}
// CurrentFastBlock retrieves the current fast-sync head block of the canonical
// chain. The block is retrieved from the blockchain's internal cache.
func (bc *BlockChain) CurrentFastBlock() *types.Block {
......@@ -571,7 +549,7 @@ func (bc *BlockChain) State() (*state.StateDB, error) {
// StateAt returns a new mutable state based on a particular point in time.
func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
return state.NewWithDiffDb(root, bc.stateCache, bc.diffdb)
return state.New(root, bc.stateCache)
}
// StateCache returns the caching database underpinning the blockchain instance.
......@@ -623,7 +601,7 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error {
func (bc *BlockChain) repair(head **types.Block) error {
for {
// Abort if we've rewound to a head block that does have associated state
if _, err := state.NewWithDiffDb((*head).Root(), bc.stateCache, bc.diffdb); err == nil {
if _, err := state.New((*head).Root(), bc.stateCache); err == nil {
log.Info("Rewound blockchain to past state", "number", (*head).Number(), "hash", (*head).Hash())
return nil
}
......@@ -912,14 +890,6 @@ func (bc *BlockChain) Stop() {
}
}
if bc.diffdb != nil {
if err := bc.diffdb.ForceCommit(); err != nil {
log.Error("Failed to commit recent state diffs", "err", err)
}
if err := bc.diffdb.Close(); err != nil {
log.Error("Failed to commit state diffs handler", "err", err)
}
}
log.Info("Blockchain manager stopped")
}
......@@ -1709,7 +1679,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er
if parent == nil {
parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1)
}
statedb, err := state.NewWithDiffDb(parent.Root, bc.stateCache, bc.diffdb)
statedb, err := state.New(parent.Root, bc.stateCache)
if err != nil {
return it.index, err
}
......
......@@ -27,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/rlp"
......@@ -59,15 +58,6 @@ func (n *proofList) Delete(key []byte) error {
panic("not supported")
}
// DiffDb is a database for storing state diffs per block
type DiffDB interface {
SetDiffKey(*big.Int, common.Address, common.Hash, bool) error
SetDiffAccount(*big.Int, common.Address) error
GetDiff(*big.Int) (diffdb.Diff, error)
Close() error
ForceCommit() error
}
// StateDBs within the ethereum protocol are used to store anything
// within the merkle trie. StateDBs take care of caching and storing
// nested states. It's the general query interface to retrieve:
......@@ -77,8 +67,6 @@ type StateDB struct {
db Database
trie Trie
diffdb DiffDB
// This map holds 'live' objects, which will get modified while processing a state transition.
stateObjects map[common.Address]*stateObject
stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie
......@@ -136,15 +124,6 @@ func New(root common.Hash, db Database) (*StateDB, error) {
}, nil
}
func NewWithDiffDb(root common.Hash, db Database, diffdb DiffDB) (*StateDB, error) {
res, err := New(root, db)
if err != nil {
return nil, err
}
res.diffdb = diffdb
return res, nil
}
// setError remembers the first non-nil error it is called with.
func (s *StateDB) setError(err error) {
if s.dbErr == nil {
......@@ -152,20 +131,6 @@ func (s *StateDB) setError(err error) {
}
}
func (s *StateDB) SetDiffKey(block *big.Int, address common.Address, key common.Hash, mutated bool) error {
if s.diffdb == nil {
return errors.New("DiffDB not set")
}
return s.diffdb.SetDiffKey(block, address, key, mutated)
}
func (s *StateDB) SetDiffAccount(block *big.Int, address common.Address) error {
if s.diffdb == nil {
return errors.New("DiffDB not set")
}
return s.diffdb.SetDiffAccount(block, address)
}
func (s *StateDB) Error() error {
return s.dbErr
}
......
......@@ -64,8 +64,6 @@ type StateDB interface {
AddPreimage(common.Hash, []byte)
ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error
SetDiffKey(block *big.Int, address common.Address, key common.Hash, mutated bool) error
SetDiffAccount(block *big.Int, address common.Address) error
}
// CallContext provides a basic interface for the EVM calling conventions. The EVM
......
......@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"math/big"
"reflect"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -34,17 +33,13 @@ var funcs = map[string]stateManagerFunction{
}
func callStateManager(input []byte, evm *EVM, contract *Contract) (ret []byte, err error) {
rawabi := evm.Context.OvmStateManager.ABI
abi := &rawabi
method, err := abi.MethodById(input)
method, err := evm.Context.OvmStateManager.ABI.MethodById(input)
if err != nil {
return nil, fmt.Errorf("cannot find method id %s: %w", input, err)
}
var inputArgs = make(map[string]interface{})
err = method.Inputs.UnpackIntoMap(inputArgs, input[4:])
if err != nil {
if err := method.Inputs.UnpackIntoMap(inputArgs, input[4:]); err != nil {
return nil, err
}
......@@ -90,7 +85,7 @@ func getAccountNonce(evm *EVM, contract *Contract, args map[string]interface{})
return nil, errors.New("Could not parse address arg in getAccountNonce")
}
nonce := evm.StateDB.GetNonce(address)
return []interface{}{new(big.Int).SetUint64(reflect.ValueOf(nonce).Uint())}, nil
return []interface{}{new(big.Int).SetUint64(nonce)}, nil
}
func getAccountEthAddress(evm *EVM, contract *Contract, args map[string]interface{}) ([]interface{}, error) {
......@@ -133,46 +128,14 @@ func putContractStorage(evm *EVM, contract *Contract, args map[string]interface{
return nil, errors.New("Could not parse value arg in putContractStorage")
}
val := toHash(_value)
// save the block number and address with modified key if it's not an eth_call
if evm.Context.EthCallSender == nil {
// save the value before
before := evm.StateDB.GetState(address, key)
evm.StateDB.SetState(address, key, val)
err := evm.StateDB.SetDiffKey(
evm.Context.BlockNumber,
address,
key,
before != val,
)
if err != nil {
log.Error("Cannot set diff key", "err", err)
}
log.Debug("Put contract storage", "address", address.Hex(), "key", key.Hex(), "val", val.Hex())
} else {
// otherwise just do the db update
evm.StateDB.SetState(address, key, val)
}
evm.StateDB.SetState(address, key, val)
return []interface{}{}, nil
}
func testAndSetAccount(evm *EVM, contract *Contract, args map[string]interface{}) ([]interface{}, error) {
address, ok := args["_address"].(common.Address)
if !ok {
return nil, errors.New("Could not parse address arg in putContractStorage")
}
if evm.Context.EthCallSender == nil {
err := evm.StateDB.SetDiffAccount(
evm.Context.BlockNumber,
address,
)
if err != nil {
log.Error("Cannot set account diff", "err", err)
}
}
return []interface{}{true}, nil
}
......@@ -196,15 +159,6 @@ func testAndSetContractStorage(evm *EVM, contract *Contract, args map[string]int
key := toHash(_key)
if evm.Context.EthCallSender == nil {
err := evm.StateDB.SetDiffKey(
evm.Context.BlockNumber,
address,
key,
changed,
)
if err != nil {
log.Error("Cannot set diff key", "err", err)
}
log.Debug("Test and Set Contract Storage", "address", address.Hex(), "key", key.Hex(), "changed", changed)
}
......
package vm
import (
"crypto/rand"
"math/big"
"os"
"sort"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/params"
)
type TestData map[*big.Int]BlockData
// per-block test data are an address + a bunch of k/v pairs
type BlockData map[common.Address][]ContractData
// keys and values are bytes32 in solidity
type ContractData struct {
key [32]uint8
value [32]uint8
mutated bool
}
// Test contract addrs
var (
contract1 = common.HexToAddress("0x000000000000000000000000000000000001")
contract2 = common.HexToAddress("0x000000000000000000000000000000000002")
)
func makeEnv(dbname string) (*diffdb.DiffDb, *EVM, TestData, *Contract) {
db, _ := diffdb.NewDiffDb(dbname, 1)
mock := &mockDb{db: *db}
env := NewEVM(Context{}, mock, params.TestChainConfig, Config{})
// re-use `dummyContractRef` from `logger_test.go`
contract := NewContract(&dummyContractRef{}, &dummyContractRef{}, new(big.Int), 0)
testData := make(TestData)
return db, env, testData, contract
}
func TestEthCallNoop(t *testing.T) {
db, env, _, contract := makeEnv("test1")
defer os.Remove("test1")
env.Context.EthCallSender = &common.Address{0}
env.Context.BlockNumber = big.NewInt(1)
args := map[string]interface{}{
"_contract": contract1,
"_key": [32]uint8{1},
"_value": [32]uint8{2},
}
putContractStorage(env, contract, args)
diff, err := db.GetDiff(env.Context.BlockNumber)
if err != nil {
t.Fatal("Db call error", err)
}
if len(diff) > 0 {
t.Fatalf("map must be empty since it was an eth call")
}
}
func TestSetDiffs(t *testing.T) {
db, env, testData, contract := makeEnv("test2")
defer os.Remove("test2")
// not an eth-call
env.Context.EthCallSender = nil
// in block 1 both contracts get touched
blockNumber := big.NewInt(5)
testData.addRandomData(blockNumber, contract1, 5)
testData.addRandomData(blockNumber, contract2, 10)
// in another block, only 1 contract gets touched
blockNumber2 := big.NewInt(6)
testData.addRandomData(blockNumber2, contract2, 10)
// insert the data in the diffdb via `putContractStorage` calls
putTestData(t, env, contract, blockNumber, testData)
// diffs match
diff, _ := db.GetDiff(blockNumber)
expected := getExpected(testData[blockNumber])
if !DiffsEqual(diff, expected) {
t.Fatalf("Diff did not match.")
}
// empty diff for the next block
diff2, err := db.GetDiff(blockNumber2)
if err != nil {
t.Fatal("Db call error", err)
}
if len(diff2) != 0 {
t.Fatalf("Diff2 should be empty since data about the next block is not added yet")
}
// insert the data and get the diff again
putTestData(t, env, contract, blockNumber2, testData)
expected2 := getExpected(testData[blockNumber2])
diff2, err = db.GetDiff(blockNumber2)
if err != nil {
t.Fatal("Db call error", err)
}
if !DiffsEqual(diff2, expected2) {
t.Fatalf("Diff did not match.")
}
}
/// Sorted equality between 2 diffs
func DiffsEqual(d1 diffdb.Diff, d2 diffdb.Diff) bool {
for k, v := range d1 {
sort.SliceStable(v, func(i, j int) bool {
return v[i].Key.Big().Cmp(v[j].Key.Big()) < 0
})
sort.SliceStable(d2[k], func(i, j int) bool {
return d2[k][i].Key.Big().Cmp(d2[k][j].Key.Big()) < 0
})
exp := d2[k]
for i, v2 := range v {
if exp[i] != v2 {
return false
}
}
}
return true
}
// inserts a bunch of data for the provided `blockNumber` for all contracts touched in that block
func putTestData(t *testing.T, env *EVM, contract *Contract, blockNumber *big.Int, testData TestData) {
blockData := testData[blockNumber]
env.Context.BlockNumber = blockNumber
for address, data := range blockData {
for _, contractData := range data {
args := map[string]interface{}{
"_contract": address,
"_key": contractData.key,
"_value": contractData.value,
}
_, err := putContractStorage(env, contract, args)
if err != nil {
t.Fatalf("Expected nil error, got %s", err)
}
}
}
}
// creates `num` random k/v entries for `contract`'s address at `blockNumber`
func (data TestData) addRandomData(blockNumber *big.Int, contract common.Address, num int) {
for i := 0; i < num; i++ {
val := ContractData{
key: randBytes(),
value: randBytes(),
mutated: true,
}
// alloc empty blockdata
if data[blockNumber] == nil {
data[blockNumber] = make(BlockData)
}
data[blockNumber][contract] = append(data[blockNumber][contract], val)
}
}
// the expected diff for the GetDiff call contains the data's keys only, the values & proofs
// are fetched via GetProof
func getExpected(testData BlockData) diffdb.Diff {
res := make(diffdb.Diff)
for address, data := range testData {
for _, contractData := range data {
key := diffdb.Key{
Key: contractData.key,
Mutated: contractData.mutated,
}
res[address] = append(res[address], key)
}
}
return res
}
// creates a random 32 byte array
func randBytes() [32]uint8 {
bytes := make([]uint8, 32)
rand.Read(bytes)
var res [32]uint8
copy(res[:], bytes)
return res
}
// Mock everything else
type mockDb struct {
db diffdb.DiffDb
}
func (mock *mockDb) SetDiffKey(block *big.Int, address common.Address, key common.Hash, mutated bool) error {
mock.db.SetDiffKey(block, address, key, mutated)
return nil
}
func (mock *mockDb) SetDiffAccount(block *big.Int, address common.Address) error {
// mock.db.SetDiffAccount(block, address)
return nil
}
func (mock *mockDb) CreateAccount(common.Address) {}
func (mock *mockDb) SubBalance(common.Address, *big.Int) {}
func (mock *mockDb) AddBalance(common.Address, *big.Int) {}
func (mock *mockDb) GetBalance(common.Address) *big.Int { return big.NewInt(0) }
func (mock *mockDb) GetNonce(common.Address) uint64 { return 0 }
func (mock *mockDb) SetNonce(common.Address, uint64) {}
func (mock *mockDb) GetCodeHash(common.Address) common.Hash { return common.Hash{} }
func (mock *mockDb) GetCode(common.Address) []byte { return []byte{} }
func (mock *mockDb) SetCode(common.Address, []byte) {}
func (mock *mockDb) GetCodeSize(common.Address) int { return 0 }
func (mock *mockDb) AddRefund(uint64) {}
func (mock *mockDb) SubRefund(uint64) {}
func (mock *mockDb) GetRefund() uint64 { return 0 }
func (mock *mockDb) GetCommittedState(common.Address, common.Hash) common.Hash { return common.Hash{} }
func (mock *mockDb) GetState(common.Address, common.Hash) common.Hash { return common.Hash{} }
func (mock *mockDb) SetState(common.Address, common.Hash, common.Hash) {}
func (mock *mockDb) Suicide(common.Address) bool { return true }
func (mock *mockDb) HasSuicided(common.Address) bool { return true }
func (mock *mockDb) Exist(common.Address) bool { return true }
func (mock *mockDb) Empty(common.Address) bool { return true }
func (mock *mockDb) RevertToSnapshot(int) {}
func (mock *mockDb) Snapshot() int { return 0 }
func (mock *mockDb) AddLog(*types.Log) {}
func (mock *mockDb) AddPreimage(common.Hash, []byte) {}
func (mock *mockDb) ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error {
return nil
}
package diffdb
import (
"github.com/ethereum/go-ethereum/common"
_ "github.com/mattn/go-sqlite3"
"database/sql"
"math/big"
)
type Key struct {
Key common.Hash
Mutated bool
}
type Diff map[common.Address][]Key
/// A DiffDb is a thin wrapper around an Sqlite3 connection.
///
/// Its purpose is to store and fetch the storage keys corresponding to an address that was
/// touched in a block.
type DiffDb struct {
db *sql.DB
tx *sql.Tx
stmt *sql.Stmt
cache uint64
// We have a db-wide counter for the number of db calls made which we reset
// whenever it hits `cache`.
numCalls uint64
}
/// This key is used to mark that an account's state has been modified (e.g. nonce or balance)
/// and that an account proof is required.
var accountKey = common.HexToHash("0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
var insertStatement = `
INSERT INTO diffs
(block, address, key, mutated)
VALUES
($1, $2, $3, $4)
ON CONFLICT DO NOTHING
`
var createStmt = `
CREATE TABLE IF NOT EXISTS diffs (
block INTEGER,
address STRING,
key STRING,
mutated BOOL,
PRIMARY KEY (block, address, key)
)
`
var selectStmt = `
SELECT * from diffs WHERE block = $1
`
/// Inserts a new row to the sqlite with the provided diff data.
func (diff *DiffDb) SetDiffKey(block *big.Int, address common.Address, key common.Hash, mutated bool) error {
// add 1 more insertion to the transaction
_, err := diff.stmt.Exec(block.Uint64(), address, key, mutated)
if err != nil {
return err
}
// increment number of calls
diff.numCalls += 1
// if we had enough calls, commit it
if diff.numCalls >= diff.cache {
if err := diff.ForceCommit(); err != nil {
return err
}
}
return nil
}
/// Inserts a new row to the sqlite indicating that the account was modified in that block
/// at a pre-set key
func (diff *DiffDb) SetDiffAccount(block *big.Int, address common.Address) error {
return diff.SetDiffKey(block, address, accountKey, true)
}
/// Commits a pending diffdb transaction
func (diff *DiffDb) ForceCommit() error {
if err := diff.tx.Commit(); err != nil {
return err
}
return diff.resetTx()
}
/// Gets all the rows for the matching block and converts them to a Diff map.
func (diff *DiffDb) GetDiff(blockNum *big.Int) (Diff, error) {
// make the query
rows, err := diff.db.Query(selectStmt, blockNum.Uint64())
if err != nil {
return nil, err
}
// initialize our data
res := make(Diff)
var block uint64
var address common.Address
var key common.Hash
var mutated bool
for rows.Next() {
// deserialize the line
err = rows.Scan(&block, &address, &key, &mutated)
if err != nil {
return nil, err
}
// add the data to the map
res[address] = append(res[address], Key{key, mutated})
}
return res, rows.Err()
}
// Initializes the transaction which we will be using to commit data to the db
func (diff *DiffDb) resetTx() error {
// reset the number of calls made
diff.numCalls = 0
// start a new tx
tx, err := diff.db.Begin()
if err != nil {
return err
}
diff.tx = tx
// the tx is about inserts
stmt, err := diff.tx.Prepare(insertStatement)
if err != nil {
return err
}
diff.stmt = stmt
return nil
}
func (diff *DiffDb) Close() error {
return diff.db.Close()
}
/// Instantiates a new DiffDb using sqlite at `path`, with `cache` insertions
/// done in a transaction before it gets committed to the database.
func NewDiffDb(path string, cache uint64) (*DiffDb, error) {
// get a handle
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
// create the table if it does not exist
_, err = db.Exec(createStmt)
if err != nil {
return nil, err
}
diffdb := &DiffDb{db: db, cache: cache}
// initialize the transaction
if err := diffdb.resetTx(); err != nil {
return nil, err
}
return diffdb, nil
}
package diffdb
import (
"math/big"
"os"
"testing"
"github.com/ethereum/go-ethereum/common"
)
func TestDiffDb(t *testing.T) {
db, err := NewDiffDb("./test_diff.db", 3)
// cleanup (sqlite will create the file if it doesn't exist)
defer os.Remove("./test_diff.db")
if err != nil {
t.Fatal(err)
}
hashes := []common.Hash{
common.Hash{0x0},
common.Hash{0x1},
common.Hash{0x2},
}
addr := common.Address{0x1}
db.SetDiffKey(big.NewInt(1), common.Address{0x1, 0x2}, common.Hash{0x12, 0x13}, false)
db.SetDiffKey(big.NewInt(1), addr, hashes[0], false)
db.SetDiffKey(big.NewInt(1), addr, hashes[1], false)
db.SetDiffKey(big.NewInt(1), addr, hashes[2], false)
db.SetDiffKey(big.NewInt(1), common.Address{0x2}, common.Hash{0x99}, false)
db.SetDiffKey(big.NewInt(2), common.Address{0x2}, common.Hash{0x98}, true)
// try overwriting, ON CONFLICT clause gets hit
err = db.SetDiffKey(big.NewInt(2), common.Address{0x2}, common.Hash{0x98}, false)
if err != nil {
t.Fatal("should be able to resolve conflict without error at the sql level")
}
diff, err := db.GetDiff(big.NewInt(1))
if err != nil {
t.Fatal("Did not expect error")
}
for i := range hashes {
if hashes[i] != diff[addr][i].Key {
t.Fatal("Did not match", hashes[i], "got", diff[addr][i].Key)
}
}
diff, _ = db.GetDiff(big.NewInt(2))
if diff[common.Address{0x2}][0].Mutated != true {
t.Fatalf("Did not match mutated")
}
}
......@@ -31,7 +31,6 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/ethdb"
......@@ -97,10 +96,6 @@ func (b *EthAPIBackend) CurrentBlock() *types.Block {
return b.eth.blockchain.CurrentBlock()
}
func (b *EthAPIBackend) GetDiff(block *big.Int) (diffdb.Diff, error) {
return b.eth.blockchain.GetDiff(block)
}
func (b *EthAPIBackend) SetHead(number uint64) {
if number == 0 {
log.Info("Cannot reset to genesis")
......
......@@ -22,7 +22,6 @@ import (
"errors"
"fmt"
"math/big"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
......@@ -190,9 +189,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
}
)
// Save the diffdb under chaindata/diffdb
diffdbPath := filepath.Join(ctx.ResolvePath("chaindata"), "diffdb")
eth.blockchain, err = core.NewBlockChainWithDiffDb(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, diffdbPath, config.DiffDbCache)
eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve)
if err != nil {
return nil, err
}
......@@ -241,6 +238,16 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
}
func makeExtraData(extra []byte) []byte {
if vm.UsingOVM {
// Make the extradata deterministic
extra, _ = rlp.EncodeToBytes([]interface{}{
uint(params.VersionMajor<<16 | params.VersionMinor<<8 | params.VersionPatch),
"geth",
"go1.15.13",
"linux",
})
return extra
}
if len(extra) == 0 {
// create default extradata
extra, _ = rlp.EncodeToBytes([]interface{}{
......
......@@ -82,7 +82,6 @@ var DefaultConfig = Config{
// safety.
MaxCallDataSize: 127000,
},
DiffDbCache: 256,
}
func init() {
......@@ -139,7 +138,6 @@ type Config struct {
DatabaseHandles int `toml:"-"`
DatabaseCache int
DatabaseFreezer string
DiffDbCache uint64
TrieCleanCache int
TrieDirtyCache int
......
......@@ -40,7 +40,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/params"
......@@ -571,61 +570,6 @@ type HeaderMeta struct {
Timestamp uint64 `json:"timestamp"`
}
func (s *PublicBlockChainAPI) GetStateDiff(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (diffdb.Diff, error) {
_, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return nil, err
}
return s.b.GetDiff(new(big.Int).Add(header.Number, big.NewInt(1)))
}
// GetStateDiffProof returns the Merkle-proofs corresponding to all the accounts and
// storage slots which were touched for a given block number or hash.
func (s *PublicBlockChainAPI) GetStateDiffProof(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*StateDiffProof, error) {
state, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || header == nil || err != nil {
return nil, err
}
// get the changed accounts for this block
diffs, err := s.GetStateDiff(ctx, blockNrOrHash)
if err != nil {
return nil, err
}
// for each changed account, get their proof
var accounts []AccountResult
for address, keys := range diffs {
// need to convert the hashes to strings, we could maybe refactor getProof
// alternatively
keyStrings := make([]string, len(keys))
for i, key := range keys {
keyStrings[i] = key.Key.String()
}
// get the proofs
res, err := s.GetProof(ctx, address, keyStrings, blockNrOrHash)
if err != nil {
return nil, err
}
accounts = append(accounts, *res)
}
// add some metadata
stateDiffProof := &StateDiffProof{
Header: &HeaderMeta{
Number: header.Number,
Hash: header.Hash(),
StateRoot: header.Root,
Timestamp: header.Time,
},
Accounts: accounts,
}
return stateDiffProof, state.Error()
}
// GetProof returns the Merkle-proof for a given account and optionally some storage keys.
func (s *PublicBlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) {
state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
......
......@@ -28,7 +28,6 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
......@@ -93,7 +92,6 @@ type Backend interface {
GetEthContext() (uint64, uint64)
GetRollupContext() (uint64, uint64, uint64)
GasLimit() uint64
GetDiff(*big.Int) (diffdb.Diff, error)
SuggestL1GasPrice(ctx context.Context) (*big.Int, error)
SetL1GasPrice(context.Context, *big.Int) error
SuggestL2GasPrice(context.Context) (*big.Int, error)
......
......@@ -30,7 +30,6 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/diffdb"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/ethdb"
......@@ -78,10 +77,6 @@ func (b *LesApiBackend) CurrentBlock() *types.Block {
return types.NewBlockWithHeader(b.eth.BlockChain().CurrentHeader())
}
func (b *LesApiBackend) GetDiff(*big.Int) (diffdb.Diff, error) {
return nil, errors.New("Diffs not supported in light client mode")
}
func (b *LesApiBackend) SetHead(number uint64) {
b.eth.handler.downloader.Cancel()
b.eth.blockchain.SetHead(number)
......
......@@ -355,6 +355,16 @@ func (w *worker) newWorkLoop(recommit time.Duration) {
timestamp = w.chain.CurrentTimestamp()
commit(false, commitInterruptNewHead)
// Remove this code for the OVM implementation. It is responsible for
// cleaning up memory with the call to `clearPending`, so be sure to
// call that in the new hot code path
/*
case <-w.chainHeadCh:
clearPending(head.Block.NumberU64())
timestamp = time.Now().Unix()
commit(false, commitInterruptNewHead)
*/
case <-timer.C:
// If mining is running resubmit a new work cycle periodically to pull in
// higher priced transactions. Disable this overhead for pending blocks.
......@@ -464,7 +474,18 @@ func (w *worker) mainLoop() {
}
tx := ev.Txs[0]
log.Debug("Attempting to commit rollup transaction", "hash", tx.Hash().Hex())
// Build the block with the tx and add it to the chain. This will
// send the block through the `taskCh` and then through the
// `resultCh` which ultimately adds the block to the blockchain
// through `bc.WriteBlockWithState`
if err := w.commitNewTx(tx); err == nil {
// `chainHeadCh` is written to when a new block is added to the
// tip of the chain. Reading from the channel will block until
// the ethereum block is added to the chain downstream of `commitNewTx`.
// This will result in a deadlock if we call `commitNewTx` with
// a transaction that cannot be added to the chain, so this
// should be updated to a select statement that can also listen
// for errors.
head := <-w.chainHeadCh
txs := head.Block.Transactions()
if len(txs) == 0 {
......@@ -474,6 +495,18 @@ func (w *worker) mainLoop() {
txn := txs[0]
height := head.Block.Number().Uint64()
log.Debug("Miner got new head", "height", height, "block-hash", head.Block.Hash().Hex(), "tx-hash", txn.Hash().Hex(), "tx-hash", tx.Hash().Hex())
// Prevent memory leak by cleaning up pending tasks
// This is mostly copied from the `newWorkLoop`
// `clearPending` function and must be called
// periodically to clean up pending tasks. This
// function was originally called in `newWorkLoop`
// but the OVM implementation no longer uses that code path.
w.pendingMu.Lock()
for h := range w.pendingTasks {
delete(w.pendingTasks, h)
}
w.pendingMu.Unlock()
} else {
log.Debug("Problem committing transaction: %w", err)
}
......@@ -895,7 +928,7 @@ func (w *worker) commitNewTx(tx *types.Transaction) error {
}
header := &types.Header{
ParentHash: parent.Hash(),
Number: num.Add(num, common.Big1),
Number: new(big.Int).Add(num, common.Big1),
GasLimit: w.config.GasFloor,
Extra: w.extra,
Time: tx.L1Timestamp(),
......@@ -915,7 +948,7 @@ func (w *worker) commitNewTx(tx *types.Transaction) error {
if w.commitTransactions(txs, w.coinbase, nil) {
return errors.New("Cannot commit transaction in miner")
}
return w.commit([]*types.Header{}, w.fullTaskHook, true, tstart)
return w.commit(nil, w.fullTaskHook, true, tstart)
}
// commitNewWork generates several new sealing tasks based on the parent block.
......@@ -1053,6 +1086,8 @@ func (w *worker) commit(uncles []*types.Header, interval func(), update bool, st
if interval != nil {
interval()
}
// Writing to the taskCh will result in the block being added to the
// chain via the resultCh
select {
case w.taskCh <- &task{receipts: receipts, state: s, block: block, createdAt: time.Now()}:
w.unconfirmed.Shift(block.NumberU64() - 1)
......
......@@ -880,7 +880,7 @@ func (s *SyncService) verifyFee(tx *types.Transaction) error {
}
if errors.Is(err, fees.ErrFeeTooHigh) {
return fmt.Errorf("%w: %d, use less than %d * %f", fees.ErrFeeTooHigh, userFee,
expectedFee, s.feeThresholdDown)
expectedFee, s.feeThresholdUp)
}
return err
}
......@@ -1092,7 +1092,7 @@ func (s *SyncService) syncTransactionRange(start, end uint64, backend Backend) e
if err != nil {
return fmt.Errorf("cannot fetch transaction %d: %w", i, err)
}
if err = s.applyTransaction(tx); err != nil {
if err := s.applyTransaction(tx); err != nil {
return fmt.Errorf("Cannot apply transaction: %w", err)
}
}
......
......@@ -8,11 +8,11 @@ ROLLUP_SYNC_SERVICE_ENABLE=true
DATADIR=$HOME/.ethereum
TARGET_GAS_LIMIT=11000000
CHAIN_ID=10
ETH1_CTC_DEPLOYMENT_HEIGHT=12410807
ETH1_L1_STANDARD_BRIDGE_ADDRESS=0xe681F80966a8b1fFadECf8068bD6F99034791c95
ETH1_L1_CROSS_DOMAIN_MESSENGER_ADDRESS=0x902e5fF5A99C4eC1C21bbab089fdabE32EF0A5DF
ETH1_CTC_DEPLOYMENT_HEIGHT=12686738
ETH1_L1_STANDARD_BRIDGE_ADDRESS=0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1
ETH1_L1_CROSS_DOMAIN_MESSENGER_ADDRESS=0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1
ADDRESS_MANAGER_OWNER_ADDRESS=0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A
ROLLUP_STATE_DUMP_PATH=https://storage.googleapis.com/optimism/mainnet/4.json
ROLLUP_STATE_DUMP_PATH=https://storage.googleapis.com/optimism/mainnet/0.4.0.json
ROLLUP_CLIENT_HTTP=http://localhost:7878
ROLLUP_POLL_INTERVAL=15s
ROLLUP_TIMESTAMP_REFRESH=3m
......@@ -20,7 +20,9 @@ CACHE=1024
RPC_PORT=8545
WS_PORT=8546
VERBOSITY=3
ROLLUP_BACKEND=l1
ROLLUP_BACKEND=l2
ROLLUP_GAS_PRICE_ORACLE_OWNER_ADDRESS=0x648E3e8101BFaB7bf5997Bd007Fb473786019159
ETH1_L1_FEE_WALLET_ADDRESS=0x391716d440c151c42cdf1c95c1d83a5427bca52c
USAGE="
Start the Sequencer or Verifier with most configuration pre-set.
......@@ -240,6 +242,7 @@ cmd="$cmd --rollup.clienthttp $ROLLUP_CLIENT_HTTP"
cmd="$cmd --rollup.pollinterval $ROLLUP_POLL_INTERVAL"
cmd="$cmd --rollup.timestamprefresh $ROLLUP_TIMESTAMP_REFRESH"
cmd="$cmd --rollup.backend $ROLLUP_BACKEND"
cmd="$cmd --rollup.gaspriceoracleowneraddress $ROLLUP_GAS_PRICE_ORACLE_OWNER_ADDRESS"
cmd="$cmd --cache $CACHE"
cmd="$cmd --rpc"
cmd="$cmd --dev"
......
# TODO: Prefix all env vars with service name
# TODO: Allow specifing the image tag to use
version: "3"
services:
# base service builder
builder:
image: ethereumoptimism/builder
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.monorepo
# this is a helper service used because there's no official hardhat image
l1_chain:
image: ethereumoptimism/hardhat
# build:
# context: ./docker/hardhat
# dockerfile: Dockerfile
ports:
# expose the service to the host for integration testing
- ${L1CHAIN_HTTP_PORT:-9545}:8545
deployer:
depends_on:
- l1_chain
image: ethereumoptimism/deployer
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.deployer
entrypoint: ./deployer.sh
environment:
FRAUD_PROOF_WINDOW_SECONDS: 0
L1_NODE_WEB3_URL: http://l1_chain:8545
# these keys are hardhat's first 2 accounts, DO NOT use in production
DEPLOYER_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
SEQUENCER_PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
# skip compilation when run in docker-compose, since the contracts
# were already compiled in the builder step
NO_COMPILE: 1
ports:
# expose the service to the host for getting the contract addrs
- ${DEPLOYER_PORT:-8080}:8081
dtl:
depends_on:
- l1_chain
- deployer
- l2geth
image: ethereumoptimism/data-transport-layer
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.data-transport-layer
# override with the dtl script and the env vars required for it
entrypoint: ./dtl.sh
env_file:
- ./envs/dtl.env
# set the rest of the env vars for the network whcih do not
# depend on the docker-compose setup
environment:
# used for setting the address manager address
URL: http://deployer:8081/addresses.json
# connect to the 2 layers
DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT: http://l1_chain:8545
DATA_TRANSPORT_LAYER__L2_RPC_ENDPOINT: http://l2geth:8545
DATA_TRANSPORT_LAYER__SYNC_FROM_L2: 'true'
DATA_TRANSPORT_LAYER__L2_CHAIN_ID: 420
ports:
- ${DTL_PORT:-7878}:7878
l2geth:
depends_on:
- l1_chain
- deployer
image: ethereumoptimism/l2geth
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.geth
# override with the geth script and the env vars required for it
entrypoint: sh ./geth.sh
env_file:
- ./envs/geth.env
environment:
ETH1_HTTP: http://l1_chain:8545
ROLLUP_TIMESTAMP_REFRESH: 5s
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
# used for getting the addresses
URL: http://deployer:8081/addresses.json
# connecting to the DTL
ROLLUP_CLIENT_HTTP: http://dtl:7878
ETH1_CTC_DEPLOYMENT_HEIGHT: 8
RETRIES: 60
ports:
- ${L2GETH_HTTP_PORT:-8545}:8545
- ${L2GETH_WS_PORT:-8546}:8546
relayer:
depends_on:
- l1_chain
- deployer
- l2geth
image: ethereumoptimism/message-relayer
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.message-relayer
entrypoint: ./relayer.sh
environment:
L1_NODE_WEB3_URL: http://l1_chain:8545
L2_NODE_WEB3_URL: http://l2geth:8545
URL: http://deployer:8081/addresses.json
# a funded hardhat account
L1_WALLET_KEY: "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97"
RETRIES: 60
POLLING_INTERVAL: 500
GET_LOGS_INTERVAL: 500
batch_submitter:
depends_on:
- l1_chain
- deployer
- l2geth
image: ethereumoptimism/batch-submitter
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.batch-submitter
entrypoint: ./batches.sh
env_file:
- ./envs/batches.env
environment:
L1_NODE_WEB3_URL: http://l1_chain:8545
L2_NODE_WEB3_URL: http://l2geth:8545
URL: http://deployer:8081/addresses.json
SEQUENCER_PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
verifier:
depends_on:
- l1_chain
- deployer
- dtl
image: ethereumoptimism/l2geth
deploy:
replicas: 0
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.geth
entrypoint: sh ./geth.sh
env_file:
- ./envs/geth.env
environment:
ETH1_HTTP: http://l1_chain:8545
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
URL: http://deployer:8081/addresses.json
ROLLUP_CLIENT_HTTP: http://dtl:7878
ROLLUP_BACKEND: 'l1'
ETH1_CTC_DEPLOYMENT_HEIGHT: 8
RETRIES: 60
ROLLUP_VERIFIER_ENABLE: 'true'
ports:
- ${VERIFIER_HTTP_PORT:-8547}:8545
- ${VERIFIER_WS_PORT:-8548}:8546
replica:
depends_on:
- dtl
image: ethereumoptimism/l2geth
deploy:
replicas: 0
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.geth
entrypoint: sh ./geth.sh
env_file:
- ./envs/geth.env
environment:
ETH1_HTTP: http://l1_chain:8545
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
URL: http://deployer:8081/addresses.json
ROLLUP_CLIENT_HTTP: http://dtl:7878
ROLLUP_BACKEND: 'l2'
ROLLUP_VERIFIER_ENABLE: 'true'
ETH1_CTC_DEPLOYMENT_HEIGHT: 8
RETRIES: 60
ports:
- ${L2GETH_HTTP_PORT:-8549}:8545
- ${L2GETH_WS_PORT:-8550}:8546
# integration_tests:
# image: ethereumoptimism/integration-tests
# deploy:
# replicas: 0
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.integration-tests
# entrypoint: ./integration-tests.sh
# environment:
# L1_URL: http://l1_chain:8545
# L2_URL: http://l2geth:8545
# URL: http://deployer:8081/addresses.json
# ENABLE_GAS_REPORT: 1
# NO_NETWORK: 1
gas_oracle:
image: ethereumoptimism/gas-oracle
deploy:
replicas: 0
# build:
# context: ..
# dockerfile: ./ops/docker/Dockerfile.gas-oracle
entrypoint: ./gas-oracle.sh
environment:
GAS_PRICE_ORACLE_ETHEREUM_HTTP_URL: http://l2geth:8545
GAS_PRICE_ORACLE_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
......@@ -32,6 +32,7 @@ COPY packages/contracts/package.json ./packages/contracts/package.json
COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json
COPY packages/batch-submitter/package.json ./packages/batch-submitter/package.json
COPY packages/message-relayer/package.json ./packages/message-relayer/package.json
COPY packages/replica-healthcheck/package.json ./packages/replica-healthcheck/package.json
COPY integration-tests/package.json ./integration-tests/package.json
RUN yarn install --frozen-lockfile
......
/* External Imports */
import { Contract, Signer, utils, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import {
Contract,
Signer,
utils,
providers,
PopulatedTransaction,
} from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Gauge, Histogram, Counter } from 'prom-client'
import * as ynatm from '@eth-optimism/ynatm'
import { RollupInfo, sleep } from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts'
import { getContractFactory } from 'old-contracts'
/* Internal Imports */
import { TxSubmissionHooks } from '..'
export interface BlockRange {
start: number
end: number
}
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
gasRetryIncrement: number
}
interface BatchSubmitterMetrics {
batchSubmitterETHBalance: Gauge<string>
......@@ -49,10 +53,6 @@ export abstract class BatchSubmitter {
readonly finalityConfirmations: number,
readonly addressManagerAddress: string,
readonly minBalanceEther: number,
readonly minGasPriceInGwei: number,
readonly maxGasPriceInGwei: number,
readonly gasRetryIncrement: number,
readonly gasThresholdInGwei: number,
readonly blockOffset: number,
readonly logger: Logger,
readonly defaultMetrics: Metrics
......@@ -190,69 +190,37 @@ export abstract class BatchSubmitter {
return true
}
public static async getReceiptWithResubmission(
txFunc: (gasPrice) => Promise<TransactionReceipt>,
resubmissionConfig: ResubmissionConfig,
logger: Logger
): Promise<TransactionReceipt> {
const {
resubmissionTimeout,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
} = resubmissionConfig
const receipt = await ynatm.send({
sendTransactionFunction: txFunc,
minGasPrice: ynatm.toGwei(minGasPriceInGwei),
maxGasPrice: ynatm.toGwei(maxGasPriceInGwei),
gasPriceScalingFunction: ynatm.LINEAR(gasRetryIncrement),
delay: resubmissionTimeout,
protected _makeHooks(txName: string): TxSubmissionHooks {
return {
beforeSendTransaction: (tx: PopulatedTransaction) => {
this.logger.info(`Submitting ${txName} transaction`, {
gasPrice: tx.gasPrice,
nonce: tx.nonce,
contractAddr: this.chainContract.address,
})
logger.debug('Resubmission tx receipt', { receipt })
return receipt
}
private async _getMinGasPriceInGwei(): Promise<number> {
if (this.minGasPriceInGwei !== 0) {
return this.minGasPriceInGwei
}
let minGasPriceInGwei = parseInt(
utils.formatUnits(await this.signer.getGasPrice(), 'gwei'),
10
)
if (minGasPriceInGwei > this.maxGasPriceInGwei) {
this.logger.warn(
'Minimum gas price is higher than max! Ethereum must be congested...'
)
minGasPriceInGwei = this.maxGasPriceInGwei
},
onTransactionResponse: (txResponse: TransactionResponse) => {
this.logger.info(`Submitted ${txName} transaction`, {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug(`${txName} transaction data`, {
data: txResponse.data,
})
},
}
return minGasPriceInGwei
}
protected async _submitAndLogTx(
txFunc: (gasPrice) => Promise<TransactionReceipt>,
submitTransaction: () => Promise<TransactionReceipt>,
successMessage: string
): Promise<TransactionReceipt> {
this.lastBatchSubmissionTimestamp = Date.now()
this.logger.debug('Waiting for receipt...')
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: this.resubmissionTimeout,
minGasPriceInGwei: await this._getMinGasPriceInGwei(),
maxGasPriceInGwei: this.maxGasPriceInGwei,
gasRetryIncrement: this.gasRetryIncrement,
}
this.logger.debug('Submitting transaction & waiting for receipt...')
let receipt: TransactionReceipt
try {
receipt = await BatchSubmitter.getReceiptWithResubmission(
txFunc,
resubmissionConfig,
this.logger
)
receipt = await submitTransaction()
} catch (err) {
this.metrics.failedSubmissions.inc()
if (err.reason) {
......
......@@ -13,6 +13,7 @@ import { Logger, Metrics } from '@eth-optimism/common-ts'
/* Internal Imports */
import { BlockRange, BatchSubmitter } from '.'
import { TransactionSubmitter } from '../utils'
export class StateBatchSubmitter extends BatchSubmitter {
// TODO: Change this so that we calculate start = scc.totalElements() and end = ctc.totalElements()!
......@@ -23,6 +24,7 @@ export class StateBatchSubmitter extends BatchSubmitter {
protected syncing: boolean
protected ctcContract: Contract
private fraudSubmissionAddress: string
private transactionSubmitter: TransactionSubmitter
constructor(
signer: Signer,
......@@ -36,10 +38,7 @@ export class StateBatchSubmitter extends BatchSubmitter {
finalityConfirmations: number,
addressManagerAddress: string,
minBalanceEther: number,
minGasPriceInGwei: number,
maxGasPriceInGwei: number,
gasRetryIncrement: number,
gasThresholdInGwei: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number,
logger: Logger,
metrics: Metrics,
......@@ -57,15 +56,12 @@ export class StateBatchSubmitter extends BatchSubmitter {
finalityConfirmations,
addressManagerAddress,
minBalanceEther,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
gasThresholdInGwei,
blockOffset,
logger,
metrics
)
this.fraudSubmissionAddress = fraudSubmissionAddress
this.transactionSubmitter = transactionSubmitter
}
/*****************************
......@@ -159,14 +155,14 @@ export class StateBatchSubmitter extends BatchSubmitter {
endBlock: number
): Promise<TransactionReceipt> {
const batch = await this._generateStateCommitmentBatch(startBlock, endBlock)
const tx = this.chainContract.interface.encodeFunctionData(
const calldata = this.chainContract.interface.encodeFunctionData(
'appendStateBatch',
[batch, startBlock]
)
const batchSizeInBytes = remove0x(tx).length / 2
const batchSizeInBytes = remove0x(calldata).length / 2
this.logger.debug('State batch generated', {
batchSizeInBytes,
tx,
calldata,
})
if (!this._shouldSubmitBatch(batchSizeInBytes)) {
......@@ -174,33 +170,23 @@ export class StateBatchSubmitter extends BatchSubmitter {
}
const offsetStartsAtIndex = startBlock - this.blockOffset
this.logger.debug('Submitting batch.', { tx })
this.logger.debug('Submitting batch.', { calldata })
const nonce = await this.signer.getTransactionCount()
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendStateBatch transaction', {
gasPrice,
nonce,
contractAddr: this.chainContract.address,
})
const contractTx = await this.chainContract.appendStateBatch(
// Generate the transaction we will repeatedly submit
const tx = await this.chainContract.populateTransaction.appendStateBatch(
batch,
offsetStartsAtIndex,
{ nonce, gasPrice }
offsetStartsAtIndex
)
this.logger.info('Submitted appendStateBatch transaction', {
txHash: contractTx.hash,
from: contractTx.from,
})
this.logger.debug('appendStateBatch transaction data', {
data: contractTx.data,
})
return this.signer.provider.waitForTransaction(
contractTx.hash,
this.numConfirmations
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendStateBatch')
)
}
return this._submitAndLogTx(contractFunction, 'Submitted state root batch!')
return this._submitAndLogTx(
submitTransaction,
'Submitted state root batch!'
)
}
/*********************
......
......@@ -22,6 +22,7 @@ import {
} from '../transaction-chain-contract'
import { BlockRange, BatchSubmitter } from '.'
import { TransactionSubmitter } from '../utils'
export interface AutoFixBatchOptions {
fixDoublePlayedDeposits: boolean
......@@ -35,6 +36,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
protected syncing: boolean
private disableQueueBatchAppend: boolean
private autoFixBatchOptions: AutoFixBatchOptions
private transactionSubmitter: TransactionSubmitter
private gasThresholdInGwei: number
constructor(
signer: Signer,
......@@ -47,10 +50,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
resubmissionTimeout: number,
addressManagerAddress: string,
minBalanceEther: number,
minGasPriceInGwei: number,
maxGasPriceInGwei: number,
gasRetryIncrement: number,
gasThresholdInGwei: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number,
logger: Logger,
metrics: Metrics,
......@@ -73,16 +74,14 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
0, // Supply dummy value because it is not used.
addressManagerAddress,
minBalanceEther,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
gasThresholdInGwei,
blockOffset,
logger,
metrics
)
this.disableQueueBatchAppend = disableQueueBatchAppend
this.autoFixBatchOptions = autoFixBatchOptions
this.gasThresholdInGwei = gasThresholdInGwei
this.transactionSubmitter = transactionSubmitter
}
/*****************************
......@@ -140,34 +139,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
)
if (!this.disableQueueBatchAppend) {
const nonce = await this.signer.getTransactionCount()
const contractFunction = async (
gasPrice
): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendQueueBatch transaction', {
gasPrice,
nonce,
contractAddr: this.chainContract.address,
})
const tx = await this.chainContract.appendQueueBatch(99999999, {
nonce,
gasPrice,
})
this.logger.info('Submitted appendQueueBatch transaction', {
txHash: tx.hash,
from: tx.from,
})
this.logger.debug('appendQueueBatch transaction data', {
data: tx.data,
})
return this.signer.provider.waitForTransaction(
tx.hash,
this.numConfirmations
)
}
// Empty the queue with a huge `appendQueueBatch(..)` call
return this._submitAndLogTx(contractFunction, 'Cleared queue!')
return this.submitAppendQueueBatch()
}
}
this.logger.info('Syncing mode enabled but queue is empty. Skipping...')
......@@ -250,36 +222,43 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
l1tipHeight,
})
const nonce = await this.signer.getTransactionCount()
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendSequencerBatch transaction', {
gasPrice,
nonce,
contractAddr: this.chainContract.address,
})
const tx = await this.chainContract.appendSequencerBatch(batchParams, {
nonce,
gasPrice,
})
this.logger.info('Submitted appendSequencerBatch transaction', {
txHash: tx.hash,
from: tx.from,
})
this.logger.debug('appendSequencerBatch transaction data', {
data: tx.data,
})
return this.signer.provider.waitForTransaction(
tx.hash,
this.numConfirmations
)
}
return this._submitAndLogTx(contractFunction, 'Submitted batch!')
return this.submitAppendSequencerBatch(batchParams)
}
/*********************
* Private Functions *
********************/
private async submitAppendQueueBatch(): Promise<TransactionReceipt> {
const tx = await this.chainContract.populateTransaction.appendQueueBatch(
ethers.constants.MaxUint256 // Completely empty the queue by appending (up to) an enormous number of queue elements.
)
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendQueueBatch')
)
}
// Empty the queue with a huge `appendQueueBatch(..)` call
return this._submitAndLogTx(submitTransaction, 'Cleared queue!')
}
private async submitAppendSequencerBatch(
batchParams: AppendSequencerBatchParams
): Promise<TransactionReceipt> {
const tx =
await this.chainContract.customPopulateTransaction.appendSequencerBatch(
batchParams
)
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendSequencerBatch')
)
}
return this._submitAndLogTx(submitTransaction, 'Submitted batch!')
}
private async _generateSequencerBatchParams(
startBlock: number,
endBlock: number
......
......@@ -16,6 +16,11 @@ import {
STATE_BATCH_SUBMITTER_LOG_TAG,
TX_BATCH_SUBMITTER_LOG_TAG,
} from '..'
import {
TransactionSubmitter,
YnatmTransactionSubmitter,
ResubmissionConfig,
} from '../utils'
interface RequiredEnvVars {
// The HTTP provider URL for L1.
......@@ -349,6 +354,18 @@ export const run = async () => {
addressManagerAddress: requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
})
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: requiredEnvVars.RESUBMISSION_TIMEOUT * 1_000,
minGasPriceInGwei: MIN_GAS_PRICE_IN_GWEI,
maxGasPriceInGwei: GAS_THRESHOLD_IN_GWEI,
gasRetryIncrement: GAS_RETRY_INCREMENT,
}
const txBatchTxSubmitter: TransactionSubmitter =
new YnatmTransactionSubmitter(
sequencerSigner,
resubmissionConfig,
requiredEnvVars.NUM_CONFIRMATIONS
)
const txBatchSubmitter = new TransactionBatchSubmitter(
sequencerSigner,
l2Provider,
......@@ -360,10 +377,8 @@ export const run = async () => {
requiredEnvVars.RESUBMISSION_TIMEOUT * 1_000,
requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
txBatchTxSubmitter,
BLOCK_OFFSET,
logger.child({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
metrics,
......@@ -371,6 +386,12 @@ export const run = async () => {
autoFixBatchOptions
)
const stateBatchTxSubmitter: TransactionSubmitter =
new YnatmTransactionSubmitter(
proposerSigner,
resubmissionConfig,
requiredEnvVars.NUM_CONFIRMATIONS
)
const stateBatchSubmitter = new StateBatchSubmitter(
proposerSigner,
l2Provider,
......@@ -383,10 +404,7 @@ export const run = async () => {
requiredEnvVars.FINALITY_CONFIRMATIONS,
requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
stateBatchTxSubmitter,
BLOCK_OFFSET,
logger.child({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
metrics,
......
export * from './batch-submitter'
export * from './utils'
export * from './transaction-chain-contract'
/* External Imports */
import { Contract, BigNumber } from 'ethers'
import { Contract, ethers } from 'ethers'
import {
TransactionResponse,
TransactionRequest,
......@@ -9,7 +9,7 @@ import {
AppendSequencerBatchParams,
BatchContext,
encodeAppendSequencerBatch,
encodeHex,
remove0x,
} from '@eth-optimism/core-utils'
export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
......@@ -19,6 +19,27 @@ export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
* where the `appendSequencerBatch(...)` function uses a specialized encoding for improved efficiency.
*/
export class CanonicalTransactionChainContract extends Contract {
public customPopulateTransaction = {
appendSequencerBatch: async (
batch: AppendSequencerBatchParams
): Promise<ethers.PopulatedTransaction> => {
const nonce = await this.signer.getTransactionCount()
const to = this.address
const data = getEncodedCalldata(batch)
const gasLimit = await this.signer.provider.estimateGas({
to,
from: await this.signer.getAddress(),
data,
})
return {
nonce,
to,
data,
gasLimit,
}
},
}
public async appendSequencerBatch(
batch: AppendSequencerBatchParams,
options?: TransactionRequest
......@@ -31,29 +52,24 @@ export class CanonicalTransactionChainContract extends Contract {
* Internal Functions *
*********************/
const APPEND_SEQUENCER_BATCH_METHOD_ID = 'appendSequencerBatch()'
const APPEND_SEQUENCER_BATCH_METHOD_ID = keccak256(
Buffer.from('appendSequencerBatch()')
).slice(2, 10)
const appendSequencerBatch = async (
OVM_CanonicalTransactionChain: Contract,
batch: AppendSequencerBatchParams,
options?: TransactionRequest
): Promise<TransactionResponse> => {
const methodId = keccak256(
Buffer.from(APPEND_SEQUENCER_BATCH_METHOD_ID)
).slice(2, 10)
const calldata = encodeAppendSequencerBatch(batch)
return OVM_CanonicalTransactionChain.signer.sendTransaction({
to: OVM_CanonicalTransactionChain.address,
data: '0x' + methodId + calldata,
data: getEncodedCalldata(batch),
...options,
})
}
const encodeBatchContext = (context: BatchContext): string => {
return (
encodeHex(context.numSequencedTransactions, 6) +
encodeHex(context.numSubsequentQueueTransactions, 6) +
encodeHex(context.timestamp, 10) +
encodeHex(context.blockNumber, 10)
)
const getEncodedCalldata = (batch: AppendSequencerBatchParams): string => {
const methodId = APPEND_SEQUENCER_BATCH_METHOD_ID
const calldata = encodeAppendSequencerBatch(batch)
return '0x' + remove0x(methodId) + remove0x(calldata)
}
export * from './tx-submission'
import { Signer, utils, ethers, PopulatedTransaction } from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import * as ynatm from '@eth-optimism/ynatm'
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
gasRetryIncrement: number
}
export type SubmitTransactionFn = (
tx: PopulatedTransaction
) => Promise<TransactionReceipt>
export interface TxSubmissionHooks {
beforeSendTransaction: (tx: PopulatedTransaction) => void
onTransactionResponse: (txResponse: TransactionResponse) => void
}
const getGasPriceInGwei = async (signer: Signer): Promise<number> => {
return parseInt(
ethers.utils.formatUnits(await signer.getGasPrice(), 'gwei'),
10
)
}
export const submitTransactionWithYNATM = async (
tx: PopulatedTransaction,
signer: Signer,
config: ResubmissionConfig,
numConfirmations: number,
hooks: TxSubmissionHooks
): Promise<TransactionReceipt> => {
const sendTxAndWaitForReceipt = async (
gasPrice
): Promise<TransactionReceipt> => {
const fullTx = {
...tx,
gasPrice,
}
hooks.beforeSendTransaction(fullTx)
const txResponse = await signer.sendTransaction(fullTx)
hooks.onTransactionResponse(txResponse)
return signer.provider.waitForTransaction(txResponse.hash, numConfirmations)
}
const minGasPrice = await getGasPriceInGwei(signer)
const receipt = await ynatm.send({
sendTransactionFunction: sendTxAndWaitForReceipt,
minGasPrice: ynatm.toGwei(minGasPrice),
maxGasPrice: ynatm.toGwei(config.maxGasPriceInGwei),
gasPriceScalingFunction: ynatm.LINEAR(config.gasRetryIncrement),
delay: config.resubmissionTimeout,
})
return receipt
}
export interface TransactionSubmitter {
submitTransaction(
tx: PopulatedTransaction,
hooks?: TxSubmissionHooks
): Promise<TransactionReceipt>
}
export class YnatmTransactionSubmitter implements TransactionSubmitter {
constructor(
readonly signer: Signer,
readonly ynatmConfig: ResubmissionConfig,
readonly numConfirmations: number
) {}
public async submitTransaction(
tx: PopulatedTransaction,
hooks?: TxSubmissionHooks
): Promise<TransactionReceipt> {
if (!hooks) {
hooks = {
beforeSendTransaction: () => undefined,
onTransactionResponse: () => undefined,
}
}
return submitTransactionWithYNATM(
tx,
this.signer,
this.ynatmConfig,
this.numConfirmations,
hooks
)
}
}
......@@ -27,6 +27,8 @@ import {
TX_BATCH_SUBMITTER_LOG_TAG,
STATE_BATCH_SUBMITTER_LOG_TAG,
BatchSubmitter,
YnatmTransactionSubmitter,
ResubmissionConfig,
} from '../../src'
import {
......@@ -200,8 +202,19 @@ describe('BatchSubmitter', () => {
sinon.restore()
})
const createBatchSubmitter = (timeout: number): TransactionBatchSubmitter =>
new TransactionBatchSubmitter(
const createBatchSubmitter = (timeout: number): TransactionBatchSubmitter => {
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: 100000,
minGasPriceInGwei: MIN_GAS_PRICE_IN_GWEI,
maxGasPriceInGwei: GAS_THRESHOLD_IN_GWEI,
gasRetryIncrement: GAS_RETRY_INCREMENT,
}
const txBatchTxSubmitter = new YnatmTransactionSubmitter(
sequencer,
resubmissionConfig,
1
)
return new TransactionBatchSubmitter(
sequencer,
l2Provider as any,
MIN_TX_SIZE,
......@@ -212,15 +225,14 @@ describe('BatchSubmitter', () => {
100000,
AddressManager.address,
1,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
txBatchTxSubmitter,
1,
new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
testMetrics,
false
)
}
describe('TransactionBatchSubmitter', () => {
describe('submitNextBatch', () => {
......@@ -375,7 +387,7 @@ describe('BatchSubmitter', () => {
.callsFake(async () => lowGasPriceWei)
const receipt = await batchSubmitter.submitNextBatch()
expect(sequencer.getGasPrice).to.have.been.calledOnce
expect(sequencer.getGasPrice).to.have.been.calledTwice
expect(receipt).to.not.be.undefined
})
})
......@@ -417,6 +429,17 @@ describe('BatchSubmitter', () => {
// submit a batch of transactions to enable state batch submission
await txBatchSubmitter.submitNextBatch()
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: 100000,
minGasPriceInGwei: MIN_GAS_PRICE_IN_GWEI,
maxGasPriceInGwei: GAS_THRESHOLD_IN_GWEI,
gasRetryIncrement: GAS_RETRY_INCREMENT,
}
const stateBatchTxSubmitter = new YnatmTransactionSubmitter(
sequencer,
resubmissionConfig,
1
)
stateBatchSubmitter = new StateBatchSubmitter(
sequencer,
l2Provider as any,
......@@ -429,10 +452,7 @@ describe('BatchSubmitter', () => {
0, // finalityConfirmations
AddressManager.address,
1,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
stateBatchTxSubmitter,
1,
new Logger({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
testMetrics,
......@@ -473,54 +493,4 @@ describe('Batch Submitter with Ganache', () => {
after(async () => {
await server.close()
})
// Unit test for getReceiptWithResubmission function,
// tests for increasing gas price on resubmission
it('should resubmit a transaction if it is not confirmed', async () => {
const gasPrices = []
const numConfirmations = 2
const sendTxFunc = async (gasPrice) => {
// push the retried gasPrice
gasPrices.push(gasPrice)
const tx = signer.sendTransaction({
to: predeploys.OVM_SequencerEntrypoint,
value: 88,
nonce: 0,
gasPrice,
})
const response = await tx
return signer.provider.waitForTransaction(response.hash, numConfirmations)
}
const resubmissionConfig = {
numConfirmations,
resubmissionTimeout: 1_000, // retry every second
minGasPriceInGwei: 0,
maxGasPriceInGwei: 100,
gasRetryIncrement: 5,
}
BatchSubmitter.getReceiptWithResubmission(
sendTxFunc,
resubmissionConfig,
new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG })
)
// Wait 1.5s for at least 1 retry
await new Promise((r) => setTimeout(r, 1500))
// Iterate through gasPrices to ensure each entry increases from
// the last
const isIncreasing = gasPrices.reduce(
(isInc, gasPrice, i, gP) =>
(isInc && gasPrice > gP[i - 1]) || Number.NEGATIVE_INFINITY,
true
)
expect(gasPrices).to.have.lengthOf.above(1) // retried at least once
expect(isIncreasing).to.be.true
})
})
import { expect } from '../setup'
import { ethers, BigNumber, Signer } from 'ethers'
import { submitTransactionWithYNATM } from '../../src/utils/tx-submission'
import { ResubmissionConfig } from '../../src'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
const nullFunction = () => undefined
const nullHooks = {
beforeSendTransaction: nullFunction,
onTransactionResponse: nullFunction,
}
describe('submitTransactionWithYNATM', async () => {
it('calls sendTransaction, waitForTransaction, and hooks with correct inputs', async () => {
const called = {
sendTransaction: false,
waitForTransaction: false,
beforeSendTransaction: false,
onTransactionResponse: false,
}
const dummyHash = 'dummy hash'
const numConfirmations = 3
const tx = {
data: 'we here though',
} as ethers.PopulatedTransaction
const sendTransaction = async (
_tx: ethers.PopulatedTransaction
): Promise<TransactionResponse> => {
called.sendTransaction = true
expect(_tx.data).to.equal(tx.data)
return {
hash: dummyHash,
} as TransactionResponse
}
const waitForTransaction = async (
hash: string,
_numConfirmations: number
): Promise<TransactionReceipt> => {
called.waitForTransaction = true
expect(hash).to.equal(dummyHash)
expect(_numConfirmations).to.equal(numConfirmations)
return {
to: '',
from: '',
status: 1,
} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction,
},
} as Signer
const hooks = {
beforeSendTransaction: (submittingTx: ethers.PopulatedTransaction) => {
called.beforeSendTransaction = true
expect(submittingTx.data).to.equal(tx.data)
},
onTransactionResponse: (txResponse: TransactionResponse) => {
called.onTransactionResponse = true
expect(txResponse.hash).to.equal(dummyHash)
},
}
const config: ResubmissionConfig = {
resubmissionTimeout: 1000,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 0,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(
tx,
signer,
config,
numConfirmations,
hooks
)
expect(called.sendTransaction).to.be.true
expect(called.waitForTransaction).to.be.true
expect(called.beforeSendTransaction).to.be.true
expect(called.onTransactionResponse).to.be.true
})
it('repeatedly increases the gas limit of the transaction when wait takes too long', async () => {
// Make transactions take longer to be included
// than our resubmission timeout
const resubmissionTimeout = 100
const txReceiptDelay = resubmissionTimeout * 3
const numConfirmations = 3
let lastGasPrice = BigNumber.from(0)
// Create a transaction which has a gas price that we will watch increment
const tx = {
gasPrice: lastGasPrice.add(1),
data: 'hello world!',
} as ethers.PopulatedTransaction
const sendTransaction = async (
_tx: ethers.PopulatedTransaction
): Promise<TransactionResponse> => {
// Ensure the gas price is always increasing
expect(_tx.gasPrice > lastGasPrice).to.be.true
lastGasPrice = _tx.gasPrice
return {
hash: 'dummy hash',
} as TransactionResponse
}
const waitForTransaction = async (
hash: string,
_numConfirmations: number
): Promise<TransactionReceipt> => {
await new Promise((r) => setTimeout(r, txReceiptDelay))
return {} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction,
},
} as Signer
const config: ResubmissionConfig = {
resubmissionTimeout,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 1000,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(tx, signer, config, 0, nullHooks)
})
})
......@@ -160,5 +160,23 @@ If you are using a network which Etherscan supports you can verify your contract
npx hardhat etherscan-verify --api-key ... --network ...
```
### Other hardhat tasks
To whitelist deployers on Mainnet you must have the whitelist Owner wallet connected, then run:
```bash
npx hardhat whitelist \
--use-ledger true \
--contracts-rpc-url https://mainnet.optimism.io \
--address ... \ # address to whitelist
```
To withdraw ETH fees to L1 on Mainnet, run:
```bash
npx hardhat withdraw-fees \
--use-ledger \ # The ledger to withdraw fees with. Ensure this wallet has ETH on L2 to pay the tx fee.
--contracts-rpc-url https://mainnet.optimism.io \
```
## Security
Please refer to our [Security Policy](https://github.com/ethereum-optimism/.github/security/policy) for information about how to disclose security issues with this code.
......@@ -16,6 +16,8 @@ import '@eth-optimism/hardhat-ovm'
import './tasks/deploy'
import './tasks/l2-gasprice'
import './tasks/set-owner'
import './tasks/whitelist'
import './tasks/withdraw-fees'
import 'hardhat-gas-reporter'
// Load environment variables from .env
......
......@@ -63,10 +63,9 @@ task('set-owner')
console.log(`Owner is currently ${owner.toString()}`)
console.log(`Setting owner to ${args.owner}`)
const tx = await Ownable.connect(signer).transferOwnership(
args.owner,
{ gasPrice: args.transactionGasPrice }
)
const tx = await Ownable.connect(signer).transferOwnership(args.owner, {
gasPrice: args.transactionGasPrice,
})
const receipt = await tx.wait()
console.log(`Success - ${receipt.transactionHash}`)
......
'use strict'
import { ethers } from 'ethers'
import { task } from 'hardhat/config'
import * as types from 'hardhat/internal/core/params/argumentTypes'
import { LedgerSigner } from '@ethersproject/hardware-wallets'
import { getContractFactory } from '../src/contract-defs'
import { predeploys } from '../src/predeploys'
// Add accounts the the OVM_DeployerWhitelist
// npx hardhat whitelist --address 0x..
task('whitelist')
.addParam('address', 'Address to whitelist', undefined, types.string)
.addOptionalParam('transactionGasPrice', 'tx.gasPrice', undefined, types.int)
.addOptionalParam(
'useLedger',
'use a ledger for signing',
false,
types.boolean
)
.addOptionalParam(
'ledgerPath',
'ledger key derivation path',
ethers.utils.defaultPath,
types.string
)
.addOptionalParam(
'contractsRpcUrl',
'Sequencer HTTP Endpoint',
process.env.CONTRACTS_RPC_URL,
types.string
)
.addOptionalParam(
'contractsDeployerKey',
'Private Key',
process.env.CONTRACTS_DEPLOYER_KEY,
types.string
)
.addOptionalParam(
'contractAddress',
'Address of Ownable contract',
predeploys.OVM_DeployerWhitelist,
types.string
)
.setAction(async (args, hre: any) => {
const provider = new ethers.providers.JsonRpcProvider(args.contractsRpcUrl)
let signer: ethers.Signer
if (!args.useLedger) {
if (!args.contractsDeployerKey) {
throw new Error('Must pass --contracts-deployer-key')
}
signer = new ethers.Wallet(args.contractsDeployerKey).connect(provider)
} else {
signer = new LedgerSigner(provider, 'default', args.ledgerPath)
}
const deployerWhitelist = getContractFactory('OVM_DeployerWhitelist')
.connect(signer)
.attach(args.contractAddress)
const addr = await signer.getAddress()
console.log(`Using signer: ${addr}`)
let owner = await deployerWhitelist.owner()
console.log(`OVM_DeployerWhitelist owner: ${owner}`)
if (owner === '0x0000000000000000000000000000000000000000') {
console.log(`Initializing whitelist`)
const response = await deployerWhitelist.initialize(addr, false, {
gasPrice: args.transactionGasPrice,
})
const receipt = await response.wait()
console.log(`Initialized whitelist: ${receipt.transactionHash}`)
owner = await deployerWhitelist.owner()
}
if (addr !== owner) {
throw new Error(`Incorrect key. Owner ${owner}, Signer ${addr}`)
}
const res = await deployerWhitelist.setWhitelistedDeployer(
args.address,
true,
{ gasPrice: args.transactionGasPrice }
)
await res.wait()
console.log(`Whitelisted ${args.address}`)
})
'use strict'
import { ethers } from 'ethers'
import { task } from 'hardhat/config'
import * as types from 'hardhat/internal/core/params/argumentTypes'
import { LedgerSigner } from '@ethersproject/hardware-wallets'
import { getContractFactory } from '../src/contract-defs'
import { predeploys } from '../src/predeploys'
// Withdraw fees from the FeeVault to L1
// npx hardhat withdraw-fees --dry-run
task('withdraw-fees')
.addOptionalParam('dryRun', 'simulate withdrawing fees', false, types.boolean)
.addOptionalParam(
'useLedger',
'use a ledger for signing',
false,
types.boolean
)
.addOptionalParam(
'ledgerPath',
'ledger key derivation path',
ethers.utils.defaultPath,
types.string
)
.addOptionalParam(
'contractsRpcUrl',
'Sequencer HTTP Endpoint',
process.env.CONTRACTS_RPC_URL,
types.string
)
.addOptionalParam(
'privateKey',
'Private Key',
process.env.CONTRACTS_DEPLOYER_KEY,
types.string
)
.setAction(async (args, hre: any) => {
const provider = new ethers.providers.JsonRpcProvider(args.contractsRpcUrl)
let signer: ethers.Signer
if (!args.useLedger) {
if (!args.contractsDeployerKey) {
throw new Error('Must pass --contracts-deployer-key')
}
signer = new ethers.Wallet(args.contractsDeployerKey).connect(provider)
} else {
signer = new LedgerSigner(provider, 'default', args.ledgerPath)
}
if (args.dryRun) {
console.log('Performing dry run of fee withdrawal...')
}
const l2FeeVault = getContractFactory('OVM_SequencerFeeVault')
.connect(signer)
.attach(predeploys.OVM_SequencerFeeVault)
const signerAddress = await signer.getAddress()
const signerBalance = await provider.getBalance(signerAddress)
const signerBalanceInETH = ethers.utils.formatEther(signerBalance)
console.log(
`Using L2 signer ${signerAddress} with a balance of ${signerBalanceInETH} ETH`
)
const l1FeeWallet = await l2FeeVault.l1FeeWallet()
const amount = await provider.getBalance(l2FeeVault.address)
const amountInETH = ethers.utils.formatEther(amount)
console.log(
`${
args.dryRun ? '[DRY RUN] ' : ''
}Withdrawing ${amountInETH} ETH to the L1 address: ${l1FeeWallet}`
)
if (args.dryRun) {
await l2FeeVault.estimateGas.withdraw()
return
} else {
const withdrawTx = await l2FeeVault.withdraw()
console.log(
`Withdrawal complete: https://optimistic.etherscan.io/tx/${withdrawTx.hash}`
)
console.log(
`Complete withdrawal in 1 week here: https://optimistic.etherscan.io/address/${predeploys.OVM_SequencerFeeVault}#withdrawaltxs`
)
}
})
REPLICA_HEALTHCHECK__ETH_NETWORK=mainnet
REPLICA_HEALTHCHECK__ETH_NETWORK_RPC_PROVIDER=https://mainnet.optimism.io
REPLICA_HEALTHCHECK__ETH_REPLICA_RPC_PROVIDER=http://localhost:9991
REPLICA_HEALTHCHECK__L2GETH_IMAGE_TAG=0.4.7
module.exports = {
extends: '../../.eslintrc.js',
}
node_modules/
build/
\ No newline at end of file
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
(The MIT License)
Copyright 2020-2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @eth-optimism/replica-healthcheck
## What is this?
`replica-healthcheck` is an express server to be run alongside a replica instance, to ensure that the replica is healthy. Currently, it exposes metrics on syncing stats and exits when the replica has a mismatched state root against the sequencer.
## Getting started
### Building and usage
After cloning and switching to the repository, install dependencies:
```bash
$ yarn
```
Use the following commands to build, use, test, and lint:
```bash
$ yarn build
$ yarn start
$ yarn test
$ yarn lint
```
### Configuration
We're using `dotenv` for our configuration.
To configure the project, clone this repository and copy the `env.example` file to `.env`.
Here's a list of environment variables:
| Variable | Purpose | Default |
| ----------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| REPLICA_HEALTHCHECK\_\_ETH_NETWORK | Ethereum Layer1 and Layer2 network (mainnet,kovan) | mainnet (change to `kovan` for the test network) |
| REPLICA_HEALTHCHECK\_\_ETH_NETWORK_RPC_PROVIDER | Layer2 source of truth endpoint, used for the sync check | https://mainnet.optimism.io (change to `https://kovan.optimism.io` for the test network) |
| REPLICA_HEALTHCHECK\_\_ETH_REPLICA_RPC_PROVIDER | Layer2 local replica endpoint, used for the sync check | http://localhost:9991 |
| REPLICA_HEALTHCHECK\_\_L2GETH_IMAGE_TAG | L2geth version | 0.4. |
{
"name": "@eth-optimism/replica-healthcheck",
"version": "0.1.0",
"private": true,
"main": "dist/index",
"files": [
"dist/index"
],
"types": "dist/index",
"author": "Optimism PBC",
"license": "MIT",
"scripts": {
"clean": "rimraf ./dist ./tsconfig.build.tsbuildinfo",
"lint": "yarn run lint:fix && yarn run lint:check",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint .",
"build": "tsc -p tsconfig.build.json",
"pre-commit": "lint-staged",
"test": "ts-mocha test/*.spec.ts",
"start": "ts-node ./src/exec/run-healthcheck-server.ts"
},
"devDependencies": {
"@types/express": "^4.17.12",
"@types/node": "^15.12.2",
"dotenv": "^10.0.0",
"supertest": "^6.1.4",
"ts-mocha": "^8.0.0",
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
},
"dependencies": {
"@eth-optimism/common-ts": "0.1.5",
"@eth-optimism/core-utils": "^0.5.1",
"ethers": "^5.3.0",
"express": "^4.17.1",
"express-prom-bundle": "^6.3.6",
"prom-client": "^13.1.0"
}
}
import * as dotenv from 'dotenv'
import { HealthcheckServer, readConfig } from '..'
;(async () => {
dotenv.config()
const healthcheckServer = new HealthcheckServer(readConfig())
healthcheckServer.init()
await healthcheckServer.runSyncCheck()
})().catch((err) => {
console.log(err)
process.exit(1)
})
import express from 'express'
import { Server } from 'net'
import promBundle from 'express-prom-bundle'
import { Gauge } from 'prom-client'
import { providers } from 'ethers'
import { Metrics, Logger } from '@eth-optimism/common-ts'
import { injectL2Context, sleep } from '@eth-optimism/core-utils'
import { binarySearchForMismatch } from './helpers'
export interface HealthcheckServerOptions {
network: string
gethRelease: string
sequencerRpcProvider: string
replicaRpcProvider: string
logger: Logger
}
export interface ReplicaMetrics {
lastMatchingStateRootHeight: Gauge<string>
replicaHeight: Gauge<string>
sequencerHeight: Gauge<string>
}
export class HealthcheckServer {
protected options: HealthcheckServerOptions
protected app: express.Express
protected logger: Logger
protected metrics: ReplicaMetrics
server: Server
constructor(options: HealthcheckServerOptions) {
this.options = options
this.app = express()
this.logger = options.logger
}
init = () => {
this.metrics = this.initMetrics()
this.server = this.initServer()
}
initMetrics = (): ReplicaMetrics => {
const metrics = new Metrics({
labels: {
network: this.options.network,
gethRelease: this.options.gethRelease,
},
})
const metricsMiddleware = promBundle({
includeMethod: true,
includePath: true,
})
this.app.use(metricsMiddleware)
return {
lastMatchingStateRootHeight: new metrics.client.Gauge({
name: 'replica_health_last_matching_state_root_height',
help: 'Height of last matching state root of replica',
registers: [metrics.registry],
}),
replicaHeight: new metrics.client.Gauge({
name: 'replica_health_height',
help: 'Block number of the latest block from the replica',
registers: [metrics.registry],
}),
sequencerHeight: new metrics.client.Gauge({
name: 'replica_health_sequencer_height',
help: 'Block number of the latest block from the sequencer',
registers: [metrics.registry],
}),
}
}
initServer = (): Server => {
this.app.get('/', (req, res) => {
res.send(`
<head><title>Replica healthcheck</title></head>
<body>
<h1>Replica healthcheck</h1>
<p><a href="/metrics">Metrics</a></p>
</body>
</html>
`)
})
const server = this.app.listen(3000, () => {
this.logger.info('Listening on port 3000')
})
return server
}
runSyncCheck = async () => {
throw new Error('trial')
const sequencerProvider = injectL2Context(
new providers.JsonRpcProvider(this.options.sequencerRpcProvider)
)
const replicaProvider = injectL2Context(
new providers.JsonRpcBatchProvider(this.options.replicaRpcProvider)
)
// Continuously loop while replica runs
while (true) {
let replicaLatest = (await replicaProvider.getBlock('latest')) as any
const sequencerCorresponding = (await sequencerProvider.getBlock(
replicaLatest.number
)) as any
if (replicaLatest.stateRoot !== sequencerCorresponding.stateRoot) {
this.logger.error(
'Latest replica state root is mismatched from sequencer'
)
const firstMismatch = await binarySearchForMismatch(
sequencerProvider,
replicaProvider,
replicaLatest.number,
this.logger
)
this.logger.error('First state root mismatch found', {
blockNumber: firstMismatch,
})
this.metrics.lastMatchingStateRootHeight.set(firstMismatch)
throw new Error('Replica state root mismatched')
}
this.logger.info('State roots matching', {
blockNumber: replicaLatest.number,
})
this.metrics.lastMatchingStateRootHeight.set(replicaLatest.number)
replicaLatest = await replicaProvider.getBlock('latest')
const sequencerLatest = await sequencerProvider.getBlock('latest')
this.logger.info('Syncing from sequencer', {
sequencerHeight: sequencerLatest.number,
replicaHeight: replicaLatest.number,
heightDifference: sequencerLatest.number - replicaLatest.number,
})
this.metrics.replicaHeight.set(replicaLatest.number)
this.metrics.sequencerHeight.set(sequencerLatest.number)
// Fetch next block and sleep if not new
while (replicaLatest.number === sequencerCorresponding.number) {
this.logger.info(
'Replica caught up with sequencer, waiting for next block'
)
await sleep(1_000)
replicaLatest = await replicaProvider.getBlock('latest')
}
}
}
}
import { providers } from 'ethers'
import { Logger } from '@eth-optimism/common-ts'
import { HealthcheckServerOptions } from './healthcheck-server'
export const readEnvOrQuitProcess = (envName: string | undefined): string => {
if (!process.env[envName]) {
console.error(`Missing environment variable: ${envName}`)
process.exit(1)
}
return process.env[envName]
}
export const readConfig = (): HealthcheckServerOptions => {
const network = readEnvOrQuitProcess('REPLICA_HEALTHCHECK__ETH_NETWORK')
const gethRelease = readEnvOrQuitProcess(
'REPLICA_HEALTHCHECK__L2GETH_IMAGE_TAG'
)
const sequencerRpcProvider = readEnvOrQuitProcess(
'REPLICA_HEALTHCHECK__ETH_NETWORK_RPC_PROVIDER'
)
const replicaRpcProvider = readEnvOrQuitProcess(
'REPLICA_HEALTHCHECK__ETH_REPLICA_RPC_PROVIDER'
)
if (!['mainnet', 'kovan', 'goerli'].includes(network)) {
console.error(
'Invalid ETH_NETWORK specified. Must be one of mainnet, kovan, or goerli'
)
process.exit(1)
}
const logger = new Logger({ name: 'replica-healthcheck' })
return {
network,
gethRelease,
sequencerRpcProvider,
replicaRpcProvider,
logger,
}
}
export const binarySearchForMismatch = async (
sequencerProvider: providers.JsonRpcProvider,
replicaProvider: providers.JsonRpcProvider,
latest: number,
logger: Logger
): Promise<number> => {
logger.info(
'Executing a binary search to determine the first mismatched block...'
)
let start = 0
let end = latest
while (start !== end) {
const middle = Math.floor((start + end) / 2)
logger.info('Checking block', { blockNumber: middle })
const [replicaBlock, sequencerBlock] = await Promise.all([
replicaProvider.getBlock(middle) as any,
sequencerProvider.getBlock(middle) as any,
])
if (replicaBlock.stateRoot === sequencerBlock.stateRoot) {
logger.info('State roots still matching', { blockNumber: middle })
start = middle
} else {
logger.error('Found mismatched state roots', {
blockNumber: middle,
sequencerBlock,
replicaBlock,
})
end = middle
}
}
return end
}
export * from './healthcheck-server'
export * from './helpers'
import request from 'supertest'
// Setup
import chai = require('chai')
const expect = chai.expect
import { Logger } from '@eth-optimism/common-ts'
import { HealthcheckServer } from '../src/healthcheck-server'
describe('HealthcheckServer', () => {
it('shoud serve correct metrics', async () => {
const logger = new Logger({ name: 'test_logger' })
const healthcheckServer = new HealthcheckServer({
network: 'kovan',
gethRelease: '0.4.20',
sequencerRpcProvider: 'http://sequencer.io',
replicaRpcProvider: 'http://replica.io',
logger,
})
try {
await healthcheckServer.init()
// Verify that the registered metrics are served at `/`
const response = await request(healthcheckServer.server)
.get('/metrics')
.send()
expect(response.status).eq(200)
expect(response.text).match(/replica_health_height gauge/)
} finally {
healthcheckServer.server.close()
}
})
})
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
{
"extends": "../../tsconfig.json"
}
......@@ -749,6 +749,15 @@
"@ethersproject/logger" "^5.4.0"
bn.js "^4.11.9"
"@ethersproject/bignumber@5.4.1":
version "5.4.1"
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.4.1.tgz#64399d3b9ae80aa83d483e550ba57ea062c1042d"
integrity sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==
dependencies:
"@ethersproject/bytes" "^5.4.0"
"@ethersproject/logger" "^5.4.0"
bn.js "^4.11.9"
"@ethersproject/bytes@5.4.0", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.0", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e"
......@@ -862,6 +871,13 @@
dependencies:
"@ethersproject/logger" "^5.4.0"
"@ethersproject/networks@5.4.1":
version "5.4.1"
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.4.1.tgz#2ce83b8e42aa85216e5d277a7952d97b6ce8d852"
integrity sha512-8SvowCKz9Uf4xC5DTKI8+il8lWqOr78kmiqAVLYT9lzB8aSmJHQMD1GSuJI0CW4hMAnzocpGpZLgiMdzsNSPig==
dependencies:
"@ethersproject/logger" "^5.4.0"
"@ethersproject/pbkdf2@5.4.0", "@ethersproject/pbkdf2@^5.0.0", "@ethersproject/pbkdf2@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.4.0.tgz#ed88782a67fda1594c22d60d0ca911a9d669641c"
......@@ -902,6 +918,31 @@
bech32 "1.1.4"
ws "7.4.6"
"@ethersproject/providers@5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.2.tgz#73df9767869a31bd88d9e27e78cff96364b8fbed"
integrity sha512-Qr8Am8hlj2gL9HwNymhFlYd52MQVVEBLoDwPxhv4ASeyNpaoRiUAQnNEuE6SnEQtiwYkpLrQtSALNLUSeyuvjA==
dependencies:
"@ethersproject/abstract-provider" "^5.4.0"
"@ethersproject/abstract-signer" "^5.4.0"
"@ethersproject/address" "^5.4.0"
"@ethersproject/basex" "^5.4.0"
"@ethersproject/bignumber" "^5.4.0"
"@ethersproject/bytes" "^5.4.0"
"@ethersproject/constants" "^5.4.0"
"@ethersproject/hash" "^5.4.0"
"@ethersproject/logger" "^5.4.0"
"@ethersproject/networks" "^5.4.0"
"@ethersproject/properties" "^5.4.0"
"@ethersproject/random" "^5.4.0"
"@ethersproject/rlp" "^5.4.0"
"@ethersproject/sha2" "^5.4.0"
"@ethersproject/strings" "^5.4.0"
"@ethersproject/transactions" "^5.4.0"
"@ethersproject/web" "^5.4.0"
bech32 "1.1.4"
ws "7.4.6"
"@ethersproject/random@5.4.0", "@ethersproject/random@^5.0.0", "@ethersproject/random@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.4.0.tgz#9cdde60e160d024be39cc16f8de3b9ce39191e16"
......@@ -2376,6 +2417,26 @@
"@truffle/interface-adapter" "^0.5.1"
web3 "1.3.6"
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
"@tsconfig/node12@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
"@tsconfig/node14@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.1":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
"@typechain/ethers-v5@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-1.0.0.tgz#9156c9a2b078f9bb00a339631221e42c26b218df"
......@@ -2494,6 +2555,16 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/express@^4.17.12":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.18"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/form-data@0.0.33":
version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-0.0.33.tgz#c9ac85b2a5fd18435b8c85d9ecb50e6d6c893ff8"
......@@ -2604,6 +2675,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.15.tgz#10ee6a6a3f971966fddfa3f6e89ef7a73ec622df"
integrity sha512-F6S4Chv4JicJmyrwlDkxUdGNSplsQdGwp1A0AJloEVDirWdZOAiRHhovDlsFkKUrquUXhz1imJhXHsf59auyAg==
"@types/node@^15.12.2":
version "15.14.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.4.tgz#aaf18436ef67f24676d92b8bbe0f5f41b08db3e8"
integrity sha512-yblJrsfCxdxYDUa2fM5sP93ZLk5xL3/+3MJei+YtsNbIdY75ePy2AiCfpq+onepzax+8/Yv+OD/fLNleWpCzVg==
"@types/node@^8.0.0":
version "8.10.66"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3"
......@@ -6784,6 +6860,42 @@ ethers@^5.0.0, ethers@^5.0.1, ethers@^5.0.2, ethers@^5.0.26, ethers@^5.0.31, eth
"@ethersproject/web" "5.4.0"
"@ethersproject/wordlists" "5.4.0"
ethers@^5.3.0:
version "5.4.2"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.2.tgz#91368e4d9c39f1111157de1c2aa1d8c1616c0f7b"
integrity sha512-JcFcNWjULzhm4tMp5cZKnU45zqN/c7rqabIITiUiQzZuP7LcYSD4WAbADo4Ja6G2orU4d/PbhAWGHGtAKYrB4Q==
dependencies:
"@ethersproject/abi" "5.4.0"
"@ethersproject/abstract-provider" "5.4.0"
"@ethersproject/abstract-signer" "5.4.0"
"@ethersproject/address" "5.4.0"
"@ethersproject/base64" "5.4.0"
"@ethersproject/basex" "5.4.0"
"@ethersproject/bignumber" "5.4.1"
"@ethersproject/bytes" "5.4.0"
"@ethersproject/constants" "5.4.0"
"@ethersproject/contracts" "5.4.0"
"@ethersproject/hash" "5.4.0"
"@ethersproject/hdnode" "5.4.0"
"@ethersproject/json-wallets" "5.4.0"
"@ethersproject/keccak256" "5.4.0"
"@ethersproject/logger" "5.4.0"
"@ethersproject/networks" "5.4.1"
"@ethersproject/pbkdf2" "5.4.0"
"@ethersproject/properties" "5.4.0"
"@ethersproject/providers" "5.4.2"
"@ethersproject/random" "5.4.0"
"@ethersproject/rlp" "5.4.0"
"@ethersproject/sha2" "5.4.0"
"@ethersproject/signing-key" "5.4.0"
"@ethersproject/solidity" "5.4.0"
"@ethersproject/strings" "5.4.0"
"@ethersproject/transactions" "5.4.0"
"@ethersproject/units" "5.4.0"
"@ethersproject/wallet" "5.4.0"
"@ethersproject/web" "5.4.0"
"@ethersproject/wordlists" "5.4.0"
ethjs-unit@0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699"
......@@ -13670,6 +13782,14 @@ supertest@^6.1.3:
methods "^1.1.2"
superagent "^6.1.0"
supertest@^6.1.4:
version "6.1.4"
resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.4.tgz#ea8953343e0ca316e80e975b39340934f754eb06"
integrity sha512-giC9Zm+Bf1CZP09ciPdUyl+XlMAu6rbch79KYiYKOGcbK2R1wH8h+APul1i/3wN6RF1XfWOIF+8X1ga+7SBrug==
dependencies:
methods "^1.1.2"
superagent "^6.1.0"
supports-color@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a"
......@@ -13816,9 +13936,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"
tar@^4.0.2, tar@^4.4.12:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
version "4.4.15"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8"
integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA==
dependencies:
chownr "^1.1.1"
fs-minipass "^1.2.5"
......@@ -14127,6 +14247,22 @@ ts-node@7.0.1:
source-map-support "^0.5.6"
yn "^2.0.0"
ts-node@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.1.0.tgz#e656d8ad3b61106938a867f69c39a8ba6efc966e"
integrity sha512-6szn3+J9WyG2hE+5W8e0ruZrzyk1uFLYye6IGMBadnOzDh8aP7t8CbFpsfCiEx2+wMixAhjFt7lOZC4+l+WbEA==
dependencies:
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
ts-node@^8.0.2:
version "8.10.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
......@@ -14331,7 +14467,7 @@ typescript@^4.2.3, typescript@^4.3.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
typescript@^4.3.5:
typescript@^4.3.2, typescript@^4.3.5:
version "4.3.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment