diff --git a/.github/workflows/publish_antithesis_images.yml b/.github/workflows/publish_antithesis_images.yml new file mode 100644 index 0000000000..d941f2fad4 --- /dev/null +++ b/.github/workflows/publish_antithesis_images.yml @@ -0,0 +1,32 @@ +name: Publish Antithesis Images + +on: + workflow_dispatch: + push: + branches: + - master + +env: + REGISTRY: us-central1-docker.pkg.dev + REPOSITORY: molten-verve-216720/avalanche-repository + +jobs: + antithesis: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Login to GAR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: _json_key + password: ${{ secrets.ANTITHESIS_GAR_JSON_KEY }} + + - name: Build and publish images + run: bash -x ./scripts/build_antithesis_images.sh + env: + IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }} + IMAGE_TAG: latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aefe0bfb8c..87f0a8d60b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -197,3 +197,24 @@ jobs: run: scripts/mock.gen.sh - shell: bash run: .github/workflows/check-clean-branch.sh + test_build_antithesis_images: + name: Build Antithesis images + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.min_go_version }} + check-latest: true + - name: Install AvalancheGo Release + shell: bash + run: BASEDIR=/tmp/e2e-test AVALANCHEGO_BUILD_PATH=/tmp/e2e-test/avalanchego ./scripts/install_avalanchego_release.sh + - name: Build Subnet-EVM Plugin Binary + shell: bash + run: ./scripts/build.sh + - name: Check that the antithesis workload is sane + shell: bash + run: go run ./tests/antithesis --avalanchego-path=/tmp/e2e-test/avalanchego/avalanchego --duration=60s + - name: Check antithesis image build + shell: bash + run: bash -x scripts/tests.build_antithesis_images.sh diff --git a/.github/workflows/trigger-antithesis.yml b/.github/workflows/trigger-antithesis.yml new file mode 100644 index 0000000000..1d3128f974 --- /dev/null +++ b/.github/workflows/trigger-antithesis.yml @@ -0,0 +1,40 @@ +name: Trigger Antithesis + +on: + # TODO(marun) Add a schedule + workflow_dispatch: + inputs: + duration: + description: 'The duration (in hours) to run the test for' + default: '0.5' + required: true + type: string + recipients: + description: 'Comma-seperated email addresses to send the test report to' + required: true + type: string + image_tag: + description: 'The image tag to target' + default: latest + required: true + type: string + +jobs: + antithesis: + name: Run Antithesis + runs-on: ubuntu-latest + steps: + - uses: antithesishq/antithesis-trigger-action@v0.5 + with: + notebook_name: avalanche + tenant: avalanche + username: ${{ secrets.ANTITHESIS_USERNAME }} + password: ${{ secrets.ANTITHESIS_PASSWORD }} + github_token: ${{ secrets.ANTITHESIS_GH_PAT }} + config_image: antithesis-subnet-evm-config:${{ github.event.inputs.image_tag || 'latest' }} + images: antithesis-subnet-evm-workload:${{ github.event.inputs.image_tag || 'latest' }};antithesis-subnet-evm-node:${{ github.event.inputs.image_tag || 'latest' }} + email_recipients: ${{ github.event.inputs.recipients || secrets.ANTITHESIS_RECIPIENTS }} + # Duration is in hours + additional_parameters: |- + custom.duration=${{ github.event.inputs.duration || '11.25' }} + custom.workload=subnet-evm diff --git a/.gitignore b/.gitignore index bf7593c1cb..1ba452b444 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,7 @@ cmd/simulator/simulator dist/ # Outputs of `scripts/diff_against.sh` -diffs/ \ No newline at end of file +diffs/ + +# clone used for antithesis image builds +avalanchego/ diff --git a/Dockerfile b/Dockerfile index 72a8076f51..2b08a3d49c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:experimental # ============= Setting up base Stage ================ -# Set required AVALANCHE_VERSION parameter in build image script -ARG AVALANCHE_VERSION +# AVALANCHEGO_NODE_IMAGE needs to identify an existing node image and should include the tag +ARG AVALANCHEGO_NODE_IMAGE # ============= Compilation Stage ================ FROM golang:1.21.12-bullseye AS builder @@ -29,8 +29,8 @@ ARG CURRENT_BRANCH RUN export SUBNET_EVM_COMMIT=$SUBNET_EVM_COMMIT && export CURRENT_BRANCH=$CURRENT_BRANCH && ./scripts/build.sh build/subnet-evm # ============= Cleanup Stage ================ -FROM avaplatform/avalanchego:$AVALANCHE_VERSION AS builtImage +FROM $AVALANCHEGO_NODE_IMAGE AS builtImage # Copy the evm binary into the correct location in the container -ARG VM_ID +ARG VM_ID=srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy COPY --from=builder /build/build/subnet-evm /avalanchego/build/plugins/$VM_ID diff --git a/go.mod b/go.mod index b76bae82b9..8bc5596d73 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.21.12 require ( github.com/VictoriaMetrics/fastcache v1.12.1 - github.com/ava-labs/avalanchego v1.11.11-0.20240813203340-ab83fb41528d + github.com/antithesishq/antithesis-sdk-go v0.3.8 + github.com/ava-labs/avalanchego v1.11.11-0.20240819192939-df91c2f4ab99 github.com/cespare/cp v0.1.0 github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 github.com/davecgh/go-spew v1.1.1 @@ -66,12 +67,16 @@ require ( github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 // indirect github.com/cockroachdb/redact v1.1.3 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/compose-spec/compose-go v1.20.2 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/distribution/reference v0.5.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-logr/logr v1.4.1 // indirect @@ -97,12 +102,14 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pires/go-proxyproto v0.6.2 // indirect @@ -114,6 +121,7 @@ require ( github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect diff --git a/go.sum b/go.sum index 37eaffff74..31b6d72788 100644 --- a/go.sum +++ b/go.sum @@ -55,9 +55,11 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/antithesishq/antithesis-sdk-go v0.3.8 h1:OvGoHxIcOXFJLyn9IJQ5DzByZ3YVAWNBc394ObzDRb8= +github.com/antithesishq/antithesis-sdk-go v0.3.8/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/ava-labs/avalanchego v1.11.11-0.20240813203340-ab83fb41528d h1:LyrKJL9avIIxBY3uTcS2dFtUMBFmI2QpAgG6qYTdA6s= -github.com/ava-labs/avalanchego v1.11.11-0.20240813203340-ab83fb41528d/go.mod h1:UkyrRDXK2E15Lq2abyae2Pt+JsWvgsg1pe0/AtoMyAM= +github.com/ava-labs/avalanchego v1.11.11-0.20240819192939-df91c2f4ab99 h1:fPWpINk7O1W4w5thqa8CGMRJ/kvycHvehBEiWwMjezI= +github.com/ava-labs/avalanchego v1.11.11-0.20240819192939-df91c2f4ab99/go.mod h1:UkyrRDXK2E15Lq2abyae2Pt+JsWvgsg1pe0/AtoMyAM= github.com/ava-labs/coreth v0.13.8-fixed-genesis-upgrade.0.20240813194342-7635a96aa180 h1:6aIHp7wbyGVYdhHVQUbG7BEcbCMEQ5SYopPPJyipyvk= github.com/ava-labs/coreth v0.13.8-fixed-genesis-upgrade.0.20240813194342-7635a96aa180/go.mod h1:/wNBVq7J7wlC2Kbov7kk6LV5xZvau7VF9zwTVOeyAjY= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -121,6 +123,8 @@ github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= +github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= @@ -150,9 +154,15 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2U github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5RVoRccG8a5DhOdWdQ4zN62zzo= github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= @@ -410,6 +420,8 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -462,6 +474,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -508,6 +522,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -797,6 +813,7 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1023,6 +1040,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/scripts/build_antithesis_images.sh b/scripts/build_antithesis_images.sh new file mode 100755 index 0000000000..e90cfaee8e --- /dev/null +++ b/scripts/build_antithesis_images.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Builds docker images for antithesis testing. + +# e.g., +# ./scripts/build_antithesis_images.sh # Build local images +# IMAGE_PREFIX=/ IMAGE_TAG=latest ./scripts/build_antithesis_images.sh # Specify a prefix to enable image push and use a specific tag + +# Directory above this script +SUBNET_EVM_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) + +# Allow configuring the clone path to point to a shared and/or existing clone of the avalanchego repo +AVALANCHEGO_CLONE_PATH="${AVALANCHEGO_CLONE_PATH:-${SUBNET_EVM_PATH}/avalanchego}" + +# Assume it's necessary to build the avalanchego node image from source +# TODO(marun) Support use of a released node image if using a release version of avalanchego + +source "${SUBNET_EVM_PATH}"/scripts/versions.sh +source "${SUBNET_EVM_PATH}"/scripts/constants.sh + +echo "checking out target avalanchego version ${AVALANCHE_VERSION}" +if [[ -d "${AVALANCHEGO_CLONE_PATH}" ]]; then + echo "updating existing clone" + cd "${AVALANCHEGO_CLONE_PATH}" + git fetch +else + echo "creating new clone" + git clone https://github.com/ava-labs/avalanchego.git "${AVALANCHEGO_CLONE_PATH}" + cd "${AVALANCHEGO_CLONE_PATH}" +fi +# Branch will be reset to $AVALANCHE_VERSION if it already exists +git checkout -B "test-${AVALANCHE_VERSION}" "${AVALANCHE_VERSION}" +cd "${SUBNET_EVM_PATH}" + +AVALANCHEGO_COMMIT_HASH="$(git --git-dir="${AVALANCHEGO_CLONE_PATH}/.git" rev-parse HEAD)" +AVALANCHEGO_IMAGE_TAG="${AVALANCHEGO_COMMIT_HASH::8}" + +# Build avalanchego node image in the clone path +pushd "${AVALANCHEGO_CLONE_PATH}" > /dev/null + NODE_ONLY=1 TEST_SETUP=avalanchego IMAGE_TAG="${AVALANCHEGO_IMAGE_TAG}" bash -x "${AVALANCHEGO_CLONE_PATH}"/scripts/build_antithesis_images.sh +popd > /dev/null + +# Specifying an image prefix will ensure the image is pushed after build +IMAGE_PREFIX="${IMAGE_PREFIX:-}" + +IMAGE_TAG="${IMAGE_TAG:-}" +if [[ -z "${IMAGE_TAG}" ]]; then + # Default to tagging with the commit hash + source "${SUBNET_EVM_PATH}"/scripts/constants.sh + IMAGE_TAG="${SUBNET_EVM_COMMIT::8}" +fi + +# The dockerfiles don't specify the golang version to minimize the changes required to bump +# the version. Instead, the golang version is provided as an argument. +GO_VERSION="$(go list -m -f '{{.GoVersion}}')" + +# Import common functions used to build images for antithesis test setups +# shellcheck source=/dev/null +source "${AVALANCHEGO_CLONE_PATH}"/scripts/lib_build_antithesis_images.sh + +build_antithesis_builder_image "${GO_VERSION}" "antithesis-subnet-evm-builder:${IMAGE_TAG}" "${AVALANCHEGO_CLONE_PATH}" "${SUBNET_EVM_PATH}" + +# Ensure avalanchego and subnet-evm binaries are available to create an initial db state that includes subnets. +"${AVALANCHEGO_CLONE_PATH}"/scripts/build.sh +"${SUBNET_EVM_PATH}"/scripts/build.sh + +echo "Generating compose configuration" +gen_antithesis_compose_config "${IMAGE_TAG}" "${SUBNET_EVM_PATH}/tests/antithesis/gencomposeconfig" \ + "${SUBNET_EVM_PATH}/build/antithesis" \ + "AVALANCHEGO_PATH=${AVALANCHEGO_CLONE_PATH}/build/avalanchego \ + AVALANCHEGO_PLUGIN_DIR=${DEFAULT_PLUGIN_DIR}" + +build_antithesis_images "${GO_VERSION}" "${IMAGE_PREFIX}" "antithesis-subnet-evm" "${IMAGE_TAG}" \ + "${AVALANCHEGO_IMAGE_TAG}" "${SUBNET_EVM_PATH}/tests/antithesis/Dockerfile" \ + "${SUBNET_EVM_PATH}/Dockerfile" "${SUBNET_EVM_PATH}" diff --git a/scripts/build_antithesis_workload.sh b/scripts/build_antithesis_workload.sh new file mode 100755 index 0000000000..942e91c0ac --- /dev/null +++ b/scripts/build_antithesis_workload.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Directory above this script +SUBNET_EVM_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) +# Load the constants +source "$SUBNET_EVM_PATH"/scripts/constants.sh + +echo "Building Workload..." +go build -o "$SUBNET_EVM_PATH/build/workload" "$SUBNET_EVM_PATH/tests/antithesis/"*.go diff --git a/scripts/build_docker_image.sh b/scripts/build_docker_image.sh index 1fe8efaefa..e8aae432cf 100755 --- a/scripts/build_docker_image.sh +++ b/scripts/build_docker_image.sh @@ -20,10 +20,13 @@ if [[ "${VM_ID}" != "${DEFAULT_VM_ID}" ]]; then DOCKERHUB_TAG="${VM_ID}-${DOCKERHUB_TAG}" fi +# Default to the release image. Will need to be overridden when testing against unreleased versions. +AVALANCHE_NODE_IMAGE=${AVALANCHE_NODE_IMAGE:-"avaplatform/avalanchego:${AVALANCHE_VERSION}"} + echo "Building Docker Image: $DOCKERHUB_REPO:$BUILD_IMAGE_ID based of AvalancheGo@$AVALANCHE_VERSION" docker build -t "$DOCKERHUB_REPO:$BUILD_IMAGE_ID" -t "$DOCKERHUB_REPO:${DOCKERHUB_TAG}" \ "$SUBNET_EVM_PATH" -f "$SUBNET_EVM_PATH/Dockerfile" \ - --build-arg AVALANCHE_VERSION="$AVALANCHE_VERSION" \ + --build-arg AVALANCHE_NODE_IMAGE="$AVALANCHE_NODE_IMAGE" \ --build-arg SUBNET_EVM_COMMIT="$SUBNET_EVM_COMMIT" \ --build-arg CURRENT_BRANCH="$CURRENT_BRANCH" \ --build-arg VM_ID="$VM_ID" \ No newline at end of file diff --git a/scripts/install_avalanchego_release.sh b/scripts/install_avalanchego_release.sh index 117dc07876..83426e7036 100755 --- a/scripts/install_avalanchego_release.sh +++ b/scripts/install_avalanchego_release.sh @@ -92,7 +92,7 @@ else if [[ $CHECKOUT_STATUS -ne 0 ]]; then echo - echo "'${VERSION}' is not a valid release tag, commit hash, or branch name" + echo "'${AVALANCHE_VERSION}' is not a valid release tag, commit hash, or branch name" exit 1 fi fi diff --git a/scripts/shellcheck.sh b/scripts/shellcheck.sh index 51675db616..f57b853362 100755 --- a/scripts/shellcheck.sh +++ b/scripts/shellcheck.sh @@ -5,9 +5,11 @@ set -euo pipefail VERSION="v0.9.0" # Scripts that are sourced from upstream and not maintained in this repo will not be shellchecked. +# Also ignore the local avalanchego clone. IGNORED_FILES=" cmd/evm/transition-test.sh metrics/validate.sh + avalanchego/* " function get_version { diff --git a/scripts/tests.build_antithesis_images.sh b/scripts/tests.build_antithesis_images.sh new file mode 100755 index 0000000000..658806524d --- /dev/null +++ b/scripts/tests.build_antithesis_images.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Validates the construction of the antithesis images by: +# +# 1. Building the antithesis test images +# 2. Extracting the docker compose configuration from the config image +# 3. Running the workload and its target network without error for a minute +# 4. Stopping the workload and its target network +# + +# e.g., +# ./scripts/tests.build_antithesis_images.sh # Test build of antithesis images +# DEBUG=1 ./scripts/tests.build_antithesis_images.sh # Retain the temporary compose path for troubleshooting + +SUBNET_EVM_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) + +# Discover the default tag that will be used for the image +source "${SUBNET_EVM_PATH}"/scripts/constants.sh +export IMAGE_TAG="${SUBNET_EVM_COMMIT::8}" + +# Build the images +bash -x "${SUBNET_EVM_PATH}"/scripts/build_antithesis_images.sh + +# Test the images +AVALANCHEGO_CLONE_PATH="${AVALANCHEGO_CLONE_PATH:-${SUBNET_EVM_PATH}/avalanchego}" +export IMAGE_NAME="antithesis-subnet-evm-config" +export DEBUG="${DEBUG:-}" +set -x +# shellcheck source=/dev/null +. "${AVALANCHEGO_CLONE_PATH}"/scripts/lib_test_antithesis_images.sh diff --git a/scripts/versions.sh b/scripts/versions.sh index 432a1746e7..d6f28fb862 100644 --- a/scripts/versions.sh +++ b/scripts/versions.sh @@ -4,7 +4,7 @@ # shellcheck disable=SC2034 # Don't export them as they're used in the context of other calls -AVALANCHE_VERSION=${AVALANCHE_VERSION:-'ab83fb41'} +AVALANCHE_VERSION=${AVALANCHE_VERSION:-'df91c2f4a'} GINKGO_VERSION=${GINKGO_VERSION:-'v2.2.0'} # This won't be used, but it's here to make code syncs easier diff --git a/tests/antithesis/Dockerfile.config b/tests/antithesis/Dockerfile.config new file mode 100644 index 0000000000..6cce3bb374 --- /dev/null +++ b/tests/antithesis/Dockerfile.config @@ -0,0 +1,6 @@ +FROM scratch AS execution + +# Copy config artifacts from the build path. For simplicity, artifacts +# are built outside of the docker image. +COPY ./build/antithesis/docker-compose.yml / +COPY ./build/antithesis/volumes /volumes diff --git a/tests/antithesis/Dockerfile.node b/tests/antithesis/Dockerfile.node new file mode 100644 index 0000000000..67538fefad --- /dev/null +++ b/tests/antithesis/Dockerfile.node @@ -0,0 +1,32 @@ +# BUILDER_IMAGE_TAG should identify the builder image +ARG BUILDER_IMAGE_TAG + +# AVALANCHEGO_NODE_IMAGE needs to identify an existing avalanchego node image and should include the tag +ARG AVALANCHEGO_NODE_IMAGE + +# ============= Compilation Stage ================ +FROM antithesis-subnet-evm-builder:$BUILDER_IMAGE_TAG AS builder + +# The builder workdir will vary between instrumented and non-instrumented builders +ARG BUILDER_WORKDIR + +WORKDIR $BUILDER_WORKDIR + +# Build the VM +RUN ./scripts/build.sh /build/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy + +# ============= Cleanup Stage ================ +FROM $AVALANCHEGO_NODE_IMAGE AS execution + +# Copy identifying information into the container. This will replace +# the avalanchego commit hash in the base image. +COPY --from=builder /build/commit_hash.txt /avalanchego/build/commit_hash.txt + +# Copy the antithesis dependencies into the container +COPY --from=builder /instrumented/symbols /symbols + +# Copy the executable into the container +COPY --from=builder /build/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy\ + /avalanchego/build/plugins/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy + +# The node image's entrypoint will be reused. diff --git a/tests/antithesis/Dockerfile.workload b/tests/antithesis/Dockerfile.workload new file mode 100644 index 0000000000..3b6a9a2e11 --- /dev/null +++ b/tests/antithesis/Dockerfile.workload @@ -0,0 +1,28 @@ +# BUILDER_IMAGE_TAG should identify the builder image +ARG BUILDER_IMAGE_TAG + +# AVALANCHEGO_NODE_IMAGE needs to identify an existing avalanchego node image and should include the tag +ARG AVALANCHEGO_NODE_IMAGE + +# ============= Compilation Stage ================ +FROM antithesis-subnet-evm-builder:$BUILDER_IMAGE_TAG AS builder + +# The builder workdir will vary between instrumented and non-instrumented builders +ARG BUILDER_WORKDIR + +WORKDIR $BUILDER_WORKDIR + +# Build the workload +RUN ./scripts/build_antithesis_workload.sh + +# ============= Cleanup Stage ================ +# Base the workflow on the node image to support bootstrap testing +FROM $AVALANCHEGO_NODE_IMAGE AS execution + +# The builder workdir will vary between instrumented and non-instrumented builders +ARG BUILDER_WORKDIR + +# Copy the executable into the container +COPY --from=builder $BUILDER_WORKDIR/build/workload ./workload + +CMD [ "./workload" ] diff --git a/tests/antithesis/README.md b/tests/antithesis/README.md new file mode 100644 index 0000000000..0924b9f63d --- /dev/null +++ b/tests/antithesis/README.md @@ -0,0 +1,11 @@ +# Antithesis Testing + +This package supports testing with +[Antithesis](https://antithesis.com/docs/introduction/introduction.html), +a SaaS offering that enables deployment of distributed systems (such +as Avalanche) to a deterministic and simulated environment that +enables discovery and reproduction of anomalous behavior. + +See avalanchego's +[documentation](https://github.com/ava-labs/avalanchego/blob/master/tests/antithesis/README.md) +for more details. diff --git a/tests/antithesis/gencomposeconfig/main.go b/tests/antithesis/gencomposeconfig/main.go new file mode 100644 index 0000000000..40cbf0037d --- /dev/null +++ b/tests/antithesis/gencomposeconfig/main.go @@ -0,0 +1,41 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "log" + "os" + "path/filepath" + + "github.com/ava-labs/avalanchego/tests/antithesis" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + + "github.com/ava-labs/subnet-evm/tests/utils" +) + +const baseImageName = "antithesis-subnet-evm" + +// Creates docker-compose.yml and its associated volumes in the target path. +func main() { + // Assume the working directory is the root of the repository + cwd, err := os.Getwd() + if err != nil { + log.Fatalf("failed to get current working directory: %s", err) + } + + genesisPath := filepath.Join(cwd, "tests/load/genesis/genesis.json") + + // Create a network with a subnet-evm subnet + network := tmpnet.LocalNetworkOrPanic() + network.Subnets = []*tmpnet.Subnet{ + utils.NewTmpnetSubnet("subnet-evm", genesisPath, utils.DefaultChainConfig, network.Nodes...), + } + + // Path to the plugin dir on subnet-evm node images that will be run by docker compose. + runtimePluginDir := "/avalanchego/build/plugins" + + if err := antithesis.GenerateComposeConfig(network, baseImageName, runtimePluginDir); err != nil { + log.Fatalf("failed to generate compose config: %v", err) + } +} diff --git a/tests/antithesis/main.go b/tests/antithesis/main.go new file mode 100644 index 0000000000..a58893a1be --- /dev/null +++ b/tests/antithesis/main.go @@ -0,0 +1,204 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "fmt" + "log" + "math/big" + "path/filepath" + "time" + + "github.com/antithesishq/antithesis-sdk-go/assert" + "github.com/antithesishq/antithesis-sdk-go/lifecycle" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests/antithesis" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + + "github.com/ava-labs/subnet-evm/accounts/abi/bind" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/ethclient" + "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/tests" + "github.com/ava-labs/subnet-evm/tests/utils" + + ago_tests "github.com/ava-labs/avalanchego/tests" + timerpkg "github.com/ava-labs/avalanchego/utils/timer" +) + +const NumKeys = 5 + +func main() { + tc := ago_tests.NewTestContext() + defer tc.Cleanup() + require := require.New(tc) + + c := antithesis.NewConfigWithSubnets( + tc, + // TODO(marun) Centralize network configuration for all test types + utils.NewTmpnetNetwork( + "antithesis-subnet-evm", + nil, + tmpnet.FlagsMap{}, + ), + func(nodes ...*tmpnet.Node) []*tmpnet.Subnet { + repoRootPath := tests.GetRepoRootPath("tests/antithesis") + genesisPath := filepath.Join(repoRootPath, "tests/load/genesis/genesis.json") + return []*tmpnet.Subnet{ + utils.NewTmpnetSubnet("subnet-evm", genesisPath, utils.DefaultChainConfig, nodes...), + } + }, + ) + ctx := ago_tests.DefaultNotifyContext(c.Duration, tc.DeferCleanup) + + require.Len(c.ChainIDs, 1) + log.Printf("CHAIN IDS: %v", c.ChainIDs) + chainID, err := ids.FromString(c.ChainIDs[0]) + require.NoError(err, "failed to parse chainID") + + genesisClient, err := ethclient.Dial(getChainURI(c.URIs[0], chainID.String())) + require.NoError(err, "failed to dial chain") + genesisKey := tmpnet.HardhatKey.ToECDSA() + genesisWorkload := &workload{ + id: 0, + client: genesisClient, + key: genesisKey, + uris: c.URIs, + } + + workloads := make([]*workload, NumKeys) + workloads[0] = genesisWorkload + + initialAmount := uint64(1_000_000_000_000_000) + for i := 1; i < NumKeys; i++ { + key, err := crypto.ToECDSA(crypto.Keccak256([]byte{uint8(i)})) + require.NoError(err, "failed to generate key") + + require.NoError(transferFunds(ctx, genesisClient, genesisKey, crypto.PubkeyToAddress(key.PublicKey), initialAmount)) + + client, err := ethclient.Dial(getChainURI(c.URIs[i%len(c.URIs)], chainID.String())) + require.NoError(err, "failed to dial chain") + + workloads[i] = &workload{ + id: i, + client: client, + key: key, + uris: c.URIs, + } + } + + lifecycle.SetupComplete(map[string]any{ + "msg": "initialized workers", + "numWorkers": NumKeys, + }) + + for _, w := range workloads[1:] { + go w.run(ctx) + } + genesisWorkload.run(ctx) +} + +type workload struct { + id int + client ethclient.Client + key *ecdsa.PrivateKey + uris []string +} + +func (w *workload) run(ctx context.Context) { + timer := timerpkg.StoppedTimer() + + tc := ago_tests.NewTestContext() + defer tc.Cleanup() + require := require.New(tc) + + balance, err := w.client.BalanceAt(ctx, crypto.PubkeyToAddress(w.key.PublicKey), nil) + require.NoError(err, "failed to fetch balance") + assert.Reachable("worker starting", map[string]any{ + "worker": w.id, + "balance": balance, + }) + + // TODO(marun) What should this value be? + txAmount := uint64(10000) + for { + // TODO(marun) Exercise a wider variety of transactions + recipientEthAddress := crypto.PubkeyToAddress(w.key.PublicKey) + err := transferFunds(ctx, w.client, w.key, recipientEthAddress, txAmount) + if err != nil { + // Log the error and continue since the problem may be + // transient. require.NoError is only for errors that should stop + // execution. + log.Printf("failed to transfer funds: %s", err) + } + + val, err := rand.Int(rand.Reader, big.NewInt(int64(time.Second))) + require.NoError(err, "failed to read randomness") + + timer.Reset(time.Duration(val.Int64())) + select { + case <-ctx.Done(): + return + case <-timer.C: + } + } +} + +func getChainURI(nodeURI string, blockchainID string) string { + return fmt.Sprintf("%s/ext/bc/%s/rpc", nodeURI, blockchainID) +} + +func transferFunds(ctx context.Context, client ethclient.Client, key *ecdsa.PrivateKey, recipientAddress common.Address, txAmount uint64) error { + chainID, err := client.ChainID(ctx) + if err != nil { + return fmt.Errorf("failed to fetch chainID: %w", err) + } + acceptedNonce, err := client.AcceptedNonceAt(ctx, crypto.PubkeyToAddress(key.PublicKey)) + if err != nil { + return fmt.Errorf("failed to fetch accepted nonce: %w", err) + } + gasTipCap, err := client.SuggestGasTipCap(ctx) + if err != nil { + return fmt.Errorf("failed to fetch suggested gas tip: %w", err) + } + gasFeeCap, err := client.EstimateBaseFee(ctx) + if err != nil { + return fmt.Errorf("failed to fetch estimated base fee: %w", err) + } + signer := types.LatestSignerForChainID(chainID) + + tx, err := types.SignNewTx(key, signer, &types.DynamicFeeTx{ + ChainID: chainID, + Nonce: acceptedNonce, + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Gas: params.TxGas, + To: &recipientAddress, + Value: big.NewInt(int64(txAmount)), + }) + if err != nil { + return fmt.Errorf("failed to format transaction: %w", err) + } + + log.Printf("sending transaction with ID %s and nonce %d\n", tx.Hash(), acceptedNonce) + err = client.SendTransaction(ctx, tx) + if err != nil { + return fmt.Errorf("failed to send transaction: %w", err) + } + + log.Printf("waiting for acceptance of transaction with ID %s\n", tx.Hash()) + if _, err := bind.WaitMined(ctx, client, tx); err != nil { + return fmt.Errorf("failed to wait for receipt: %v", err) + } + log.Printf("confirmed acceptance of transaction with ID %s\n", tx.Hash()) + + return nil +}