diff --git a/.changeset/five-fireants-notice.md b/.changeset/five-fireants-notice.md new file mode 100644 index 0000000000000..6e5731e20e2c1 --- /dev/null +++ b/.changeset/five-fireants-notice.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/common-ts': minor +--- + +Minor upgrade to BaseServiceV2 to expose a full customizable server, instead of just metrics. diff --git a/.changeset/ninety-boxes-melt.md b/.changeset/ninety-boxes-melt.md new file mode 100644 index 0000000000000..d2d633159508d --- /dev/null +++ b/.changeset/ninety-boxes-melt.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/fault-detector': patch +--- + +Smarter starting height for fault-detector diff --git a/.changeset/rude-eggs-pull.md b/.changeset/rude-eggs-pull.md new file mode 100644 index 0000000000000..08a76b14723e8 --- /dev/null +++ b/.changeset/rude-eggs-pull.md @@ -0,0 +1,9 @@ +--- +'@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 diff --git a/.changeset/twelve-insects-marry.md b/.changeset/twelve-insects-marry.md new file mode 100644 index 0000000000000..9cdeab941db48 --- /dev/null +++ b/.changeset/twelve-insects-marry.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/teleportr': patch +--- + +Better availability endpoint + retries diff --git a/.changeset/wicked-knives-jog.md b/.changeset/wicked-knives-jog.md new file mode 100644 index 0000000000000..302cba7d08f1e --- /dev/null +++ b/.changeset/wicked-knives-jog.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/fault-detector': patch +--- + +Fix order in which a metric was bumped then emitted to fix off by one issue diff --git a/.circleci/config.yml b/.circleci/config.yml index cc77028a86350..604b8c42071b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000000000..a273ce80be93a --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,23 @@ +# 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 diff --git a/batch-submitter/go.mod b/batch-submitter/go.mod index e96a0aef0e541..ea91e7445b6ab 100644 --- a/batch-submitter/go.mod +++ b/batch-submitter/go.mod @@ -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 diff --git a/batch-submitter/go.sum b/batch-submitter/go.sum index cefaf74b6a0e2..1756970c6cdd9 100644 --- a/batch-submitter/go.sum +++ b/batch-submitter/go.sum @@ -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= diff --git a/l2geth/eth/downloader/downloader_test.go b/l2geth/eth/downloader/downloader_test.go index 9362b088b1e62..028792971f8a1 100644 --- a/l2geth/eth/downloader/downloader_test.go +++ b/l2geth/eth/downloader/downloader_test.go @@ -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() diff --git a/packages/common-ts/package.json b/packages/common-ts/package.json index 633b3ff070ebb..1336c6235f01f 100644 --- a/packages/common-ts/package.json +++ b/packages/common-ts/package.json @@ -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", diff --git a/packages/common-ts/src/base-service/base-service-v2.ts b/packages/common-ts/src/base-service/base-service-v2.ts index c825acbdd538c..896b62320c5c6 100644 --- a/packages/common-ts/src/base-service/base-service-v2.ts +++ b/packages/common-ts/src/base-service/base-service-v2.ts @@ -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 = { @@ -28,6 +31,7 @@ export type OptionsSpec = { validator: (spec?: Spec) => ValidatorSpec 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 = { [P in keyof Required]: { type: new (configuration: any) => TMetrics[P] @@ -43,6 +52,8 @@ export type MetricsSpec = { } } +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 metricsSpec: MetricsSpec options?: Partial 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 { 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 + /** + * Initialization function for router. + * + * @param router Express router. + */ + protected routes?(router: ExpressRouter): Promise + /** * Main function. Runs repeatedly when run() is called. */ diff --git a/packages/contracts/test/helpers/test-runner/json-test-runner.ts b/packages/contracts/test/helpers/test-runner/json-test-runner.ts index c03c1bf57a4a8..001ec50cb37bd 100644 --- a/packages/contracts/test/helpers/test-runner/json-test-runner.ts +++ b/packages/contracts/test/helpers/test-runner/json-test-runner.ts @@ -1,17 +1,17 @@ /* 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)) + ) } }) } diff --git a/packages/drippie-mon/src/service.ts b/packages/drippie-mon/src/service.ts index 4958483a84efa..b1286ebb45c51 100644 --- a/packages/drippie-mon/src/service.ts +++ b/packages/drippie-mon/src/service.ts @@ -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) { 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 { diff --git a/packages/fault-detector/hardhat.config.ts b/packages/fault-detector/hardhat.config.ts new file mode 100644 index 0000000000000..51ebadf6ceec6 --- /dev/null +++ b/packages/fault-detector/hardhat.config.ts @@ -0,0 +1,13 @@ +import { HardhatUserConfig } from 'hardhat/types' + +// Hardhat plugins +import '@nomiclabs/hardhat-ethers' +import '@nomiclabs/hardhat-waffle' + +const config: HardhatUserConfig = { + mocha: { + timeout: 50000, + }, +} + +export default config diff --git a/packages/fault-detector/package.json b/packages/fault-detector/package.json index 2281f680b30d6..ea5c34c35dc8c 100644 --- a/packages/fault-detector/package.json +++ b/packages/fault-detector/package.json @@ -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" diff --git a/packages/fault-detector/src/helpers.ts b/packages/fault-detector/src/helpers.ts new file mode 100644 index 0000000000000..6f242bc2bacdf --- /dev/null +++ b/packages/fault-detector/src/helpers.ts @@ -0,0 +1,64 @@ +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 => { + 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 => { + 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 + } +} diff --git a/packages/fault-detector/src/index.ts b/packages/fault-detector/src/index.ts index caf7fffa10172..e787cf766e232 100644 --- a/packages/fault-detector/src/index.ts +++ b/packages/fault-detector/src/index.ts @@ -1 +1,2 @@ export * from './service' +export * from './helpers' diff --git a/packages/fault-detector/src/service.ts b/packages/fault-detector/src/service.ts index 402c555271047..496eb1263ee7b 100644 --- a/packages/fault-detector/src/service.ts +++ b/packages/fault-detector/src/service.ts @@ -1,10 +1,15 @@ 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 { constructor(options?: Partial) { 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 { 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 { } async init(): Promise { - 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 { - 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 { 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 { 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 { } } + 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) diff --git a/packages/fault-detector/test/helpers.spec.ts b/packages/fault-detector/test/helpers.spec.ts new file mode 100644 index 0000000000000..efdb120b16fd4 --- /dev/null +++ b/packages/fault-detector/test/helpers.spec.ts @@ -0,0 +1,211 @@ +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 + let FakeCanonicalTransactionChain: FakeContract + 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) + }) + }) + }) +}) diff --git a/packages/fault-detector/test/setup.ts b/packages/fault-detector/test/setup.ts new file mode 100644 index 0000000000000..c20ad0f59006b --- /dev/null +++ b/packages/fault-detector/test/setup.ts @@ -0,0 +1,10 @@ +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 } diff --git a/packages/message-relayer/src/service.ts b/packages/message-relayer/src/service.ts index 2a1e692eca9c9..d5ddd68376029 100644 --- a/packages/message-relayer/src/service.ts +++ b/packages/message-relayer/src/service.ts @@ -37,20 +37,25 @@ export class MessageRelayerService extends BaseServiceV2< > { constructor(options?: Partial) { 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, diff --git a/packages/replica-healthcheck/src/service.ts b/packages/replica-healthcheck/src/service.ts index 69927d9564857..980748e1ab753 100644 --- a/packages/replica-healthcheck/src/service.ts +++ b/packages/replica-healthcheck/src/service.ts @@ -32,17 +32,21 @@ export class HealthcheckService extends BaseServiceV2< > { constructor(options?: Partial) { 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, diff --git a/specs/deposits.md b/specs/deposits.md index 6bdc8efe8bcef..1dd449cfcf61e 100644 --- a/specs/deposits.md +++ b/specs/deposits.md @@ -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`. diff --git a/specs/glossary.md b/specs/glossary.md index 5876b760c3c83..68d6ab6134e5e 100644 --- a/specs/glossary.md +++ b/specs/glossary.md @@ -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 diff --git a/specs/guaranteed-gas-market.md b/specs/guaranteed-gas-market.md new file mode 100644 index 0000000000000..642f194bd5ee0 --- /dev/null +++ b/specs/guaranteed-gas-market.md @@ -0,0 +1,121 @@ +# Guaranteed Gas Fee Market + + + +**Table of Contents** + +- [Gas Stipend](#gas-stipend) +- [Limiting Guaranteed Gas](#limiting-guaranteed-gas) +- [Rationale for burning L1 Gas](#rationale-for-burning-l1-gas) + + + +[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 = +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. diff --git a/teleportr/api/chain_data.go b/teleportr/api/chain_data.go new file mode 100644 index 0000000000000..343c611aade0b --- /dev/null +++ b/teleportr/api/chain_data.go @@ -0,0 +1,179 @@ +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 +} diff --git a/teleportr/api/chain_data_test.go b/teleportr/api/chain_data_test.go new file mode 100644 index 0000000000000..fe24bf81d2f57 --- /dev/null +++ b/teleportr/api/chain_data_test.go @@ -0,0 +1,53 @@ +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) +} diff --git a/teleportr/api/server.go b/teleportr/api/server.go index b10856b7214af..ab5468e5db348 100644 --- a/teleportr/api/server.go +++ b/teleportr/api/server.go @@ -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, } diff --git a/teleportr/drivers/disburser/driver.go b/teleportr/drivers/disburser/driver.go index 31125dea5ff4c..b2fe2262e3692 100644 --- a/teleportr/drivers/disburser/driver.go +++ b/teleportr/drivers/disburser/driver.go @@ -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 diff --git a/teleportr/drivers/disburser/metrics.go b/teleportr/drivers/disburser/metrics.go index 7eb6accc38869..c3eb11346f3ae 100644 --- a/teleportr/drivers/disburser/metrics.go +++ b/teleportr/drivers/disburser/metrics.go @@ -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", + }), } } diff --git a/teleportr/drivers/disburser/retry.go b/teleportr/drivers/disburser/retry.go new file mode 100644 index 0000000000000..781824e9c3f85 --- /dev/null +++ b/teleportr/drivers/disburser/retry.go @@ -0,0 +1,47 @@ +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 +} diff --git a/teleportr/drivers/disburser/retry_test.go b/teleportr/drivers/disburser/retry_test.go new file mode 100644 index 0000000000000..133203d617f12 --- /dev/null +++ b/teleportr/drivers/disburser/retry_test.go @@ -0,0 +1,66 @@ +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"))) +} diff --git a/teleportr/flags/api_flags.go b/teleportr/flags/api_flags.go index 0ccf2e2416477..229dd39228ec2 100644 --- a/teleportr/flags/api_flags.go +++ b/teleportr/flags/api_flags.go @@ -36,6 +36,7 @@ var APIFlags = []cli.Flag{ APIHostnameFlag, APIPortFlag, DisburserWalletAddressFlag, + DisburserAddressFlag, L1EthRpcFlag, L2EthRpcFlag, DepositAddressFlag, diff --git a/teleportr/go.mod b/teleportr/go.mod index ad1ec512dd645..42981332b2e49 100644 --- a/teleportr/go.mod +++ b/teleportr/go.mod @@ -1,6 +1,6 @@ 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 diff --git a/teleportr/go.sum b/teleportr/go.sum index e140bc21be2c0..0d96a22979718 100644 --- a/teleportr/go.sum +++ b/teleportr/go.sum @@ -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= diff --git a/yarn.lock b/yarn.lock index 3ecbb230709a2..0637d1b1e696a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2572,7 +2572,7 @@ safe-buffer "^5.1.1" util.promisify "^1.0.0" -"@nomiclabs/hardhat-ethers@^2.0.0": +"@nomiclabs/hardhat-ethers@^2.0.0", "@nomiclabs/hardhat-ethers@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.6.tgz#1c695263d5b46a375dcda48c248c4fba9dfe2fc2" integrity sha512-q2Cjp20IB48rEn2NPjR1qxsIQBvFVYW9rFRCFq+bC4RUrn1Ljz3g4wM8uSlgIBZYBi2JMXxmOzFqHraczxq4Ng== @@ -2621,7 +2621,7 @@ semver "^6.3.0" undici "^4.14.1" -"@nomiclabs/hardhat-waffle@^2.0.0", "@nomiclabs/hardhat-waffle@^2.0.2": +"@nomiclabs/hardhat-waffle@^2.0.0", "@nomiclabs/hardhat-waffle@^2.0.2", "@nomiclabs/hardhat-waffle@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz#9c538a09c5ed89f68f5fd2dc3f78f16ed1d6e0b1" integrity sha512-049PHSnI1CZq6+XTbrMbMv5NaL7cednTfPenx02k3cEh8wBMLa6ys++dBETJa6JjfwgA9nBhhHQ173LJv6k2Pg== @@ -3257,6 +3257,11 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650" integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg== +"@types/chai@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04" + integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ== + "@types/concat-stream@^1.6.0": version "1.6.1" resolved "https://registry.yarnpkg.com/@types/concat-stream/-/concat-stream-1.6.1.tgz#24bcfc101ecf68e886aaedce60dfd74b632a1b74" @@ -3290,7 +3295,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.12": +"@types/express@^4.17.12", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -3393,6 +3398,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== +"@types/morgan@^1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540" + integrity sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.5.5": version "2.5.10" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" @@ -5057,6 +5069,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcfg@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/bcfg/-/bcfg-0.1.6.tgz#f77a6323bddef14f3886222e7ef8ccc0bc2143ec" @@ -5210,6 +5229,24 @@ body-parser@1.19.0, body-parser@^1.16.0: raw-body "2.4.0" type-is "~1.6.17" +body-parser@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + boundary@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/boundary/-/boundary-1.0.1.tgz#4d67dc2602c0cc16dd9bce7ebf87e948290f5812" @@ -5449,6 +5486,11 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + bytewise-core@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/bytewise-core/-/bytewise-core-1.2.3.tgz#3fb410c7e91558eb1ab22a82834577aa6bd61d42" @@ -6854,6 +6896,11 @@ depcheck@^1.4.3: semver "^7.3.2" yargs "^16.1.0" +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -6877,6 +6924,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -8022,7 +8074,7 @@ ethereum-cryptography@^1.0.3: "@scure/bip32" "1.0.1" "@scure/bip39" "1.0.0" -ethereum-waffle@^3.0.0: +ethereum-waffle@^3.0.0, ethereum-waffle@^3.4.4: version "3.4.4" resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.4.4.tgz#1378b72040697857b7f5e8f473ca8f97a37b5840" integrity sha512-PA9+jCjw4WC3Oc5ocSMBj5sXvueWQeAbvCA+hUlb6oFgwwKyq5ka3bWQ7QZcjzIX+TdFkxP4IbFmoY2D8Dkj9Q== @@ -8470,7 +8522,7 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -express-prom-bundle@^6.3.6: +express-prom-bundle@^6.3.6, express-prom-bundle@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/express-prom-bundle/-/express-prom-bundle-6.4.1.tgz#a688050b9e090f6969825c33143106d3e0e5a70e" integrity sha512-Sg0svLQe/SS5z1tHDTVfZVjNumobiDlXM0jmemt5Dm9K6BX8z9yCwEr93zbko6fNMR4zKav77iPfxUWi6gAjNA== @@ -9945,6 +9997,17 @@ http-errors@1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-https@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" @@ -12578,6 +12641,17 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + mri@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" @@ -13260,6 +13334,13 @@ oboe@2.1.5: dependencies: http-https "^1.0.0" +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -13267,6 +13348,11 @@ on-finished@^2.3.0, on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -14256,11 +14342,25 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.10.5: + version "6.10.5" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" + integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== + dependencies: + side-channel "^1.0.4" + qs@^6.4.0, qs@^6.7.0, qs@^6.9.4: version "6.10.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" @@ -14359,6 +14459,16 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + raw-body@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" @@ -15310,6 +15420,11 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -15885,6 +16000,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -16579,6 +16699,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"