Commit 957455d4 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2741 from ethereum-optimism/develop

Develop -> Master
parents 85c16583 84a8934c
---
'@eth-optimism/common-ts': minor
---
Minor upgrade to BaseServiceV2 to expose a full customizable server, instead of just metrics.
---
'@eth-optimism/fault-detector': patch
---
Smarter starting height for fault-detector
---
'@eth-optimism/common-ts': minor
'@eth-optimism/drippie-mon': minor
'@eth-optimism/fault-detector': minor
'@eth-optimism/message-relayer': minor
'@eth-optimism/replica-healthcheck': minor
---
BaseServiceV2 exposes service name and version as standard synthetic metric
---
'@eth-optimism/teleportr': patch
---
Better availability endpoint + retries
---
'@eth-optimism/fault-detector': patch
---
Fix order in which a metric was bumped then emitted to fix off by one issue
......@@ -498,9 +498,39 @@ jobs:
name: run itests
command: make test-integration
semgrep-scan:
parameters:
diff_branch:
type: string
default: develop
environment:
# Scan changed files in PRs, block on new issues only (existing issues ignored)
SEMGREP_BASELINE_REF: << parameters.diff_branch >>
SEMGREP_REPO_URL: << pipeline.project.git_url >>
SEMGREP_BRANCH: << pipeline.git.branch >>
# Change job timeout (default is 1800 seconds; set to 0 to disable)
SEMGREP_TIMEOUT: 3000
docker:
- image: returntocorp/semgrep
steps:
- checkout
- run:
name: "Set environment variables" # for PR comments and in-app hyperlinks to findings
command: |
echo 'export SEMGREP_COMMIT=$CIRCLE_SHA1' >> $BASH_ENV
echo 'export SEMGREP_PR_ID=${CIRCLE_PULL_REQUEST##*/}' >> $BASH_ENV
echo 'export SEMGREP_JOB_URL=$CIRCLE_BUILD_URL' >> $BASH_ENV
echo 'export SEMGREP_REPO_NAME=$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME' >> $BASH_ENV
- run:
name: "Semgrep scan"
command: semgrep ci
workflows:
main:
jobs:
- semgrep-scan
- yarn-monorepo
- bedrock-go-tests
- bedrock-markdown
......
# Common large paths
node_modules/
build/
dist/
vendor/
.env/
.venv/
.tox/
*.min.js
# Common test paths
test/
tests/
# Semgrep rules folder
.semgrep
# Semgrep-action log folder
.semgrep_logs/
l2geth/
packages/*/node_modules
packages/*/test
\ No newline at end of file
......@@ -9,7 +9,7 @@ replace github.com/ethereum-optimism/optimism/l2geth v0.0.0 => ../l2geth
require (
github.com/ethereum-optimism/optimism/bss-core v0.0.0
github.com/ethereum-optimism/optimism/l2geth v0.0.0
github.com/ethereum/go-ethereum v1.10.16
github.com/ethereum/go-ethereum v1.10.17
github.com/getsentry/sentry-go v0.12.0
github.com/prometheus/client_golang v1.11.0
github.com/stretchr/testify v1.7.0
......@@ -21,6 +21,7 @@ require (
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd v0.22.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
......@@ -29,6 +30,7 @@ require (
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/decred/dcrd/hdkeychain/v3 v3.0.0 // indirect
github.com/elastic/gosigar v0.12.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
......
......@@ -38,6 +38,9 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo=
github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
......@@ -120,6 +123,9 @@ github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13P
github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA=
github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c=
github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y=
github.com/btcsuite/btcd/btcec/v2 v2.1.2 h1:YoYoC9J0jwfukodSBMzZYUVQ8PTiYg4BnOWiJVzTmLs=
github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
......@@ -190,6 +196,8 @@ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHu
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrutil/v3 v3.0.0 h1:n6uQaTQynIhCY89XsoDk2WQqcUcnbD+zUM9rnZcIOZo=
github.com/decred/dcrd/dcrutil/v3 v3.0.0/go.mod h1:iVsjcqVzLmYFGCZLet2H7Nq+7imV9tYcuY+0lC2mNsY=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0 h1:hOPb4c8+K6bE3a/qFtzt2Z2yzK4SpmXmxvCTFp8vMxI=
......@@ -206,6 +214,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
......@@ -230,8 +240,9 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/ethereum/go-ethereum v1.10.4/go.mod h1:nEE0TP5MtxGzOMd7egIrbPJMQBnhVU3ELNxhBglIzhg=
github.com/ethereum/go-ethereum v1.10.16 h1:3oPrumn0bCW/idjcxMn5YYVCdK7VzJYIvwGZUGLEaoc=
github.com/ethereum/go-ethereum v1.10.16/go.mod h1:Anj6cxczl+AHy63o4X9O8yWNHuN5wMpfb8MAnHkWn7Y=
github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8=
github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
......@@ -298,7 +309,10 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
......@@ -366,6 +380,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
......@@ -418,6 +433,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/huin/goupnp v1.0.1-0.20210310174557-0ca763054c88/go.mod h1:nNs7wvRfN1eKaMknBydLNQU6146XQim8t4h+q90biWo=
github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM=
github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ=
github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
......@@ -559,6 +575,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
......@@ -885,6 +902,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
......@@ -893,6 +911,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
......
......@@ -705,6 +705,7 @@ func TestBoundedHeavyForkedSync64Fast(t *testing.T) { testBoundedHeavyForkedSyn
func TestBoundedHeavyForkedSync64Light(t *testing.T) { testBoundedHeavyForkedSync(t, 64, LightSync) }
func testBoundedHeavyForkedSync(t *testing.T, protocol int, mode SyncMode) {
t.Skip("Flaky test")
t.Parallel()
tester := newTester()
......
......@@ -34,21 +34,26 @@
"@eth-optimism/core-utils": "0.8.6",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.7",
"body-parser": "^1.20.0",
"commander": "^9.0.0",
"dotenv": "^16.0.0",
"envalid": "^7.2.2",
"ethers": "^5.6.8",
"express": "^4.17.1",
"express-prom-bundle": "^6.4.1",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"pino": "^6.11.3",
"pino-multi-stream": "^5.3.0",
"pino-sentry": "^0.7.0",
"prom-client": "^13.1.0"
"prom-client": "^13.1.0",
"qs": "^6.10.5"
},
"devDependencies": {
"@ethersproject/abstract-provider": "^5.6.1",
"@ethersproject/abstract-signer": "^5.6.2",
"@types/express": "^4.17.12",
"@types/express": "^4.17.13",
"@types/morgan": "^1.9.3",
"@types/pino": "^6.3.6",
"@types/pino-multi-stream": "^5.1.1",
"chai": "^4.3.4",
......
......@@ -6,11 +6,14 @@ import { Command, Option } from 'commander'
import { ValidatorSpec, Spec, cleanEnv } from 'envalid'
import { sleep } from '@eth-optimism/core-utils'
import snakeCase from 'lodash/snakeCase'
import express from 'express'
import express, { Router } from 'express'
import prometheus, { Registry } from 'prom-client'
import promBundle from 'express-prom-bundle'
import bodyParser from 'body-parser'
import morgan from 'morgan'
import { Logger } from '../common/logger'
import { Metric } from './metrics'
import { Metric, Gauge, Counter } from './metrics'
import { validators } from './validators'
export type Options = {
......@@ -19,8 +22,8 @@ export type Options = {
export type StandardOptions = {
loopIntervalMs?: number
metricsServerPort?: number
metricsServerHostname?: string
port?: number
hostname?: string
}
export type OptionsSpec<TOptions extends Options> = {
......@@ -28,6 +31,7 @@ export type OptionsSpec<TOptions extends Options> = {
validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]>
desc: string
default?: TOptions[P]
secret?: boolean
}
}
......@@ -35,6 +39,11 @@ export type MetricsV2 = {
[key: string]: Metric
}
export type StandardMetrics = {
metadata: Gauge
unhandledErrors: Counter
}
export type MetricsSpec<TMetrics extends MetricsV2> = {
[P in keyof Required<TMetrics>]: {
type: new (configuration: any) => TMetrics[P]
......@@ -43,6 +52,8 @@ export type MetricsSpec<TMetrics extends MetricsV2> = {
}
}
export type ExpressRouter = Router
/**
* BaseServiceV2 is an advanced but simple base class for long-running TypeScript services.
*/
......@@ -71,6 +82,11 @@ export abstract class BaseServiceV2<
*/
protected done: boolean
/**
* Whether or not the service is currently healthy.
*/
protected healthy: boolean
/**
* Logger class for this service.
*/
......@@ -89,7 +105,7 @@ export abstract class BaseServiceV2<
/**
* Metrics.
*/
protected readonly metrics: TMetrics
protected readonly metrics: TMetrics & StandardMetrics
/**
* Registry for prometheus metrics.
......@@ -97,19 +113,19 @@ export abstract class BaseServiceV2<
protected readonly metricsRegistry: Registry
/**
* Metrics server.
* App server.
*/
protected metricsServer: Server
protected server: Server
/**
* Port for the metrics server.
* Port for the app server.
*/
protected readonly metricsServerPort: number
protected readonly port: number
/**
* Hostname for the metrics server.
* Hostname for the app server.
*/
protected readonly metricsServerHostname: string
protected readonly hostname: string
/**
* @param params Options for the construction of the service.
......@@ -122,18 +138,19 @@ export abstract class BaseServiceV2<
* @param params.options Options to pass to the service.
* @param params.loops Whether or not the service should loop. Defaults to true.
* @param params.loopIntervalMs Loop interval in milliseconds. Defaults to zero.
* @param params.metricsServerPort Port for the metrics server. Defaults to 7300.
* @param params.metricsServerHostname Hostname for the metrics server. Defaults to 0.0.0.0.
* @param params.port Port for the app server. Defaults to 7300.
* @param params.hostname Hostname for the app server. Defaults to 0.0.0.0.
*/
constructor(params: {
name: string
version: string
optionsSpec: OptionsSpec<TOptions>
metricsSpec: MetricsSpec<TMetrics>
options?: Partial<TOptions>
loop?: boolean
loopIntervalMs?: number
metricsServerPort?: number
metricsServerHostname?: string
port?: number
hostname?: string
}) {
this.loop = params.loop !== undefined ? params.loop : true
this.state = {} as TServiceState
......@@ -148,15 +165,40 @@ export abstract class BaseServiceV2<
desc: 'Loop interval in milliseconds',
default: params.loopIntervalMs || 0,
},
metricsServerPort: {
port: {
validator: validators.num,
desc: 'Port for the metrics server',
default: params.metricsServerPort || 7300,
desc: 'Port for the app server',
default: params.port || 7300,
},
metricsServerHostname: {
hostname: {
validator: validators.str,
desc: 'Hostname for the metrics server',
default: params.metricsServerHostname || '0.0.0.0',
desc: 'Hostname for the app server',
default: params.hostname || '0.0.0.0',
},
}
// List of options that can safely be logged.
const publicOptionNames = Object.entries(params.optionsSpec)
.filter(([, spec]) => {
return spec.secret !== true
})
.map(([key]) => {
return key
})
// Add default metrics to metrics spec.
;(params.metricsSpec as any) = {
...(params.metricsSpec || {}),
// Users cannot set these options.
metadata: {
type: Gauge,
desc: 'Service metadata',
labels: ['name', 'version'].concat(publicOptionNames),
},
unhandledErrors: {
type: Counter,
desc: 'Unhandled errors',
},
}
......@@ -264,16 +306,17 @@ export abstract class BaseServiceV2<
labelNames: spec.labels || [],
})
return acc
}, {}) as TMetrics
}, {}) as TMetrics & StandardMetrics
// Create the metrics server.
this.metricsRegistry = prometheus.register
this.metricsServerPort = this.options.metricsServerPort
this.metricsServerHostname = this.options.metricsServerHostname
this.port = this.options.port
this.hostname = this.options.hostname
// Set up everything else.
this.loopIntervalMs = this.options.loopIntervalMs
this.logger = new Logger({ name: params.name })
this.healthy = true
// Gracefully handle stop signals.
const maxSignalCount = 3
......@@ -298,6 +341,19 @@ export abstract class BaseServiceV2<
// Handle stop signals.
process.on('SIGTERM', stop)
process.on('SIGINT', stop)
// Set metadata synthetic metric.
this.metrics.metadata.set(
{
name: params.name,
version: params.version,
...publicOptionNames.reduce((acc, key) => {
acc[key] = config.str(key)
return acc
}, {}),
},
1
)
}
/**
......@@ -307,30 +363,55 @@ export abstract class BaseServiceV2<
public async run(): Promise<void> {
this.done = false
// Start the metrics server if not yet running.
if (!this.metricsServer) {
this.logger.info('starting metrics server')
// Start the app server if not yet running.
if (!this.server) {
this.logger.info('starting app server')
await new Promise((resolve) => {
const app = express()
// Start building the app.
const app = express()
// Body parsing.
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.get('/metrics', async (_, res) => {
res.status(200).send(await this.metricsRegistry.metrics())
// Logging.
app.use(morgan('short'))
// Metrics.
// Will expose a /metrics endpoint by default.
app.use(
promBundle({
promRegistry: this.metricsRegistry,
includeMethod: true,
includePath: true,
includeStatusCode: true,
})
)
this.metricsServer = app.listen(
this.metricsServerPort,
this.metricsServerHostname,
() => {
resolve(null)
}
)
// Health status.
app.get('/healthz', async (req, res) => {
return res.json({
ok: this.healthy,
})
})
this.logger.info(`metrics started`, {
port: this.metricsServerPort,
hostname: this.metricsServerHostname,
route: '/metrics',
// Registery user routes.
if (this.routes) {
const router = express.Router()
this.routes(router)
app.use('/api', router)
}
// Wait for server to come up.
await new Promise((resolve) => {
this.server = app.listen(this.port, this.hostname, () => {
resolve(null)
})
})
this.logger.info(`app server started`, {
port: this.port,
hostname: this.hostname,
})
}
......@@ -347,6 +428,7 @@ export abstract class BaseServiceV2<
try {
await this.main()
} catch (err) {
this.metrics.unhandledErrors.inc()
this.logger.error('caught an unhandled exception', {
message: err.message,
stack: err.stack,
......@@ -381,15 +463,15 @@ export abstract class BaseServiceV2<
}
// Shut down the metrics server if it's running.
if (this.metricsServer) {
if (this.server) {
this.logger.info('stopping metrics server')
await new Promise((resolve) => {
this.metricsServer.close(() => {
this.server.close(() => {
resolve(null)
})
})
this.logger.info('metrics server stopped')
this.metricsServer = undefined
this.server = undefined
}
}
......@@ -398,6 +480,13 @@ export abstract class BaseServiceV2<
*/
protected init?(): Promise<void>
/**
* Initialization function for router.
*
* @param router Express router.
*/
protected routes?(router: ExpressRouter): Promise<void>
/**
* Main function. Runs repeatedly when run() is called.
*/
......
/* External Imports */
import { ethers } from 'hardhat'
import { Contract, BigNumber } from 'ethers'
import { Contract } from 'ethers'
import { expect } from '../../setup'
const bigNumberify = (arr: any[]) => {
return arr.map((el: any) => {
if (typeof el === 'number') {
return BigNumber.from(el)
return ethers.BigNumber.from(el)
} else if (typeof el === 'string' && /^\d+n$/gm.test(el)) {
return BigNumber.from(el.slice(0, el.length - 1))
return ethers.BigNumber.from(el.slice(0, el.length - 1))
} else if (typeof el === 'string' && el.length > 2 && el.startsWith('0x')) {
return BigNumber.from(el.toLowerCase())
return ethers.BigNumber.from(el.toLowerCase())
} else if (Array.isArray(el)) {
return bigNumberify(el)
} else {
......@@ -34,9 +34,10 @@ export const runJsonTest = (contractName: string, json: any): void => {
await expect(contract.functions[functionName](...test.in)).to.be
.reverted
} else {
expect(
bigNumberify(await contract.functions[functionName](...test.in))
).to.deep.equal(bigNumberify(test.out))
const result = await contract.functions[functionName](...test.in)
expect(JSON.stringify(bigNumberify(result))).to.deep.equal(
JSON.stringify(bigNumberify(test.out))
)
}
})
}
......
......@@ -14,7 +14,6 @@ type DrippieMonOptions = {
}
type DrippieMonMetrics = {
metadata: Gauge
isExecutable: Gauge
executedDripCount: Gauge
unexpectedRpcErrors: Counter
......@@ -31,6 +30,8 @@ export class DrippieMonService extends BaseServiceV2<
> {
constructor(options?: Partial<DrippieMonOptions>) {
super({
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'drippie-mon',
loop: true,
loopIntervalMs: 60_000,
......@@ -39,6 +40,7 @@ export class DrippieMonService extends BaseServiceV2<
rpc: {
validator: validators.provider,
desc: 'Provider for network where Drippie is deployed',
secret: true,
},
drippieAddress: {
validator: validators.str,
......@@ -46,11 +48,6 @@ export class DrippieMonService extends BaseServiceV2<
},
},
metricsSpec: {
metadata: {
type: Gauge,
desc: 'Drippie Monitor metadata',
labels: ['version', 'address'],
},
isExecutable: {
type: Gauge,
desc: 'Whether or not the drip is currently executable',
......@@ -76,15 +73,6 @@ export class DrippieMonService extends BaseServiceV2<
DrippieArtifact.abi,
this.options.rpc
)
this.metrics.metadata.set(
{
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
address: this.options.drippieAddress,
},
1
)
}
protected async main(): Promise<void> {
......
import { HardhatUserConfig } from 'hardhat/types'
// Hardhat plugins
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
const config: HardhatUserConfig = {
mocha: {
timeout: 50000,
},
}
export default config
......@@ -10,7 +10,8 @@
],
"scripts": {
"start": "ts-node ./src/service.ts",
"test:coverage": "echo 'No tests defined.'",
"test": "hardhat test",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json",
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check",
......@@ -32,13 +33,22 @@
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"devDependencies": {
"@defi-wonderland/smock": "^2.0.7",
"@nomiclabs/hardhat-ethers": "^2.0.6",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@types/chai": "^4.3.1",
"@types/dateformat": "^5.0.0",
"chai-as-promised": "^7.1.1",
"dateformat": "^4.5.1",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.8",
"hardhat": "^2.9.6",
"lodash": "^4.17.21",
"ts-node": "^10.7.0"
},
"dependencies": {
"@eth-optimism/common-ts": "^0.2.8",
"@eth-optimism/contracts": "^0.5.24",
"@eth-optimism/core-utils": "^0.8.5",
"@eth-optimism/sdk": "^1.1.6",
"@ethersproject/abstract-provider": "^5.6.1"
......
import { Contract, ethers } from 'ethers'
/**
* Finds the Event that corresponds to a given state batch by index.
*
* @param scc StateCommitmentChain contract.
* @param index State batch index to search for.
* @returns Event corresponding to the batch.
*/
export const findEventForStateBatch = async (
scc: Contract,
index: number
): Promise<ethers.Event> => {
const events = await scc.queryFilter(scc.filters.StateBatchAppended(index))
// Only happens if the batch with the given index does not exist yet.
if (events.length === 0) {
throw new Error(`unable to find event for batch`)
}
// Should never happen.
if (events.length > 1) {
throw new Error(`found too many events for batch`)
}
return events[0]
}
/**
* Finds the first state batch index that has not yet passed the fault proof window.
*
* @param scc StateCommitmentChain contract.
* @returns Starting state root batch index.
*/
export const findFirstUnfinalizedStateBatchIndex = async (
scc: Contract
): Promise<number> => {
const fpw = (await scc.FRAUD_PROOF_WINDOW()).toNumber()
const latestBlock = await scc.provider.getBlock('latest')
const totalBatches = (await scc.getTotalBatches()).toNumber()
// Perform a binary search to find the next batch that will pass the challenge period.
let lo = 0
let hi = totalBatches
while (lo !== hi) {
const mid = Math.floor((lo + hi) / 2)
const event = await findEventForStateBatch(scc, mid)
const block = await event.getBlock()
if (block.timestamp + fpw < latestBlock.timestamp) {
lo = mid + 1
} else {
hi = mid
}
}
// Result will be zero if the chain is less than FPW seconds old. Only returns undefined in the
// case that no batches have been submitted for an entire challenge period.
if (lo === totalBatches) {
return undefined
} else {
return lo
}
}
export * from './service'
export * from './helpers'
import { BaseServiceV2, Gauge, validators } from '@eth-optimism/common-ts'
import { sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { CrossChainMessenger } from '@eth-optimism/sdk'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import { Contract, ethers } from 'ethers'
import dateformat from 'dateformat'
import {
findFirstUnfinalizedStateBatchIndex,
findEventForStateBatch,
} from './helpers'
type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
......@@ -19,6 +24,7 @@ type Metrics = {
}
type State = {
scc: Contract
messenger: CrossChainMessenger
highestCheckedBatchIndex: number
}
......@@ -26,6 +32,8 @@ type State = {
export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options>) {
super({
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'fault-detector',
loop: true,
loopIntervalMs: 1000,
......@@ -34,14 +42,16 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
secret: true,
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
secret: true,
},
startBatchIndex: {
validator: validators.num,
default: 0,
default: -1,
desc: 'Batch index to start checking from',
},
},
......@@ -67,19 +77,31 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
}
async init(): Promise<void> {
const network = await this.options.l1RpcProvider.getNetwork()
this.state.messenger = new CrossChainMessenger({
l1SignerOrProvider: this.options.l1RpcProvider,
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId: network.chainId,
l1ChainId: await getChainId(this.options.l1RpcProvider),
})
this.state.highestCheckedBatchIndex = this.options.startBatchIndex
// We use this a lot, a bit cleaner to pull out to the top level of the state object.
this.state.scc = this.state.messenger.contracts.l1.StateCommitmentChain
// Figure out where to start syncing from.
if (this.options.startBatchIndex === -1) {
this.logger.info(`finding appropriate starting height`)
this.state.highestCheckedBatchIndex =
await findFirstUnfinalizedStateBatchIndex(this.state.scc)
} else {
this.state.highestCheckedBatchIndex = this.options.startBatchIndex
}
this.logger.info(`starting height`, {
startBatchIndex: this.state.highestCheckedBatchIndex,
})
}
async main(): Promise<void> {
const latestBatchIndex =
await this.state.messenger.contracts.l1.StateCommitmentChain.getTotalBatches()
const latestBatchIndex = await this.state.scc.getTotalBatches()
if (this.state.highestCheckedBatchIndex >= latestBatchIndex.toNumber()) {
await sleep(15000)
return
......@@ -89,41 +111,30 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
this.logger.info(`checking batch`, {
batchIndex: this.state.highestCheckedBatchIndex,
latestIndex: latestBatchIndex.toNumber(),
})
const targetEvents =
await this.state.messenger.contracts.l1.StateCommitmentChain.queryFilter(
this.state.messenger.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(
this.state.highestCheckedBatchIndex
)
let event: ethers.Event
try {
event = await findEventForStateBatch(
this.state.scc,
this.state.highestCheckedBatchIndex
)
if (targetEvents.length === 0) {
this.logger.error(`unable to find event for batch`, {
batchIndex: this.state.highestCheckedBatchIndex,
})
this.metrics.inUnexpectedErrorState.set(1)
return
}
if (targetEvents.length > 1) {
this.logger.error(`found too many events for batch`, {
} catch (err) {
this.logger.error(`got unexpected error while searching for batch`, {
batchIndex: this.state.highestCheckedBatchIndex,
error: err,
})
this.metrics.inUnexpectedErrorState.set(1)
return
}
const targetEvent = targetEvents[0]
const batchTransaction = await targetEvent.getTransaction()
const [stateRoots] =
this.state.messenger.contracts.l1.StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
batchTransaction.data
)
const batchTransaction = await event.getTransaction()
const [stateRoots] = this.state.scc.interface.decodeFunctionData(
'appendStateBatch',
batchTransaction.data
)
const batchStart = targetEvent.args._prevTotalElements.toNumber() + 1
const batchSize = targetEvent.args._batchSize.toNumber()
const batchStart = event.args._prevTotalElements.toNumber() + 1
const batchSize = event.args._batchSize.toNumber()
// `getBlockRange` has a limit of 1000 blocks, so we have to break this request out into
// multiple requests of maximum 1000 blocks in the case that batchSize > 1000.
......@@ -143,8 +154,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
for (const [i, stateRoot] of stateRoots.entries()) {
if (blocks[i].stateRoot !== stateRoot) {
this.metrics.isCurrentlyMismatched.set(1)
const fpw =
await this.state.messenger.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
const fpw = await this.state.scc.FRAUD_PROOF_WINDOW()
this.logger.error(`state root mismatch`, {
blockNumber: blocks[i].number,
expectedStateRoot: blocks[i].stateRoot,
......@@ -162,10 +172,10 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
}
}
this.state.highestCheckedBatchIndex++
this.metrics.highestCheckedBatchIndex.set(
this.state.highestCheckedBatchIndex
)
this.state.highestCheckedBatchIndex++
// If we got through the above without throwing an error, we should be fine to reset.
this.metrics.isCurrentlyMismatched.set(0)
......
import hre from 'hardhat'
import { Contract } from 'ethers'
import { toRpcHexString } from '@eth-optimism/core-utils'
import {
getContractFactory,
getContractInterface,
} from '@eth-optimism/contracts'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { smock, FakeContract } from '@defi-wonderland/smock'
import { expect } from './setup'
import {
findEventForStateBatch,
findFirstUnfinalizedStateBatchIndex,
} from '../src'
describe('helpers', () => {
// Can be any non-zero value, 1000 is fine.
const challengeWindowSeconds = 1000
let signer: SignerWithAddress
before(async () => {
;[signer] = await hre.ethers.getSigners()
})
let FakeBondManager: FakeContract<Contract>
let FakeCanonicalTransactionChain: FakeContract<Contract>
let AddressManager: Contract
let ChainStorageContainer: Contract
let StateCommitmentChain: Contract
beforeEach(async () => {
// Set up fakes
FakeBondManager = await smock.fake(getContractInterface('BondManager'))
FakeCanonicalTransactionChain = await smock.fake(
getContractInterface('CanonicalTransactionChain')
)
// Set up contracts
AddressManager = await getContractFactory(
'Lib_AddressManager',
signer
).deploy()
ChainStorageContainer = await getContractFactory(
'ChainStorageContainer',
signer
).deploy(AddressManager.address, 'StateCommitmentChain')
StateCommitmentChain = await getContractFactory(
'StateCommitmentChain',
signer
).deploy(AddressManager.address, challengeWindowSeconds, 10000000)
// Set addresses in manager
await AddressManager.setAddress(
'ChainStorageContainer-SCC-batches',
ChainStorageContainer.address
)
await AddressManager.setAddress(
'StateCommitmentChain',
StateCommitmentChain.address
)
await AddressManager.setAddress(
'CanonicalTransactionChain',
FakeCanonicalTransactionChain.address
)
await AddressManager.setAddress('BondManager', FakeBondManager.address)
// Set up mock returns
FakeCanonicalTransactionChain.getTotalElements.returns(1000000000) // just needs to be large
FakeBondManager.isCollateralized.returns(true)
})
describe('findEventForStateBatch', () => {
describe('when the event exists once', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
})
it('should return the event', async () => {
const event = await findEventForStateBatch(StateCommitmentChain, 0)
expect(event.args._batchIndex).to.equal(0)
})
})
describe('when the event does not exist', () => {
it('should throw an error', async () => {
await expect(
findEventForStateBatch(StateCommitmentChain, 0)
).to.eventually.be.rejectedWith('unable to find event for batch')
})
})
describe('when more than one event exists', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
await hre.ethers.provider.send('hardhat_setStorageAt', [
ChainStorageContainer.address,
'0x2',
hre.ethers.constants.HashZero,
])
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
})
it('should throw an error', async () => {
await expect(
findEventForStateBatch(StateCommitmentChain, 0)
).to.eventually.be.rejectedWith('found too many events for batch')
})
})
})
describe('findFirstUnfinalizedIndex', () => {
describe('when the chain is more then FPW seconds old', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
// Simulate FPW passing
await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(challengeWindowSeconds * 2),
])
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
1
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
2
)
})
it('should find the first batch older than the FPW', async () => {
const first = await findFirstUnfinalizedStateBatchIndex(
StateCommitmentChain
)
expect(first).to.equal(1)
})
})
describe('when the chain is less than FPW seconds old', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
1
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
2
)
})
it('should return zero', async () => {
const first = await findFirstUnfinalizedStateBatchIndex(
StateCommitmentChain
)
expect(first).to.equal(0)
})
})
describe('when no batches submitted for the entire FPW', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
1
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
2
)
// Simulate FPW passing and no new batches
await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(challengeWindowSeconds * 2),
])
// Mine a block to force timestamp to update
await hre.ethers.provider.send('hardhat_mine', ['0x1'])
})
it('should return undefined', async () => {
const first = await findFirstUnfinalizedStateBatchIndex(
StateCommitmentChain
)
expect(first).to.equal(undefined)
})
})
})
})
import chai = require('chai')
import chaiAsPromised from 'chai-as-promised'
// Chai plugins go here.
chai.use(chaiAsPromised)
const should = chai.should()
const expect = chai.expect
export { should, expect }
......@@ -37,20 +37,25 @@ export class MessageRelayerService extends BaseServiceV2<
> {
constructor(options?: Partial<MessageRelayerOptions>) {
super({
name: 'Message_Relayer',
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'message-relayer',
options,
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1.',
secret: true,
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2.',
secret: true,
},
l1Wallet: {
validator: validators.wallet,
desc: 'Wallet used to interact with L1.',
secret: true,
},
fromL2TransactionIndex: {
validator: validators.num,
......
......@@ -32,17 +32,21 @@ export class HealthcheckService extends BaseServiceV2<
> {
constructor(options?: Partial<HealthcheckOptions>) {
super({
name: 'Healthcheck',
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
name: 'healthcheck',
loopIntervalMs: 5000,
options,
optionsSpec: {
referenceRpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
secret: true,
},
targetRpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
secret: true,
},
onDivergenceWaitMs: {
validator: validators.num,
......
......@@ -50,9 +50,11 @@ transaction types:
1. They are derived from Layer 1 blocks, and must be included as part of the protocol.
2. They do not include signature validation (see [User-Deposited Transactions][user-deposited]
for the rationale).
3. They buy their L2 gas on L1 and, as such, the L2 gas is not refundable.
We define a new [EIP-2718] compatible transaction type with the prefix `0x7E`, and the following
fields (rlp encoded in the order they appear here):
We define a new [EIP-2718] compatible transaction type with the prefix `0x7E`, and then a versioned
byte sequence. The first version has `0x00` as the version byte and then as the following fields
(rlp encoded in the order they appear here):
[EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
......@@ -75,6 +77,9 @@ Picking a high identifier minimizes the risk that the identifier will be used be
transaction type on the L1 chain in the future. We don't pick `0x7F` itself in case it becomes used
for a variable-length encoding scheme.
We chose to add a version field to the deposit transaction to enable the protocol to upgrade the deposit
transaction type without having to take another [EIP-2718] transaction type selector.
### Source hash computation
The `sourceHash` of a deposit transaction is computed based on the origin:
......@@ -138,6 +143,11 @@ follows:
- `context.gas` set to `gasLimit`
- `context.value` set to `sendValue`
No gas is bought on L2 and no refund is provided. The gas used for the deposit is subtracted from
the gas pool on L2. Gas usage exactly matches other transaction types (including intrinsic gas).
If a deposit runs out of gas or has some other failure, the mint will succeed and the nonce of the
account will be increased, but no other state transition will occur.
#### Nonce Handling
Despite the lack of signature validation, we still increment the nonce of the `from` account when a
......@@ -163,7 +173,7 @@ This transaction MUST have the following values:
contract][predeploy]).
3. `mint` is `0`
4. `value` is `0`
5. `gasLimit` is set to the maximum available.
5. `gasLimit` is set to 75,000.
6. `data` is an [ABI] encoded call to the [L1 attributes predeployed contract][predeploy]'s
`setL1BlockValues()` function with correct values associated with the corresponding L1 block (cf.
[reference implementation][l1-attr-ref-implem]).
......@@ -244,6 +254,10 @@ feed contract][deposit-feed-contract] on L1.
The deposit contract is deployed to L1. Deposited transactions are derived from the values in
the `TransactionDeposited` event(s) emitted by the deposit contract.
The deposit contract is responsible for maintaing the [guaranteed gas market](./guaranteed-gas-market.md),
charging deposits for gas to be used on L2, and ensuring that the total amount of guaranted
gas in a single L1 block does not exceed the L2 block gas limit.
The deposit contract handles two special cases:
1. A contract creation deposit, which is indicated by setting the `isCreation` flag to `true`.
......
......@@ -15,6 +15,7 @@
- [Receipt](#receipt)
- [Transaction Type](#transaction-type)
- [Fork Choice Rule](#fork-choice-rule)
- [Priority Gas Auction](#priority-gas-auction)
- [Sequencing](#sequencing)
- [Sequencing window](#sequencing-window)
- [Sequencing epoch](#sequencing-epoch)
......@@ -137,11 +138,20 @@ Different transaction types can contain different payloads, and be handled diffe
The fork choice rule is the rule used to determined which block is to be considered as the head of a blockchain. On L1,
this is determined by the proof of stake rules.
L2 also has a fork choice rule, although the rules vary depending on wether we want the sequencer-confirmed head, the
L2 also has a fork choice rule, although the rules vary depending on whether we want the sequencer-confirmed head, the
on-chain-confirmed head, or the on-chain-finalized head.
> TODO: define and link to those concepts
## Priority Gas Auction
Transactions in ethereum are ordered by the price that the transaction pays to the miner. Priority Gas Auctions
(PGAs) occur when multiple parties are competing to be the first transaction in a block. Each party continuously
updates the gas price of their transaction. PGAs occur when there is value in submitting a transaction before other
parties (like being the first deposit or submitting a deposit before there is not more guaranteed gas remaining).
PGAs tend to have negative externalities on the network due to a large amount of transactions being submitted in a
very short amount of time.
------------------------------------------------------------------------------------------------------------------------
# Sequencing
......
# Guaranteed Gas Fee Market
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Gas Stipend](#gas-stipend)
- [Limiting Guaranteed Gas](#limiting-guaranteed-gas)
- [Rationale for burning L1 Gas](#rationale-for-burning-l1-gas)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
[Deposited transaction](./glossary.md#deposited-transaction) are transactions on L2 that are
initiated on L1. The gas that they use on L2 is bought on L1 via a gas burn or a direct payment. We
maintain a fee market and hard cap on the amount of gas provided to all deposits in a single L1
block.
The gas provided to deposited transactions is sometimes called "guaranteed gas". The gas provided to
deposited transactions is unique in the regard that it is not refundable. It cannot be refunded as
it is sometimes paid for with a gas burn and there may not be any ETH left to refund.
The **guaranteed gas** is composed of a gas stipend, and of any guaranteed gas the user would like
to purchase (on L1) on top of that.
Guaranteed gas on L2 is bought in the following manner. An L2 gas price is calculated via an
EIP-1559-style algorithm. The total amount of ETH required to buy that gas is then calculated as
(`guaranteed gas * L2 deposit basefee`). The contract then accepts that amount of ETH (in a future
upgrade) or (only method right now), burns an amount of L1 gas that corresponds to the L2 cost
(`L2 cost / L1 Basefee`). The L2 gas price for guaranteed gas is not synchronized with the basefee
on L2 and will likely be different.
## Gas Stipend
To offset the gas spent on the deposit event, we credit `gas spent * L1 basefee` ETH to the cost of
the L2 gas, where `gas spent` is the amount of L1 gas spent processing the deposit. If the ETH value
of this credit is greater than the ETH value of the requested guaranteed gas
(`requested guaranteed gas * L2 gas price`), no L1 gas is burnt.
## Limiting Guaranteed Gas
The total amount of guaranteed gas that can be bought in a single L1 block must be limited to
prevent a denial of service attack against L2 as well as ensure the total amount of guaranteed gas
stays below the L2 block gas limit.
We set a guaranteed gas limit of 8,000,000 gas per L1 block and a target of 2,000,000 gas per L1
block. These numbers enabled occasional large transactions while staying within our target and
maximum gas usage on L2.
Because the amount of guaranteed L2 gas that can be purchased in a single block is now limited,
we implement an EIP-1559-style fee market to reduce congestion on deposits. By setting the limit
at a multiple of the target, we enable deposits to temporarily use more L2 gas at a greater cost.
```python
# Pseudocode to update the L2 Deposit Basefee and cap the amount of guaranteed gas
# bought in a block. Calling code must handle the gas burn and validity checks on
# the ability of the account to afford this gas.
BASE_FEE_MAX_CHANGE_DENOMINATOR = 8
ELASTICITY_MULTIPLIER = 4
MAX_RESOURCE_LIMIT = 8,000,000
TARGET_RESOURCE_LIMIT = MAX_RESOURCE_LIMIT / ELASTICITY_MULTIPLIER
MINIMUM_BASEFEE=10000
# prev_basefee is a u128, prev_bought_gas and prev_num are u64s
prev_basefee, prev_bought_gas, prev_num = <values from previous update>
now_num = block.number
# Clamp the full basefee to a specific range. The minimum value in the range should be around 100-1000
# to enable faster responses in the basefee. This replaces the `max` mechanism in the ethereum 1559
# implementation (it also serves to enable the basefee to increase if it is very small).
def clamp(v: i256, min: u128, max: u128) -> u128:
if v < i256(min):
return min
elif v > i256(max):
return max
else:
return u128(v)
# If this is a new block, update the basefee and reset the total gas
# If not, just update the total gas
if prev_num == now_num:
now_basefee = prev_basefee
now_bought_gas = prev_bought_gas + requested_gas
elif prev_num != now_num :
# Width extension and conversion to signed integer math
gas_used_delta = int128(prev_bought_gas) - int128(TARGET_RESOURCE_LIMIT)
# Use truncating (round to 0) division - solidity's default.
# Sign extend gas_used_delta & prev_basefee to 256 bits to avoid overflows here.
base_fee_per_gas_delta = prev_basefee * gas_used_delta / TARGET_RESOURCE_LIMIT / BASE_FEE_MAX_CHANGE_DENOMINATOR
now_basefee_wide = prev_basefee + base_fee_per_gas_delta
now_basefee = clamp(now_basefee_wide, min=MINIMUM_BASEFEE, max=UINT_64_MAX_VALUE)
now_bought_gas = requested_gas
# If we skipped multiple blocks between the previous block and now update the basefee again.
# This is not exactly the same as iterating the above function, but quite close for reasonable
# gas target values. It is also constant time wrt the number of missed blocks which is important
# for keeping gas usage stable.
if prev_num + 1 < now_num:
n = now_num - prev_num - 1
# Apply 7/8 reduction to prev_basefee for the n empty blocks in a row.
now_basefee_wide = prev_basefee * pow(1-(1/BASE_FEE_MAX_CHANGE_DENOMINATOR), n)
now_basefee = clamp(now_basefee_wide, min=MINIMUM_BASEFEE, max=UINT_64_MAX_VALUE)
require(now_bought_gas < MAX_RESOURCE_LIMIT)
store_values(now_basefee, now_bought_gas, now_num)
```
## Rationale for burning L1 Gas
If we collect ETH directly to pay for L2 gas, every (indirect) caller of the deposit function will need
to be marked with the payable selector. This won't be possible for many existing projects. Unfortunately
this is quite wasteful. As such, we will provide two options to buy L2 gas:
1. Burn L1 Gas
2. Send ETH to the Optimism Portal (Not yet supported)
The payable version (Option 2) will likely have discount applied to it (or conversely, #1 has a
premium applied to it).
For the initial release of bedrock, only #1 is supported.
package api
import (
"context"
"math/big"
"sync"
"time"
"github.com/ethereum-optimism/optimism/teleportr/bindings/deposit"
"github.com/ethereum-optimism/optimism/teleportr/bindings/disburse"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
type ChainDataReader interface {
Get(ctx context.Context) (*ChainData, error)
}
type ChainData struct {
MaxBalance *big.Int
DisburserBalance *big.Int
NextDisbursementID uint64
DepositContractBalance *big.Int
NextDepositID uint64
MaxDepositAmount *big.Int
MinDepositAmount *big.Int
}
type chainDataReaderImpl struct {
l1Client *ethclient.Client
l2Client *ethclient.Client
depositContract *deposit.TeleportrDeposit
depositContractAddr common.Address
disburserContract *disburse.TeleportrDisburser
disburserWalletAddr common.Address
}
func NewChainDataReader(
l1Client, l2Client *ethclient.Client,
depositContractAddr, disburserWalletAddr common.Address,
depositContract *deposit.TeleportrDeposit,
disburserContract *disburse.TeleportrDisburser,
) ChainDataReader {
return &chainDataReaderImpl{
l1Client: l1Client,
l2Client: l2Client,
depositContract: depositContract,
depositContractAddr: depositContractAddr,
disburserContract: disburserContract,
disburserWalletAddr: disburserWalletAddr,
}
}
func (c *chainDataReaderImpl) maxDepositBalance(ctx context.Context) (*big.Int, error) {
return c.depositContract.MaxBalance(&bind.CallOpts{
Context: ctx,
})
}
func (c *chainDataReaderImpl) disburserBalance(ctx context.Context) (*big.Int, error) {
return c.l2Client.BalanceAt(ctx, c.disburserWalletAddr, nil)
}
func (c *chainDataReaderImpl) nextDisbursementID(ctx context.Context) (uint64, error) {
total, err := c.disburserContract.TotalDisbursements(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return 0, err
}
return total.Uint64(), nil
}
func (c *chainDataReaderImpl) depositContractBalance(ctx context.Context) (*big.Int, error) {
return c.l1Client.BalanceAt(ctx, c.depositContractAddr, nil)
}
func (c *chainDataReaderImpl) nextDepositID(ctx context.Context) (uint64, error) {
total, err := c.depositContract.TotalDeposits(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return 0, err
}
return total.Uint64(), nil
}
func (c *chainDataReaderImpl) maxDepositAmount(ctx context.Context) (*big.Int, error) {
return c.depositContract.MaxDepositAmount(&bind.CallOpts{
Context: ctx,
})
}
func (c *chainDataReaderImpl) minDepositAmount(ctx context.Context) (*big.Int, error) {
return c.depositContract.MinDepositAmount(&bind.CallOpts{
Context: ctx,
})
}
func (c *chainDataReaderImpl) Get(ctx context.Context) (*ChainData, error) {
maxBalance, err := c.maxDepositBalance(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("max_balance").Inc()
return nil, err
}
disburserBal, err := c.disburserBalance(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("disburser_wallet_balance_at").Inc()
return nil, err
}
nextDisbursementID, err := c.nextDisbursementID(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("next_disbursement_id").Inc()
return nil, err
}
depositContractBalance, err := c.depositContractBalance(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("deposit_balance_at").Inc()
return nil, err
}
nextDepositID, err := c.nextDepositID(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("next_deposit_id").Inc()
return nil, err
}
maxDepositAmount, err := c.maxDepositAmount(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("max_deposit_amount").Inc()
return nil, err
}
minDepositAmount, err := c.minDepositAmount(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("min_deposit_amount").Inc()
return nil, err
}
return &ChainData{
MaxBalance: maxBalance,
DisburserBalance: disburserBal,
NextDisbursementID: nextDisbursementID,
DepositContractBalance: depositContractBalance,
NextDepositID: nextDepositID,
MaxDepositAmount: maxDepositAmount,
MinDepositAmount: minDepositAmount,
}, nil
}
type cachingChainDataReader struct {
inner ChainDataReader
interval time.Duration
last time.Time
data *ChainData
mu sync.Mutex
}
func NewCachingChainDataReader(inner ChainDataReader, interval time.Duration) ChainDataReader {
return &cachingChainDataReader{
inner: inner,
interval: interval,
}
}
func (c *cachingChainDataReader) Get(ctx context.Context) (*ChainData, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.data != nil && time.Since(c.last) < c.interval {
return c.data, nil
}
data, err := c.inner.Get(ctx)
if err != nil {
return nil, err
}
c.data = data
c.last = time.Now()
return c.data, nil
}
package api
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type NoopChainDataReader struct {
CallCount int
Data *ChainData
}
func (n *NoopChainDataReader) Get(ctx context.Context) (*ChainData, error) {
n.CallCount++
return &ChainData{
MaxBalance: n.Data.MaxBalance,
DisburserBalance: n.Data.DisburserBalance,
NextDisbursementID: n.Data.NextDisbursementID,
DepositContractBalance: n.Data.DepositContractBalance,
NextDepositID: n.Data.NextDepositID,
MaxDepositAmount: n.Data.MaxDepositAmount,
MinDepositAmount: n.Data.MinDepositAmount,
}, nil
}
func TestCachingChainDataReaderGet(t *testing.T) {
inner := &NoopChainDataReader{
Data: &ChainData{
NextDisbursementID: 1,
},
}
require.Equal(t, inner.CallCount, 0)
cdr := NewCachingChainDataReader(inner, 5*time.Millisecond)
data, err := cdr.Get(context.Background())
require.NoError(t, err)
require.Equal(t, 1, inner.CallCount)
require.NotNil(t, data)
inner.Data = &ChainData{
NextDisbursementID: 2,
}
data, err = cdr.Get(context.Background())
require.NoError(t, err)
require.Equal(t, 1, inner.CallCount)
require.EqualValues(t, data.NextDisbursementID, 1)
time.Sleep(10 * time.Millisecond)
data, err = cdr.Get(context.Background())
require.NoError(t, err)
require.Equal(t, 2, inner.CallCount)
require.EqualValues(t, data.NextDisbursementID, 2)
}
......@@ -21,10 +21,10 @@ import (
"github.com/ethereum-optimism/optimism/bss-core/metrics"
"github.com/ethereum-optimism/optimism/bss-core/txmgr"
"github.com/ethereum-optimism/optimism/teleportr/bindings/deposit"
"github.com/ethereum-optimism/optimism/teleportr/bindings/disburse"
"github.com/ethereum-optimism/optimism/teleportr/db"
"github.com/ethereum-optimism/optimism/teleportr/flags"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
......@@ -39,6 +39,8 @@ type ContextKey string
const (
ContextKeyReqID ContextKey = "req_id"
MaxLagBeforeUnavailable = 10
)
func Main(gitVersion string) func(*cli.Context) error {
......@@ -61,6 +63,11 @@ func Main(gitVersion string) func(*cli.Context) error {
return err
}
disburserAddr, err := bsscore.ParseAddress(cfg.DisburserAddress)
if err != nil {
return err
}
l1Client, err := dial.L1EthClientWithTimeout(
ctx, cfg.L1EthRpc, cfg.DisableHTTP2,
)
......@@ -84,6 +91,22 @@ func Main(gitVersion string) func(*cli.Context) error {
return err
}
disburserContract, err := disburse.NewTeleportrDisburser(
disburserAddr, l2Client,
)
if err != nil {
return err
}
cdr := NewChainDataReader(
l1Client,
l2Client,
depositAddr,
disburserWalletAddr,
depositContract,
disburserContract,
)
// TODO(conner): make read-only
database, err := db.Open(db.Config{
Host: cfg.PostgresHost,
......@@ -107,9 +130,8 @@ func Main(gitVersion string) func(*cli.Context) error {
l1Client,
l2Client,
database,
NewCachingChainDataReader(cdr, time.Minute),
depositAddr,
disburserWalletAddr,
depositContract,
cfg.NumConfirmations,
)
......@@ -151,6 +173,7 @@ type Config struct {
DepositAddress string
NumConfirmations uint64
DisburserWalletAddress string
DisburserAddress string
PostgresHost string
PostgresPort uint16
PostgresUser string
......@@ -172,6 +195,7 @@ func NewConfig(ctx *cli.Context) (Config, error) {
DepositAddress: ctx.GlobalString(flags.DepositAddressFlag.Name),
NumConfirmations: ctx.GlobalUint64(flags.NumDepositConfirmationsFlag.Name),
DisburserWalletAddress: ctx.GlobalString(flags.DisburserWalletAddressFlag.Name),
DisburserAddress: ctx.GlobalString(flags.DisburserAddressFlag.Name),
PostgresHost: ctx.GlobalString(flags.PostgresHostFlag.Name),
PostgresPort: uint16(ctx.GlobalUint64(flags.PostgresPortFlag.Name)),
PostgresUser: ctx.GlobalString(flags.PostgresUserFlag.Name),
......@@ -193,14 +217,13 @@ const (
)
type Server struct {
ctx context.Context
l1Client *ethclient.Client
l2Client *ethclient.Client
database *db.Database
depositAddr common.Address
disburserWalletAddr common.Address
depositContract *deposit.TeleportrDeposit
numConfirmations uint64
ctx context.Context
l1Client *ethclient.Client
l2Client *ethclient.Client
database *db.Database
chainDataReader ChainDataReader
depositAddr common.Address
numConfirmations uint64
httpServer *http.Server
}
......@@ -210,9 +233,8 @@ func NewServer(
l1Client *ethclient.Client,
l2Client *ethclient.Client,
database *db.Database,
chainDataReader ChainDataReader,
depositAddr common.Address,
disburserWalletAddr common.Address,
depositContract *deposit.TeleportrDeposit,
numConfirmations uint64,
) *Server {
if numConfirmations == 0 {
......@@ -220,14 +242,13 @@ func NewServer(
}
return &Server{
ctx: ctx,
l1Client: l1Client,
l2Client: l2Client,
database: database,
depositAddr: depositAddr,
disburserWalletAddr: disburserWalletAddr,
depositContract: depositContract,
numConfirmations: numConfirmations,
ctx: ctx,
l1Client: l1Client,
l2Client: l2Client,
database: database,
chainDataReader: chainDataReader,
depositAddr: depositAddr,
numConfirmations: numConfirmations,
}
}
......@@ -275,6 +296,7 @@ type StatusResponse struct {
MaximumBalanceWei string `json:"maximum_balance_wei"`
MinDepositAmountWei string `json:"min_deposit_amount_wei"`
MaxDepositAmountWei string `json:"max_deposit_amount_wei"`
DisbursementLag uint64 `json:"disbursement_lag"`
IsAvailable bool `json:"is_available"`
}
......@@ -283,54 +305,26 @@ func (s *Server) HandleStatus(
w http.ResponseWriter,
r *http.Request,
) error {
maxBalance, err := s.depositContract.MaxBalance(&bind.CallOpts{
Context: ctx,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("max_balance").Inc()
return err
}
minDepositAmount, err := s.depositContract.MinDepositAmount(&bind.CallOpts{
Context: ctx,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("min_deposit_amount").Inc()
return err
}
maxDepositAmount, err := s.depositContract.MaxDepositAmount(&bind.CallOpts{
Context: ctx,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("max_deposit_amount").Inc()
return err
}
curBalance, err := s.l1Client.BalanceAt(ctx, s.depositAddr, nil)
if err != nil {
rpcErrorsTotal.WithLabelValues("deposit_balance_at").Inc()
return err
}
disburserWalletBal, err := s.l2Client.BalanceAt(ctx, s.disburserWalletAddr, nil)
chainData, err := s.chainDataReader.Get(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("disburser_wallet_balance_at").Inc()
return err
}
balanceAfterMaxDeposit := new(big.Int).Add(
curBalance, maxDepositAmount,
chainData.DepositContractBalance, chainData.MaxDepositAmount,
)
isAvailable := maxBalance.Cmp(balanceAfterMaxDeposit) >= 0 && disburserWalletBal.Cmp(maxDepositAmount) > 0
disbursementLag := chainData.NextDepositID - chainData.NextDisbursementID
isAvailable := chainData.MaxBalance.Cmp(balanceAfterMaxDeposit) >= 0 &&
chainData.DisburserBalance.Cmp(chainData.MaxDepositAmount) > 0 &&
disbursementLag < MaxLagBeforeUnavailable
resp := StatusResponse{
DisburserWalletBalanceWei: disburserWalletBal.String(),
DepositContractBalanceWei: curBalance.String(),
MaximumBalanceWei: maxBalance.String(),
MinDepositAmountWei: minDepositAmount.String(),
MaxDepositAmountWei: maxDepositAmount.String(),
DisburserWalletBalanceWei: chainData.DisburserBalance.String(),
DepositContractBalanceWei: chainData.DepositContractBalance.String(),
MaximumBalanceWei: chainData.MaxBalance.String(),
MinDepositAmountWei: chainData.MinDepositAmount.String(),
MaxDepositAmountWei: chainData.MaxDepositAmount.String(),
DisbursementLag: disbursementLag,
IsAvailable: isAvailable,
}
......
......@@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/cenkalti/backoff"
"github.com/ethereum-optimism/optimism/bss-core/metrics"
"github.com/ethereum-optimism/optimism/bss-core/txmgr"
"github.com/ethereum-optimism/optimism/teleportr/bindings/deposit"
......@@ -344,7 +345,20 @@ func (d *Driver) SendTransaction(
return err
}
return d.cfg.L2Client.SendTransaction(ctx, tx)
// This requires special handling - if this request fails,
// then teleportr will halt. Use exponential backoff here to
// handle expected failures (e.g., 503s, 524s, etc.).
return backoff.Retry(func() error {
subCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
err := d.cfg.L2Client.SendTransaction(subCtx, tx)
if !IsRetryableError(err) {
d.metrics.FailedTXSubmissions.WithLabelValues("permanent").Inc()
return backoff.Permanent(err)
}
d.metrics.FailedTXSubmissions.WithLabelValues("recoverable").Inc()
return err
}, DefaultBackoff)
}
// processPendingTxs is a helper method which updates Postgres with the effects
......
......@@ -75,6 +75,10 @@ type Metrics struct {
// DepositContractBalance tracks Teleportr's deposit contract balance.
DepositContractBalance prometheus.Gauge
// FailedTXSubmissions tracks failed requests to eth_sendRawTransaction
// during transaction submission.
FailedTXSubmissions *prometheus.CounterVec
}
// NewMetrics initializes a new, extended metrics object.
......@@ -136,5 +140,12 @@ func NewMetrics(subsystem string) *Metrics {
Help: "Balance in Wei of Teleportr's deposit contract",
Subsystem: base.SubsystemName(),
}),
FailedTXSubmissions: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "failed_tx_submissions",
Help: "Number of failed transaction submissions",
Subsystem: base.SubsystemName(),
}, []string{
"type",
}),
}
}
package disburser
import (
"context"
"errors"
"regexp"
"time"
"github.com/ethereum/go-ethereum/rpc"
"github.com/cenkalti/backoff"
)
var retryRegexes = []*regexp.Regexp{
regexp.MustCompile("read: connection reset by peer$"),
}
var DefaultBackoff = &backoff.ExponentialBackOff{
InitialInterval: backoff.DefaultInitialInterval,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: time.Minute,
Clock: backoff.SystemClock,
}
func IsRetryableError(err error) bool {
msg := err.Error()
if httpErr, ok := err.(rpc.HTTPError); ok {
if httpErr.StatusCode == 503 || httpErr.StatusCode == 524 || httpErr.StatusCode == 429 {
return true
}
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
for _, reg := range retryRegexes {
if reg.MatchString(msg) {
return true
}
}
return false
}
package disburser
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)
func TestIsRetryableError(t *testing.T) {
var resCode int32
var res atomic.Value
res.Store([]byte{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(int(atomic.LoadInt32(&resCode)))
_, _ = w.Write(res.Load().([]byte))
}))
defer server.Close()
client, err := ethclient.Dial(server.URL)
require.NoError(t, err)
tests := []struct {
code int
retryable bool
}{
{
503,
true,
},
{
524,
true,
},
{
429,
true,
},
{
500,
false,
},
{
200,
false,
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("http %d", tt.code), func(t *testing.T) {
atomic.StoreInt32(&resCode, int32(tt.code))
_, err := client.BlockNumber(context.Background())
require.Equal(t, tt.retryable, IsRetryableError(err))
})
}
require.True(t, IsRetryableError(context.DeadlineExceeded))
require.True(t, IsRetryableError(errors.New("read: connection reset by peer")))
}
......@@ -36,6 +36,7 @@ var APIFlags = []cli.Flag{
APIHostnameFlag,
APIPortFlag,
DisburserWalletAddressFlag,
DisburserAddressFlag,
L1EthRpcFlag,
L2EthRpcFlag,
DepositAddressFlag,
......
module github.com/ethereum-optimism/optimism/teleportr
go 1.17
go 1.18
replace github.com/ethereum-optimism/optimism/bss-core v0.0.0 => ../bss-core
......@@ -20,6 +20,7 @@ require (
github.com/VictoriaMetrics/fastcache v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.2 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
......
......@@ -130,6 +130,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
......
This diff is collapsed.
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