diff --git a/.dockerignore b/.dockerignore index 51424900e8..2d5303a3be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,6 +36,7 @@ arbitrator/tools/wasmer/target/ arbitrator/tools/wasm-tools/ arbitrator/tools/pricers/ arbitrator/tools/module_roots/ +arbitrator/tools/stylus_benchmark arbitrator/langs/rust/target/ arbitrator/langs/bf/target/ diff --git a/.github/buildspec.yml b/.github/buildspec.yml new file mode 100644 index 0000000000..9b6503bb5d --- /dev/null +++ b/.github/buildspec.yml @@ -0,0 +1,36 @@ +version: 0.2 + +phases: + pre_build: + commands: + - git submodule update --init + - echo Logging in to Dockerhub.... + - docker login --username $DOCKERHUB_USERNAME --password $DOCKERHUB_PASSWORD + - aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin $REPOSITORY_URI + - COMMIT_HASH=$(git rev-parse --short=7 HEAD || echo "latest") + - VERSION_TAG=$(git tag --points-at HEAD | sed '/-/!s/$/_/' | sort -rV | sed 's/_$//' | head -n 1 | grep ^ || git show -s --pretty=%D | sed 's/, /\n/g' | grep -v '^origin/' |grep -v '^grafted\|HEAD\|master\|main$' || echo "dev") + - NITRO_VERSION=${VERSION_TAG}-${COMMIT_HASH} + - IMAGE_TAG=${NITRO_VERSION} + - NITRO_DATETIME=$(git show -s --date=iso-strict --format=%cd) + - NITRO_MODIFIED="false" + - echo ${NITRO_VERSION} > ./.nitro-tag.txt + build: + commands: + - echo Build started on `date` + - echo Building the Docker image ${NITRO_VERSION}... + - DOCKER_BUILDKIT=1 docker build . -t nitro-node-slim --target nitro-node-slim --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - DOCKER_BUILDKIT=1 docker build . -t nitro-node --target nitro-node --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - DOCKER_BUILDKIT=1 docker build . -t nitro-node-dev --target nitro-node-dev --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - DOCKER_BUILDKIT=1 docker build . -t nitro-node-validator --target nitro-node-validator --build-arg version=$NITRO_VERSION --build-arg datetime=$NITRO_DATETIME --build-arg modified=$NITRO_MODIFIED + - docker tag nitro-node:latest $REPOSITORY_URI:$IMAGE_TAG-$ARCH_TAG + - docker tag nitro-node-slim:latest $REPOSITORY_URI:$IMAGE_TAG-slim-$ARCH_TAG + - docker tag nitro-node-dev:latest $REPOSITORY_URI:$IMAGE_TAG-dev-$ARCH_TAG + - docker tag nitro-node-validator:latest $REPOSITORY_URI:$IMAGE_TAG-validator-$ARCH_TAG + post_build: + commands: + - echo Build completed on `date` + - echo pushing to repo + - docker push $REPOSITORY_URI:$IMAGE_TAG-$ARCH_TAG + - docker push $REPOSITORY_URI:$IMAGE_TAG-slim-$ARCH_TAG + - docker push $REPOSITORY_URI:$IMAGE_TAG-dev-$ARCH_TAG + - docker push $REPOSITORY_URI:$IMAGE_TAG-validator-$ARCH_TAG diff --git a/.github/workflows/arbitrator-ci.yml b/.github/workflows/arbitrator-ci.yml index 392eb876c0..dd58a30571 100644 --- a/.github/workflows/arbitrator-ci.yml +++ b/.github/workflows/arbitrator-ci.yml @@ -50,15 +50,13 @@ jobs: - name: Install go uses: actions/setup-go@v4 with: - go-version: 1.21.x + go-version: 1.23.x - name: Install custom go-ethereum run: | cd /tmp - git clone --branch v1.13.8 --depth 1 https://github.com/ethereum/go-ethereum.git + git clone --branch v1.14.11 --depth 1 https://github.com/ethereum/go-ethereum.git cd go-ethereum - # Enable KZG point evaluation precompile early - sed -i 's#var PrecompiledContractsBerlin = map\[common.Address\]PrecompiledContract{#\0 common.BytesToAddress([]byte{0x0a}): \&kzgPointEvaluation{},#g' core/vm/contracts.go go build -o /usr/local/bin/geth ./cmd/geth - name: Setup nodejs @@ -173,6 +171,9 @@ jobs: - name: Rustfmt - langs/rust run: cargo fmt --all --manifest-path arbitrator/langs/rust/Cargo.toml -- --check + - name: Rustfmt - tools/stylus_benchmark + run: cargo fmt --all --manifest-path arbitrator/tools/stylus_benchmark/Cargo.toml -- --check + - name: Make proofs from test cases run: make -j test-gen-proofs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acd6295b7c..1eda1d9b7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: redis: image: redis ports: - - 6379:6379 + - 6379:6379 strategy: fail-fast: false @@ -46,7 +46,7 @@ jobs: - name: Install go uses: actions/setup-go@v4 with: - go-version: 1.21.x + go-version: 1.23.x - name: Install wasm-ld run: | @@ -87,12 +87,12 @@ jobs: uses: actions/cache@v3 with: path: | - ~/.cargo/registry/ - ~/.cargo/git/ + ~/.cargo/ arbitrator/target/ arbitrator/wasm-libraries/target/ - arbitrator/wasm-libraries/soft-float/SoftFloat/build + arbitrator/wasm-libraries/soft-float/ target/etc/initial-machine-cache/ + /home/runner/.rustup/toolchains/ key: ${{ runner.os }}-cargo-${{ steps.install-rust.outputs.rustc_hash }}-min-${{ hashFiles('arbitrator/Cargo.lock') }}-${{ matrix.test-mode }} restore-keys: ${{ runner.os }}-cargo-${{ steps.install-rust.outputs.rustc_hash }}- @@ -145,89 +145,62 @@ jobs: env: TEST_STATE_SCHEME: path run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/... -timeout 20m -tags=cionly > >(stdbuf -oL tee -a full.log | grep -vE "INFO|seal"); then - exit 1 - fi - done + echo "Running tests with Path Scheme" >> full.log + ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags cionly --timeout 20m --cover - name: run tests without race detection and hash state scheme if: matrix.test-mode == 'defaults' env: TEST_STATE_SCHEME: hash run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -timeout 20m -tags=cionly; then - exit 1 - fi - done - - - name: run tests with race detection and path state scheme - if: matrix.test-mode == 'race' - env: - TEST_STATE_SCHEME: path - run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -race -timeout 30m > >(stdbuf -oL tee -a full.log | grep -vE "INFO|seal"); then - exit 1 - fi - done + echo "Running tests with Hash Scheme" >> full.log + ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags cionly --timeout 20m - name: run tests with race detection and hash state scheme if: matrix.test-mode == 'race' env: TEST_STATE_SCHEME: hash run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -race -timeout 30m; then - exit 1 - fi - done + echo "Running tests with Hash Scheme" >> full.log + ${{ github.workspace }}/.github/workflows/gotestsum.sh --race --timeout 30m - name: run redis tests if: matrix.test-mode == 'defaults' - run: TEST_REDIS=redis://localhost:6379/0 gotestsum --format short-verbose -- -p 1 -run TestRedis ./arbnode/... ./system_tests/... -coverprofile=coverage-redis.txt -covermode=atomic -coverpkg=./... + run: | + echo "Running redis tests" >> full.log + TEST_REDIS=redis://localhost:6379/0 gotestsum --format short-verbose -- -p 1 -run TestRedis ./arbnode/... ./system_tests/... -coverprofile=coverage-redis.txt -covermode=atomic -coverpkg=./... + + - name: create block input json file + if: matrix.test-mode == 'defaults' + run: | + gotestsum --format short-verbose -- -run TestProgramStorage$ ./system_tests/... --count 1 --recordBlockInputs.WithBaseDir="${{ github.workspace }}/target" --recordBlockInputs.WithTimestampDirEnabled=false --recordBlockInputs.WithBlockIdInFileNameEnabled=false + + - name: run arbitrator prover on block input json + if: matrix.test-mode == 'defaults' + run: | + make build-prover-bin + target/bin/prover target/machines/latest/machine.wavm.br -b --json-inputs="${{ github.workspace }}/target/TestProgramStorage/block_inputs.json" + + - name: run jit prover on block input json + if: matrix.test-mode == 'defaults' + run: | + make build-jit + if [ -n "$(target/bin/jit --binary target/machines/latest/replay.wasm --cranelift --json-inputs='${{ github.workspace }}/target/TestProgramStorage/block_inputs.json')" ]; then + echo "Error: Command produced output." + exit 1 + fi - name: run challenge tests if: matrix.test-mode == 'challenge' - run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/... -tags=challengetest -run=TestChallenge > >(stdbuf -oL tee -a full.log | grep -vE "INFO|seal"); then - exit 1 - fi - done + run: ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags challengetest --run TestChallenge --timeout 60m --cover - name: run stylus tests if: matrix.test-mode == 'stylus' - run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -timeout 60m -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/... -tags=stylustest -run="TestProgramArbitrator" > >(stdbuf -oL tee -a full.log | grep -vE "INFO|seal"); then - exit 1 - fi - done + run: ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags stylustest --run TestProgramArbitrator --timeout 60m --cover - name: run long stylus tests if: matrix.test-mode == 'long' - run: | - packages=`go list ./...` - for package in $packages; do - echo running tests for $package - if ! stdbuf -oL gotestsum --format short-verbose --packages="$package" --rerun-fails=2 --no-color=false -- -timeout 60m -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/... -tags=stylustest -run="TestProgramLong" > >(stdbuf -oL tee -a full.log | grep -vE "INFO|seal"); then - exit 1 - fi - done + run: ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags stylustest --run TestProgramLong --timeout 60m --cover - name: Archive detailed run log uses: actions/upload-artifact@v3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1cde8f06b9..26447947d4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -73,7 +73,7 @@ jobs: - name: Install go uses: actions/setup-go@v4 with: - go-version: 1.21.x + go-version: 1.23.x - name: Install rust stable uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/gotestsum.sh b/.github/workflows/gotestsum.sh new file mode 100755 index 0000000000..ed631847b7 --- /dev/null +++ b/.github/workflows/gotestsum.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +check_missing_value() { + if [[ $1 -eq 0 || $2 == -* ]]; then + echo "missing $3 argument value" + exit 1 + fi +} + +timeout="" +tags="" +run="" +race=false +cover=false +while [[ $# -gt 0 ]]; do + case $1 in + --timeout) + shift + check_missing_value $# "$1" "--timeout" + timeout=$1 + shift + ;; + --tags) + shift + check_missing_value $# "$1" "--tags" + tags=$1 + shift + ;; + --run) + shift + check_missing_value $# "$1" "--run" + run=$1 + shift + ;; + --race) + race=true + shift + ;; + --cover) + cover=true + shift + ;; + *) + echo "Invalid argument: $1" + exit 1 + ;; + esac +done + +packages=$(go list ./...) +for package in $packages; do + cmd="stdbuf -oL gotestsum --format short-verbose --packages=\"$package\" --rerun-fails=2 --no-color=false --" + + if [ "$timeout" != "" ]; then + cmd="$cmd -timeout $timeout" + fi + + if [ "$tags" != "" ]; then + cmd="$cmd -tags=$tags" + fi + + if [ "$run" != "" ]; then + cmd="$cmd -run=$run" + fi + + if [ "$race" == true ]; then + cmd="$cmd -race" + fi + + if [ "$cover" == true ]; then + cmd="$cmd -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/..." + fi + + cmd="$cmd > >(stdbuf -oL tee -a full.log | grep -vE \"INFO|seal\")" + + echo "" + echo running tests for "$package" + echo "$cmd" + + if ! eval "$cmd"; then + exit 1 + fi +done diff --git a/.github/workflows/shellcheck-ci.yml b/.github/workflows/shellcheck-ci.yml new file mode 100644 index 0000000000..d1c7b58580 --- /dev/null +++ b/.github/workflows/shellcheck-ci.yml @@ -0,0 +1,30 @@ +name: ShellCheck CI +run-name: ShellCheck CI triggered from @${{ github.actor }} of ${{ github.head_ref }} + +on: + workflow_dispatch: + merge_group: + pull_request: + push: + branches: + - master + +jobs: + shellcheck: + name: Run ShellCheck + runs-on: ubuntu-8 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + with: + ignore_paths: >- + ./fastcache/** + ./contracts/** + ./safe-smart-account/** + ./go-ethereum/** + ./nitro-testnode/** + ./brotli/** + ./arbitrator/** diff --git a/.github/workflows/submodule-pin-check.sh b/.github/workflows/submodule-pin-check.sh deleted file mode 100755 index aecb287ce1..0000000000 --- a/.github/workflows/submodule-pin-check.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -declare -Ar exceptions=( - [contracts]=origin/develop - [nitro-testnode]=origin/master - - #TODO Rachel to check these are the intended branches. - [arbitrator/langs/c]=origin/vm-storage-cache - [arbitrator/tools/wasmer]=origin/adopt-v4.2.8 -) - -divergent=0 -for mod in `git submodule --quiet foreach 'echo $name'`; do - branch=origin/HEAD - if [[ -v exceptions[$mod] ]]; then - branch=${exceptions[$mod]} - fi - - if ! git -C $mod merge-base --is-ancestor HEAD $branch; then - echo $mod diverges from $branch - divergent=1 - fi -done - -exit $divergent - diff --git a/.github/workflows/submodule-pin-check.yml b/.github/workflows/submodule-pin-check.yml index e459bad34d..94fa705655 100644 --- a/.github/workflows/submodule-pin-check.yml +++ b/.github/workflows/submodule-pin-check.yml @@ -1,21 +1,70 @@ -name: Submodule Pin Check +name: Merge Checks on: - pull_request: + pull_request_target: branches: [ master ] types: [synchronize, opened, reopened] +permissions: + statuses: write + jobs: submodule-pin-check: - name: Submodule Pin Check + name: Check Submodule Pin runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: recursive + submodules: true + persist-credentials: false + ref: "${{ github.event.pull_request.head.sha }}" - name: Check all submodules are ancestors of origin/HEAD or configured branch - run: ${{ github.workspace }}/.github/workflows/submodule-pin-check.sh + run: | + status_state="pending" + declare -Ar exceptions=( + [contracts]=origin/pre-bold + [nitro-testnode]=origin/master + + #TODO Rachel to check these are the intended branches. + [arbitrator/langs/c]=origin/vm-storage-cache + [arbitrator/tools/wasmer]=origin/adopt-v4.2.8 + ) + divergent=0 + for mod in `git submodule --quiet foreach 'echo $name'`; do + branch=origin/HEAD + if [[ -v exceptions[$mod] ]]; then + branch=${exceptions[$mod]} + fi + + if ! git -C $mod merge-base --is-ancestor HEAD $branch; then + echo $mod diverges from $branch + divergent=1 + fi + done + if [ $divergent -eq 0 ]; then + status_state="success" + else + resp="$(curl -sSL --fail-with-body \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/commits/${{ github.event.pull_request.head.sha }}/statuses")" + if ! jq -e '.[] | select(.context == "Submodule Pin Check")' > /dev/null <<< "$resp"; then + # Submodule pin check is failling and no status exists + # Keep it without a status to keep the green checkmark appearing + # Otherwise, the commit and PR's CI will appear to be indefinitely pending + # Merging will still be blocked until the required status appears + exit 0 + fi + fi + curl -sSL --fail-with-body \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/statuses/${{ github.event.pull_request.head.sha }}" \ + -d '{"context":"Submodule Pin Check","state":"'"$status_state"'"}' diff --git a/.gitmodules b/.gitmodules index d4d26282ae..24df007a79 100644 --- a/.gitmodules +++ b/.gitmodules @@ -23,6 +23,9 @@ [submodule "nitro-testnode"] path = nitro-testnode url = https://github.com/OffchainLabs/nitro-testnode.git +[submodule "bold"] + path = bold + url = https://github.com/OffchainLabs/bold.git [submodule "arbitrator/langs/rust"] path = arbitrator/langs/rust url = https://github.com/OffchainLabs/stylus-sdk-rs.git diff --git a/.golangci.yml b/.golangci.yml index 0594670137..8e597f950a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,6 +16,7 @@ linters: enable: - asciicheck # check for non-ascii characters - errorlint # enure error wrapping is safely done + - gci # keep imports sorted deterministically - gocritic # check for certain simplifications - gofmt # ensure code is formatted - gosec # check for security concerns @@ -30,6 +31,13 @@ linters-settings: # check-type-assertions: true + gci: + sections: + - standard + - default + - prefix(github.com/ethereum/go-ethereum) + - prefix(github.com/offchainlabs) + gocritic: disabled-tags: - experimental diff --git a/Dockerfile b/Dockerfile index 459412ca05..de15567131 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,7 +66,7 @@ COPY --from=wasm-libs-builder /workspace/ / FROM wasm-base AS wasm-bin-builder # pinned go version -RUN curl -L https://golang.org/dl/go1.21.10.linux-`dpkg --print-architecture`.tar.gz | tar -C /usr/local -xzf - +RUN curl -L https://golang.org/dl/go1.23.1.linux-`dpkg --print-architecture`.tar.gz | tar -C /usr/local -xzf - COPY ./Makefile ./go.mod ./go.sum ./ COPY ./arbcompress ./arbcompress COPY ./arbos ./arbos @@ -218,8 +218,9 @@ COPY ./scripts/download-machine.sh . #RUN ./download-machine.sh consensus-v20 0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4 RUN ./download-machine.sh consensus-v30 0xb0de9cb89e4d944ae6023a3b62276e54804c242fd8c4c2d8e6cc4450f5fa8b1b && true RUN ./download-machine.sh consensus-v31 0x260f5fa5c3176a856893642e149cf128b5a8de9f828afec8d11184415dd8dc69 +RUN ./download-machine.sh consensus-v32 0x184884e1eb9fefdc158f6c8ac912bb183bf3cf83f0090317e0bc4ac5860baa39 -FROM golang:1.21.10-bookworm AS node-builder +FROM golang:1.23.1-bookworm AS node-builder WORKDIR /workspace ARG version="" ARG datetime="" @@ -233,6 +234,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ COPY go.mod go.sum ./ COPY go-ethereum/go.mod go-ethereum/go.sum go-ethereum/ COPY fastcache/go.mod fastcache/go.sum fastcache/ +COPY bold/go.mod bold/go.sum bold/ RUN go mod download COPY . ./ COPY --from=contracts-builder workspace/contracts/build/ contracts/build/ diff --git a/LICENSE.md b/LICENSE.md index ea9a53da75..13e28a591a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -19,10 +19,10 @@ Additional Use Grant: You may use the Licensed Work in a production environment validating the correctness of the posted chain state, or to deploy and operate (x) a blockchain that settles to a Covered Arbitrum Chain or (y) a blockchain in accordance with, and subject to, the [Arbitrum - Expansion Program Term of Use](https://docs.arbitrum.foundation/assets/files/Arbitrum%20Expansion%20Program%20Jan182024-4f08b0c2cb476a55dc153380fa3e64b0.pdf). For purposes of this + Expansion Program Term of Use](https://docs.arbitrum.foundation/aep/ArbitrumExpansionProgramTerms.pdf). For purposes of this Additional Use Grant, the "Covered Arbitrum Chains" are (a) Arbitrum One (chainid:42161), Arbitrum Nova (chainid:42170), - rbitrum Rinkeby testnet/Rinkarby (chainid:421611),Arbitrum Nitro + Arbitrum Rinkeby testnet/Rinkarby (chainid:421611),Arbitrum Nitro Goerli testnet (chainid:421613), and Arbitrum Sepolia Testnet (chainid:421614); (b) any future blockchains authorized to be designated as Covered Arbitrum Chains by the decentralized autonomous diff --git a/Makefile b/Makefile index b49a7bafe1..da82678586 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,14 @@ ifneq ($(origin GOLANG_LDFLAGS),undefined) GOLANG_PARAMS = -ldflags="-extldflags '-ldl' $(GOLANG_LDFLAGS)" endif +UNAME_S := $(shell uname -s) + +# In Mac OSX, there are a lot of warnings emitted if these environment variables aren't set. +ifeq ($(UNAME_S), Darwin) + export MACOSX_DEPLOYMENT_TARGET := $(shell sw_vers -productVersion) + export CGO_LDFLAGS := -Wl,-no_warn_duplicate_libraries +endif + precompile_names = AddressTable Aggregator BLS Debug FunctionTable GasInfo Info osTest Owner RetryableTx Statistics Sys precompiles = $(patsubst %,./solgen/generated/%.go, $(precompile_names)) @@ -141,10 +149,14 @@ stylus_test_erc20_wasm = $(call get_stylus_test_wasm,erc20) stylus_test_erc20_src = $(call get_stylus_test_rust,erc20) stylus_test_read-return-data_wasm = $(call get_stylus_test_wasm,read-return-data) stylus_test_read-return-data_src = $(call get_stylus_test_rust,read-return-data) +stylus_test_hostio-test_wasm = $(call get_stylus_test_wasm,hostio-test) +stylus_test_hostio-test_src = $(call get_stylus_test_rust,hostio-test) -stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_multicall_wasm) $(stylus_test_log_wasm) $(stylus_test_create_wasm) $(stylus_test_math_wasm) $(stylus_test_sdk-storage_wasm) $(stylus_test_erc20_wasm) $(stylus_test_read-return-data_wasm) $(stylus_test_evm-data_wasm) $(stylus_test_bfs:.b=.wasm) +stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_multicall_wasm) $(stylus_test_log_wasm) $(stylus_test_create_wasm) $(stylus_test_math_wasm) $(stylus_test_sdk-storage_wasm) $(stylus_test_erc20_wasm) $(stylus_test_read-return-data_wasm) $(stylus_test_evm-data_wasm) $(stylus_test_hostio-test_wasm) $(stylus_test_bfs:.b=.wasm) stylus_benchmarks = $(wildcard $(stylus_dir)/*.toml $(stylus_dir)/src/*.rs) $(stylus_test_wasms) +CBROTLI_WASM_BUILD_ARGS ?=-d + # user targets .PHONY: push @@ -275,6 +287,7 @@ clean: rm -f arbitrator/wasm-libraries/soft-float/SoftFloat/build/Wasm-Clang/*.a rm -f arbitrator/wasm-libraries/forward/*.wat rm -rf arbitrator/stylus/tests/*/target/ arbitrator/stylus/tests/*/*.wasm + rm -rf brotli/buildfiles @rm -rf contracts/build contracts/cache solgen/go/ @rm -f .make/* @@ -479,6 +492,10 @@ $(stylus_test_erc20_wasm): $(stylus_test_erc20_src) $(cargo_nightly) --manifest-path $< --release --config $(stylus_cargo) @touch -c $@ # cargo might decide to not rebuild the binary +$(stylus_test_hostio-test_wasm): $(stylus_test_hostio-test_src) + $(cargo_nightly) --manifest-path $< --release --config $(stylus_cargo) + @touch -c $@ # cargo might decide to not rebuild the binary + contracts/test/prover/proofs/float%.json: $(arbitrator_cases)/float%.wasm $(prover_bin) $(output_latest)/soft-float.wasm $(prover_bin) $< -l $(output_latest)/soft-float.wasm -o $@ -b --allow-hostapi --require-success @@ -570,9 +587,9 @@ contracts/test/prover/proofs/%.json: $(arbitrator_cases)/%.wasm $(prover_bin) @touch $@ .make/cbrotli-wasm: $(DEP_PREDICATE) $(ORDER_ONLY_PREDICATE) .make - test -f target/lib-wasm/libbrotlicommon-static.a || ./scripts/build-brotli.sh -w -d - test -f target/lib-wasm/libbrotlienc-static.a || ./scripts/build-brotli.sh -w -d - test -f target/lib-wasm/libbrotlidec-static.a || ./scripts/build-brotli.sh -w -d + test -f target/lib-wasm/libbrotlicommon-static.a || ./scripts/build-brotli.sh -w $(CBROTLI_WASM_BUILD_ARGS) + test -f target/lib-wasm/libbrotlienc-static.a || ./scripts/build-brotli.sh -w $(CBROTLI_WASM_BUILD_ARGS) + test -f target/lib-wasm/libbrotlidec-static.a || ./scripts/build-brotli.sh -w $(CBROTLI_WASM_BUILD_ARGS) @touch $@ .make/wasm-lib: $(DEP_PREDICATE) arbitrator/wasm-libraries/soft-float/SoftFloat/build/Wasm-Clang/softfloat.a $(ORDER_ONLY_PREDICATE) .make diff --git a/README.md b/README.md index 1f0e4ac81c..30904238dc 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Nitro is currently licensed under a [Business Source License](./LICENSE.md), sim The Additional Use Grant also permits the deployment of the Nitro software, in a permissionless fashion and without cost, as a new blockchain provided that the chain settles to either Arbitrum One or Arbitrum Nova. -For those that prefer to deploy the Nitro software either directly on Ethereum (i.e. an L2) or have it settle to another Layer-2 on top of Ethereum, the [Arbitrum Expansion Program (the "AEP")](https://docs.arbitrum.foundation/assets/files/Arbitrum%20Expansion%20Program%20Jan182024-4f08b0c2cb476a55dc153380fa3e64b0.pdf) was recently established. The AEP allows for the permissionless deployment in the aforementioned fashion provided that 10% of net revenue (as more fully described in the AEP) is contributed back to the Arbitrum community in accordance with the requirements of the AEP. +For those that prefer to deploy the Nitro software either directly on Ethereum (i.e. an L2) or have it settle to another Layer-2 on top of Ethereum, the [Arbitrum Expansion Program (the "AEP")](https://docs.arbitrum.foundation/aep/ArbitrumExpansionProgramTerms.pdf) was recently established. The AEP allows for the permissionless deployment in the aforementioned fashion provided that 10% of net revenue (as more fully described in the AEP) is contributed back to the Arbitrum community in accordance with the requirements of the AEP. ## Contact diff --git a/arbcompress/compress_common.go b/arbcompress/compress_common.go index a61dd9a171..997232e7cc 100644 --- a/arbcompress/compress_common.go +++ b/arbcompress/compress_common.go @@ -17,6 +17,8 @@ func compressedBufferSizeFor(length int) int { return length + (length>>10)*8 + 64 // actual limit is: length + (length >> 14) * 4 + 6 } -func CompressLevel(input []byte, level int) ([]byte, error) { +func CompressLevel(input []byte, level uint64) ([]byte, error) { + // level is trusted and shouldn't be anything crazy + // #nosec G115 return Compress(input, uint32(level), EmptyDictionary) } diff --git a/arbcompress/native.go b/arbcompress/native.go index 8244010979..943d21e89e 100644 --- a/arbcompress/native.go +++ b/arbcompress/native.go @@ -7,11 +7,12 @@ package arbcompress /* -#cgo CFLAGS: -g -Wall -I${SRCDIR}/../target/include/ +#cgo CFLAGS: -g -I${SRCDIR}/../target/include/ #cgo LDFLAGS: ${SRCDIR}/../target/lib/libstylus.a -lm #include "arbitrator.h" */ import "C" + import ( "errors" "fmt" diff --git a/arbitrator/Cargo.lock b/arbitrator/Cargo.lock index 79a9117a31..2b437968fa 100644 --- a/arbitrator/Cargo.lock +++ b/arbitrator/Cargo.lock @@ -215,7 +215,6 @@ dependencies = [ "prover", "serde", "serde_json", - "serde_with 3.9.0", ] [[package]] @@ -496,6 +495,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + [[package]] name = "colorchoice" version = "1.0.2" @@ -705,38 +710,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - [[package]] name = "darling" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -753,24 +734,13 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core 0.13.4", - "quote", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core 0.20.10", + "darling_core", "quote", "syn 2.0.72", ] @@ -928,7 +898,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" dependencies = [ - "darling 0.20.10", + "darling", "proc-macro2", "quote", "syn 2.0.72", @@ -1750,7 +1720,7 @@ dependencies = [ "rustc-demangle", "serde", "serde_json", - "serde_with 1.14.0", + "serde_with", "sha2 0.9.9", "sha3 0.9.1", "smallvec", @@ -2073,16 +2043,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros 1.5.2", -] - [[package]] name = "serde_with" version = "3.9.0" @@ -2097,29 +2057,17 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "serde_with_macros 3.9.0", + "serde_with_macros", "time", ] -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling 0.13.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "serde_with_macros" version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ - "darling 0.20.10", + "darling", "proc-macro2", "quote", "syn 2.0.72", @@ -2226,12 +2174,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -2270,13 +2212,13 @@ dependencies = [ "bincode", "brotli", "caller-env", + "clru", "derivative", "eyre", "fnv", "hex", "lazy_static", "libc", - "lru", "num-bigint", "parking_lot", "prover", diff --git a/arbitrator/Cargo.toml b/arbitrator/Cargo.toml index 94ca08b0b5..3c5228daf2 100644 --- a/arbitrator/Cargo.toml +++ b/arbitrator/Cargo.toml @@ -12,6 +12,7 @@ members = [ exclude = [ "stylus/tests/", "tools/wasmer/", + "tools/stylus_benchmark", ] resolver = "2" @@ -24,9 +25,7 @@ repository = "https://github.com/OffchainLabs/nitro.git" rust-version = "1.67" [workspace.dependencies] -cfg-if = "1.0.0" lazy_static = "1.4.0" -lru = "0.12.3" num_enum = { version = "0.7.2", default-features = false } ruint2 = "1.9.0" wasmparser = "0.121" diff --git a/arbitrator/arbutil/src/benchmark.rs b/arbitrator/arbutil/src/benchmark.rs new file mode 100644 index 0000000000..580d0191a0 --- /dev/null +++ b/arbitrator/arbutil/src/benchmark.rs @@ -0,0 +1,14 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +use crate::evm::api::Ink; +use std::time::{Duration, Instant}; + +// Benchmark is used to track the performance of blocks of code in stylus +#[derive(Clone, Copy, Debug, Default)] +pub struct Benchmark { + pub timer: Option, + pub elapsed_total: Duration, + pub ink_start: Option, + pub ink_total: Ink, +} diff --git a/arbitrator/arbutil/src/evm/api.rs b/arbitrator/arbutil/src/evm/api.rs index 093e7f2984..0a603a3bb2 100644 --- a/arbitrator/arbutil/src/evm/api.rs +++ b/arbitrator/arbutil/src/evm/api.rs @@ -73,19 +73,103 @@ impl DataReader for VecReader { } } +macro_rules! derive_math { + ($t:ident) => { + impl std::ops::Add for $t { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + } + + impl std::ops::AddAssign for $t { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } + } + + impl std::ops::Sub for $t { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + } + + impl std::ops::SubAssign for $t { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } + } + + impl std::ops::Mul for $t { + type Output = Self; + + fn mul(self, rhs: u64) -> Self { + Self(self.0 * rhs) + } + } + + impl std::ops::Mul<$t> for u64 { + type Output = $t; + + fn mul(self, rhs: $t) -> $t { + $t(self * rhs.0) + } + } + + impl $t { + /// Equivalent to the Add trait, but const. + pub const fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + + /// Equivalent to the Sub trait, but const. + pub const fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + + pub const fn saturating_add(self, rhs: Self) -> Self { + Self(self.0.saturating_add(rhs.0)) + } + + pub const fn saturating_sub(self, rhs: Self) -> Self { + Self(self.0.saturating_sub(rhs.0)) + } + + pub fn to_be_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } + } + }; +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +#[must_use] +pub struct Gas(pub u64); + +derive_math!(Gas); + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +#[must_use] +pub struct Ink(pub u64); + +derive_math!(Ink); + pub trait EvmApi: Send + 'static { /// Reads the 32-byte value in the EVM state trie at offset `key`. /// Returns the value and the access cost in gas. /// Analogous to `vm.SLOAD`. - fn get_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64); + fn get_bytes32(&mut self, key: Bytes32, evm_api_gas_to_use: Gas) -> (Bytes32, Gas); /// Stores the given value at the given key in Stylus VM's cache of the EVM state trie. /// Note that the actual values only get written after calls to `set_trie_slots`. - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64; + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas; /// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested. /// Analogous to repeated invocations of `vm.SSTORE`. - fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result; + fn flush_storage_cache(&mut self, clear: bool, gas_left: Gas) -> Result; /// Reads the 32-byte value in the EVM's transient state trie at offset `key`. /// Analogous to `vm.TLOAD`. @@ -102,10 +186,10 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, + gas_left: Gas, + gas_req: Gas, value: Bytes32, - ) -> (u32, u64, UserOutcomeKind); + ) -> (u32, Gas, UserOutcomeKind); /// Delegate-calls the contract at the given address. /// Returns the EVM return data's length, the gas cost, and whether the call succeeded. @@ -114,9 +198,9 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind); + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind); /// Static-calls the contract at the given address. /// Returns the EVM return data's length, the gas cost, and whether the call succeeded. @@ -125,9 +209,9 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind); + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind); /// Deploys a new contract using the init code provided. /// Returns the new contract's address on success, or the error reason on failure. @@ -137,8 +221,8 @@ pub trait EvmApi: Send + 'static { &mut self, code: Vec, endowment: Bytes32, - gas: u64, - ) -> (eyre::Result, u32, u64); + gas: Gas, + ) -> (eyre::Result, u32, Gas); /// Deploys a new contract using the init code provided, with an address determined in part by the `salt`. /// Returns the new contract's address on success, or the error reason on failure. @@ -149,8 +233,8 @@ pub trait EvmApi: Send + 'static { code: Vec, endowment: Bytes32, salt: Bytes32, - gas: u64, - ) -> (eyre::Result, u32, u64); + gas: Gas, + ) -> (eyre::Result, u32, Gas); /// Returns the EVM return data. /// Analogous to `vm.RETURNDATACOPY`. @@ -164,21 +248,21 @@ pub trait EvmApi: Send + 'static { /// Gets the balance of the given account. /// Returns the balance and the access cost in gas. /// Analogous to `vm.BALANCE`. - fn account_balance(&mut self, address: Bytes20) -> (Bytes32, u64); + fn account_balance(&mut self, address: Bytes20) -> (Bytes32, Gas); /// Returns the code and the access cost in gas. /// Analogous to `vm.EXTCODECOPY`. - fn account_code(&mut self, address: Bytes20, gas_left: u64) -> (D, u64); + fn account_code(&mut self, address: Bytes20, gas_left: Gas) -> (D, Gas); /// Gets the hash of the given address's code. /// Returns the hash and the access cost in gas. /// Analogous to `vm.EXTCODEHASH`. - fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, u64); + fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, Gas); /// Determines the cost in gas of allocating additional wasm pages. /// Note: has the side effect of updating Geth's memory usage tracker. /// Not analogous to any EVM opcode. - fn add_pages(&mut self, pages: u16) -> u64; + fn add_pages(&mut self, pages: u16) -> Gas; /// Captures tracing information for hostio invocations during native execution. fn capture_hostio( @@ -186,7 +270,7 @@ pub trait EvmApi: Send + 'static { name: &str, args: &[u8], outs: &[u8], - start_ink: u64, - end_ink: u64, + start_ink: Ink, + end_ink: Ink, ); } diff --git a/arbitrator/arbutil/src/evm/mod.rs b/arbitrator/arbutil/src/evm/mod.rs index 1671e67072..063194b0c6 100644 --- a/arbitrator/arbutil/src/evm/mod.rs +++ b/arbitrator/arbutil/src/evm/mod.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::{Bytes20, Bytes32}; +use api::Gas; pub mod api; pub mod req; @@ -9,74 +10,77 @@ pub mod storage; pub mod user; // params.SstoreSentryGasEIP2200 -pub const SSTORE_SENTRY_GAS: u64 = 2300; +pub const SSTORE_SENTRY_GAS: Gas = Gas(2300); // params.ColdAccountAccessCostEIP2929 -pub const COLD_ACCOUNT_GAS: u64 = 2600; +pub const COLD_ACCOUNT_GAS: Gas = Gas(2600); // params.ColdSloadCostEIP2929 -pub const COLD_SLOAD_GAS: u64 = 2100; +pub const COLD_SLOAD_GAS: Gas = Gas(2100); // params.WarmStorageReadCostEIP2929 -pub const WARM_SLOAD_GAS: u64 = 100; +pub const WARM_SLOAD_GAS: Gas = Gas(100); // params.WarmStorageReadCostEIP2929 (see enable1153 in jump_table.go) -pub const TLOAD_GAS: u64 = WARM_SLOAD_GAS; -pub const TSTORE_GAS: u64 = WARM_SLOAD_GAS; +pub const TLOAD_GAS: Gas = WARM_SLOAD_GAS; +pub const TSTORE_GAS: Gas = WARM_SLOAD_GAS; // params.LogGas and params.LogDataGas -pub const LOG_TOPIC_GAS: u64 = 375; -pub const LOG_DATA_GAS: u64 = 8; +pub const LOG_TOPIC_GAS: Gas = Gas(375); +pub const LOG_DATA_GAS: Gas = Gas(8); // params.CopyGas -pub const COPY_WORD_GAS: u64 = 3; +pub const COPY_WORD_GAS: Gas = Gas(3); // params.Keccak256Gas -pub const KECCAK_256_GAS: u64 = 30; -pub const KECCAK_WORD_GAS: u64 = 6; +pub const KECCAK_256_GAS: Gas = Gas(30); +pub const KECCAK_WORD_GAS: Gas = Gas(6); // vm.GasQuickStep (see gas.go) -pub const GAS_QUICK_STEP: u64 = 2; +pub const GAS_QUICK_STEP: Gas = Gas(2); // vm.GasQuickStep (see jump_table.go) -pub const ADDRESS_GAS: u64 = GAS_QUICK_STEP; +pub const ADDRESS_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see eips.go) -pub const BASEFEE_GAS: u64 = GAS_QUICK_STEP; +pub const BASEFEE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see eips.go) -pub const CHAINID_GAS: u64 = GAS_QUICK_STEP; +pub const CHAINID_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const COINBASE_GAS: u64 = GAS_QUICK_STEP; +pub const COINBASE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const GASLIMIT_GAS: u64 = GAS_QUICK_STEP; +pub const GASLIMIT_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const NUMBER_GAS: u64 = GAS_QUICK_STEP; +pub const NUMBER_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const TIMESTAMP_GAS: u64 = GAS_QUICK_STEP; +pub const TIMESTAMP_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const GASLEFT_GAS: u64 = GAS_QUICK_STEP; +pub const GASLEFT_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const CALLER_GAS: u64 = GAS_QUICK_STEP; +pub const CALLER_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const CALLVALUE_GAS: u64 = GAS_QUICK_STEP; +pub const CALLVALUE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const GASPRICE_GAS: u64 = GAS_QUICK_STEP; +pub const GASPRICE_GAS: Gas = GAS_QUICK_STEP; // vm.GasQuickStep (see jump_table.go) -pub const ORIGIN_GAS: u64 = GAS_QUICK_STEP; +pub const ORIGIN_GAS: Gas = GAS_QUICK_STEP; + +pub const ARBOS_VERSION_STYLUS_CHARGING_FIXES: u64 = 32; #[derive(Clone, Copy, Debug, Default)] #[repr(C)] pub struct EvmData { + pub arbos_version: u64, pub block_basefee: Bytes32, pub chainid: u64, pub block_coinbase: Bytes20, diff --git a/arbitrator/arbutil/src/evm/req.rs b/arbitrator/arbutil/src/evm/req.rs index b1c8d99972..621f41e951 100644 --- a/arbitrator/arbutil/src/evm/req.rs +++ b/arbitrator/arbutil/src/evm/req.rs @@ -7,15 +7,15 @@ use crate::{ storage::{StorageCache, StorageWord}, user::UserOutcomeKind, }, - format::Utf8OrHex, - pricing::EVM_API_INK, Bytes20, Bytes32, }; use eyre::{bail, eyre, Result}; use std::collections::hash_map::Entry; +use super::api::{Gas, Ink}; + pub trait RequestHandler: Send + 'static { - fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, u64); + fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, Gas); } pub struct EvmApiRequestor> { @@ -35,7 +35,7 @@ impl> EvmApiRequestor { } } - fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, u64) { + fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec, D, Gas) { self.handler.request(req_type, req_data) } @@ -45,10 +45,10 @@ impl> EvmApiRequestor { call_type: EvmApiMethod, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, + gas_left: Gas, + gas_req: Gas, value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { let mut request = Vec::with_capacity(20 + 32 + 8 + 8 + input.len()); request.extend(contract); request.extend(value); @@ -73,8 +73,8 @@ impl> EvmApiRequestor { code: Vec, endowment: Bytes32, salt: Option, - gas: u64, - ) -> (Result, u32, u64) { + gas: Gas, + ) -> (Result, u32, Gas) { let mut request = Vec::with_capacity(8 + 2 * 32 + code.len()); request.extend(gas.to_be_bytes()); request.extend(endowment); @@ -100,19 +100,19 @@ impl> EvmApiRequestor { } impl> EvmApi for EvmApiRequestor { - fn get_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64) { + fn get_bytes32(&mut self, key: Bytes32, evm_api_gas_to_use: Gas) -> (Bytes32, Gas) { let cache = &mut self.storage_cache; let mut cost = cache.read_gas(); let value = cache.entry(key).or_insert_with(|| { let (res, _, gas) = self.handler.request(EvmApiMethod::GetBytes32, key); - cost = cost.saturating_add(gas).saturating_add(EVM_API_INK); + cost = cost.saturating_add(gas).saturating_add(evm_api_gas_to_use); StorageWord::known(res.try_into().unwrap()) }); (value.value, cost) } - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 { + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas { let cost = self.storage_cache.write_gas(); match self.storage_cache.entry(key) { Entry::Occupied(mut key) => key.get_mut().value = value, @@ -121,7 +121,7 @@ impl> EvmApi for EvmApiRequestor { cost } - fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result { + fn flush_storage_cache(&mut self, clear: bool, gas_left: Gas) -> Result { let mut data = Vec::with_capacity(64 * self.storage_cache.len() + 8); data.extend(gas_left.to_be_bytes()); @@ -136,12 +136,17 @@ impl> EvmApi for EvmApiRequestor { self.storage_cache.clear(); } if data.len() == 8 { - return Ok(0); // no need to make request + return Ok(Gas(0)); // no need to make request } let (res, _, cost) = self.request(EvmApiMethod::SetTrieSlots, data); - if res[0] != EvmApiStatus::Success.into() { - bail!("{}", String::from_utf8_or_hex(res)); + let status = res + .first() + .copied() + .map(EvmApiStatus::from) + .unwrap_or(EvmApiStatus::Failure); + if status != EvmApiStatus::Success { + bail!("{:?}", status); } Ok(cost) } @@ -156,8 +161,13 @@ impl> EvmApi for EvmApiRequestor { data.extend(key); data.extend(value); let (res, ..) = self.request(EvmApiMethod::SetTransientBytes32, data); - if res[0] != EvmApiStatus::Success.into() { - bail!("{}", String::from_utf8_or_hex(res)); + let status = res + .first() + .copied() + .map(EvmApiStatus::from) + .unwrap_or(EvmApiStatus::Failure); + if status != EvmApiStatus::Success { + bail!("{:?}", status); } Ok(()) } @@ -166,10 +176,10 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, + gas_left: Gas, + gas_req: Gas, value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { self.call_request( EvmApiMethod::ContractCall, contract, @@ -184,9 +194,9 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { self.call_request( EvmApiMethod::DelegateCall, contract, @@ -201,9 +211,9 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { self.call_request( EvmApiMethod::StaticCall, contract, @@ -218,8 +228,8 @@ impl> EvmApi for EvmApiRequestor { &mut self, code: Vec, endowment: Bytes32, - gas: u64, - ) -> (Result, u32, u64) { + gas: Gas, + ) -> (Result, u32, Gas) { self.create_request(EvmApiMethod::Create1, code, endowment, None, gas) } @@ -228,8 +238,8 @@ impl> EvmApi for EvmApiRequestor { code: Vec, endowment: Bytes32, salt: Bytes32, - gas: u64, - ) -> (Result, u32, u64) { + gas: Gas, + ) -> (Result, u32, Gas) { self.create_request(EvmApiMethod::Create2, code, endowment, Some(salt), gas) } @@ -250,15 +260,15 @@ impl> EvmApi for EvmApiRequestor { Ok(()) } - fn account_balance(&mut self, address: Bytes20) -> (Bytes32, u64) { + fn account_balance(&mut self, address: Bytes20) -> (Bytes32, Gas) { let (res, _, cost) = self.request(EvmApiMethod::AccountBalance, address); (res.try_into().unwrap(), cost) } - fn account_code(&mut self, address: Bytes20, gas_left: u64) -> (D, u64) { + fn account_code(&mut self, address: Bytes20, gas_left: Gas) -> (D, Gas) { if let Some((stored_address, data)) = self.last_code.as_ref() { if address == *stored_address { - return (data.clone(), 0); + return (data.clone(), Gas(0)); } } let mut req = Vec::with_capacity(20 + 8); @@ -270,12 +280,12 @@ impl> EvmApi for EvmApiRequestor { (data, cost) } - fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, u64) { + fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, Gas) { let (res, _, cost) = self.request(EvmApiMethod::AccountCodeHash, address); (res.try_into().unwrap(), cost) } - fn add_pages(&mut self, pages: u16) -> u64 { + fn add_pages(&mut self, pages: u16) -> Gas { self.request(EvmApiMethod::AddPages, pages.to_be_bytes()).2 } @@ -284,18 +294,20 @@ impl> EvmApi for EvmApiRequestor { name: &str, args: &[u8], outs: &[u8], - start_ink: u64, - end_ink: u64, + start_ink: Ink, + end_ink: Ink, ) { let mut request = Vec::with_capacity(2 * 8 + 3 * 2 + name.len() + args.len() + outs.len()); request.extend(start_ink.to_be_bytes()); request.extend(end_ink.to_be_bytes()); - request.extend((name.len() as u16).to_be_bytes()); - request.extend((args.len() as u16).to_be_bytes()); - request.extend((outs.len() as u16).to_be_bytes()); + // u32 is enough to represent the slices lengths because the WASM environment runs in 32 bits. + request.extend((name.len() as u32).to_be_bytes()); + request.extend((args.len() as u32).to_be_bytes()); + request.extend((outs.len() as u32).to_be_bytes()); request.extend(name.as_bytes()); request.extend(args); request.extend(outs); - self.request(EvmApiMethod::CaptureHostIO, request); + // ignore response (including gas) as we're just tracing + _ = self.request(EvmApiMethod::CaptureHostIO, request); } } diff --git a/arbitrator/arbutil/src/evm/storage.rs b/arbitrator/arbutil/src/evm/storage.rs index 32b60dd21b..5f688364d7 100644 --- a/arbitrator/arbutil/src/evm/storage.rs +++ b/arbitrator/arbutil/src/evm/storage.rs @@ -5,6 +5,8 @@ use crate::Bytes32; use fnv::FnvHashMap as HashMap; use std::ops::{Deref, DerefMut}; +use super::api::Gas; + /// Represents the EVM word at a given key. #[derive(Debug)] pub struct StorageWord { @@ -37,23 +39,23 @@ pub struct StorageCache { } impl StorageCache { - pub const REQUIRED_ACCESS_GAS: u64 = 10; + pub const REQUIRED_ACCESS_GAS: Gas = Gas(10); - pub fn read_gas(&mut self) -> u64 { + pub fn read_gas(&mut self) -> Gas { self.reads += 1; match self.reads { - 0..=32 => 0, - 33..=128 => 2, - _ => 10, + 0..=32 => Gas(0), + 33..=128 => Gas(2), + _ => Gas(10), } } - pub fn write_gas(&mut self) -> u64 { + pub fn write_gas(&mut self) -> Gas { self.writes += 1; match self.writes { - 0..=8 => 0, - 9..=64 => 7, - _ => 10, + 0..=8 => Gas(0), + 9..=64 => Gas(7), + _ => Gas(10), } } } diff --git a/arbitrator/arbutil/src/lib.rs b/arbitrator/arbutil/src/lib.rs index 9c48a9fefc..e17e8d9448 100644 --- a/arbitrator/arbutil/src/lib.rs +++ b/arbitrator/arbutil/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +pub mod benchmark; /// cbindgen:ignore pub mod color; pub mod crypto; diff --git a/arbitrator/arbutil/src/pricing.rs b/arbitrator/arbutil/src/pricing.rs index 4614b02a2a..4d6bf827be 100644 --- a/arbitrator/arbutil/src/pricing.rs +++ b/arbitrator/arbutil/src/pricing.rs @@ -1,20 +1,22 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +use crate::evm::api::Ink; + /// For hostios that may return something. -pub const HOSTIO_INK: u64 = 8400; +pub const HOSTIO_INK: Ink = Ink(8400); /// For hostios that include pointers. -pub const PTR_INK: u64 = 13440 - HOSTIO_INK; +pub const PTR_INK: Ink = Ink(13440).sub(HOSTIO_INK); /// For hostios that involve an API cost. -pub const EVM_API_INK: u64 = 59673; +pub const EVM_API_INK: Ink = Ink(59673); /// For hostios that involve a div or mod. -pub const DIV_INK: u64 = 20000; +pub const DIV_INK: Ink = Ink(20000); /// For hostios that involve a mulmod. -pub const MUL_MOD_INK: u64 = 24100; +pub const MUL_MOD_INK: Ink = Ink(24100); /// For hostios that involve an addmod. -pub const ADD_MOD_INK: u64 = 21000; +pub const ADD_MOD_INK: Ink = Ink(21000); diff --git a/arbitrator/arbutil/src/types.rs b/arbitrator/arbutil/src/types.rs index 6cf1d6cdf7..722a89b81e 100644 --- a/arbitrator/arbutil/src/types.rs +++ b/arbitrator/arbutil/src/types.rs @@ -8,6 +8,7 @@ use std::{ borrow::Borrow, fmt, ops::{Deref, DerefMut}, + str::FromStr, }; // These values must be kept in sync with `arbutil/preimage_type.go`, @@ -83,6 +84,32 @@ impl From for Bytes32 { } } +impl FromStr for Bytes32 { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + // Remove the "0x" prefix if present + let s = s.strip_prefix("0x").unwrap_or(s); + + // Pad with leading zeros if the string is shorter than 64 characters (32 bytes) + let padded = format!("{:0>64}", s); + + // Decode the hex string using the hex crate + let decoded_bytes = hex::decode(padded).map_err(|_| "Invalid hex string")?; + + // Ensure the decoded bytes is exactly 32 bytes + if decoded_bytes.len() != 32 { + return Err("Hex string too long for Bytes32"); + } + + // Create a 32-byte array and fill it with the decoded bytes. + let mut b = [0u8; 32]; + b.copy_from_slice(&decoded_bytes); + + Ok(Bytes32(b)) + } +} + impl TryFrom<&[u8]> for Bytes32 { type Error = std::array::TryFromSliceError; @@ -249,3 +276,77 @@ impl From for Bytes20 { <[u8; 20]>::from(x).into() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_bytes32() { + let b = Bytes32::from(0x12345678u32); + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x12, 0x34, 0x56, 0x78, + ]; + assert_eq!(b, Bytes32(expected)); + } + + #[test] + fn test_from_str_short() { + // Short hex string + let b = Bytes32::from_str("0x12345678").unwrap(); + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x12, 0x34, 0x56, 0x78, + ]; + assert_eq!(b, Bytes32(expected)); + } + + #[test] + fn test_from_str_very_short() { + // Short hex string + let b = Bytes32::from_str("0x1").unwrap(); + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0x1, + ]; + assert_eq!(b, Bytes32(expected)); + } + + #[test] + fn test_from_str_no_prefix() { + // Short hex string + let b = Bytes32::from_str("12345678").unwrap(); + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x12, 0x34, 0x56, 0x78, + ]; + assert_eq!(b, Bytes32(expected)); + } + + #[test] + fn test_from_str_full() { + // Full-length hex string + let b = + Bytes32::from_str("0x0000000000000000000000000000000000000000000000000000000012345678") + .unwrap(); + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x12, 0x34, 0x56, 0x78, + ]; + assert_eq!(b, Bytes32(expected)); + } + + #[test] + fn test_from_str_invalid_non_hex() { + let s = "0x123g5678"; // Invalid character 'g' + assert!(Bytes32::from_str(s).is_err()); + } + + #[test] + fn test_from_str_too_big() { + let s = + "0123456789ABCDEF0123456789ABCDEF01234567890123456789ABCDEF01234567890123456789ABCDEF0"; // 65 characters + assert!(Bytes32::from_str(s).is_err()); + } +} diff --git a/arbitrator/bench/Cargo.toml b/arbitrator/bench/Cargo.toml index 3ab5b99b08..74b948aca8 100644 --- a/arbitrator/bench/Cargo.toml +++ b/arbitrator/bench/Cargo.toml @@ -3,10 +3,6 @@ name = "bench" version = "0.1.0" edition = "2021" -[lib] -name = "bench" -path = "src/lib.rs" - [[bin]] name = "benchbin" path = "src/bin.rs" @@ -20,7 +16,6 @@ clap = { version = "4.4.8", features = ["derive"] } gperftools = { version = "0.2.0", optional = true } serde = { version = "1.0.130", features = ["derive", "rc"] } serde_json = "1.0.67" -serde_with = { version = "3.8.1", features = ["base64"] } [features] counters = [] diff --git a/arbitrator/bench/src/bin.rs b/arbitrator/bench/src/bin.rs index f7e69f5373..f9bd85ce53 100644 --- a/arbitrator/bench/src/bin.rs +++ b/arbitrator/bench/src/bin.rs @@ -1,6 +1,5 @@ use std::{path::PathBuf, time::Duration}; -use bench::prepare::*; use clap::Parser; use eyre::bail; @@ -10,21 +9,22 @@ use gperftools::profiler::PROFILER; #[cfg(feature = "heapprof")] use gperftools::heap_profiler::HEAP_PROFILER; -use prover::machine::MachineStatus; - #[cfg(feature = "counters")] use prover::{machine, memory, merkle}; +use prover::machine::MachineStatus; +use prover::prepare::prepare_machine; + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - /// Path to a preimages text file + /// Path to a preimages json file #[arg(short, long)] - preimages_path: PathBuf, + json_inputs: PathBuf, /// Path to a machine.wavm.br #[arg(short, long)] - machine_path: PathBuf, + binary: PathBuf, } fn main() -> eyre::Result<()> { @@ -33,7 +33,7 @@ fn main() -> eyre::Result<()> { println!("Running benchmark with always merkleize feature on"); for step_size in step_sizes { - let mut machine = prepare_machine(args.preimages_path.clone(), args.machine_path.clone())?; + let mut machine = prepare_machine(args.json_inputs.clone(), args.binary.clone())?; let _ = machine.hash(); let mut hash_times = vec![]; let mut step_times = vec![]; diff --git a/arbitrator/bench/src/lib.rs b/arbitrator/bench/src/lib.rs deleted file mode 100644 index 5f7c024094..0000000000 --- a/arbitrator/bench/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod parse_input; -pub mod prepare; diff --git a/arbitrator/bench/src/parse_input.rs b/arbitrator/bench/src/parse_input.rs deleted file mode 100644 index decc67372a..0000000000 --- a/arbitrator/bench/src/parse_input.rs +++ /dev/null @@ -1,76 +0,0 @@ -use arbutil::Bytes32; -use serde::{Deserialize, Serialize}; -use serde_json; -use serde_with::base64::Base64; -use serde_with::As; -use serde_with::DisplayFromStr; -use std::{ - collections::HashMap, - io::{self, BufRead}, -}; - -mod prefixed_hex { - use serde::{self, Deserialize, Deserializer, Serializer}; - - pub fn serialize(bytes: &Vec, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - if let Some(s) = s.strip_prefix("0x") { - hex::decode(s).map_err(serde::de::Error::custom) - } else { - Err(serde::de::Error::custom("missing 0x prefix")) - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PreimageMap(HashMap>); - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -pub struct BatchInfo { - pub number: u64, - #[serde(with = "As::")] - pub data_b64: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -pub struct StartState { - #[serde(with = "prefixed_hex")] - pub block_hash: Vec, - #[serde(with = "prefixed_hex")] - pub send_root: Vec, - pub batch: u64, - pub pos_in_batch: u64, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -pub struct FileData { - pub id: u64, - pub has_delayed_msg: bool, - pub delayed_msg_nr: u64, - #[serde(with = "As::>>")] - pub preimages_b64: HashMap>>, - pub batch_info: Vec, - #[serde(with = "As::")] - pub delayed_msg_b64: Vec, - pub start_state: StartState, -} - -impl FileData { - pub fn from_reader(mut reader: R) -> io::Result { - let data = serde_json::from_reader(&mut reader)?; - Ok(data) - } -} diff --git a/arbitrator/jit/src/lib.rs b/arbitrator/jit/src/lib.rs new file mode 100644 index 0000000000..d0ad76bd03 --- /dev/null +++ b/arbitrator/jit/src/lib.rs @@ -0,0 +1,51 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use std::path::PathBuf; +use structopt::StructOpt; + +mod arbcompress; +mod caller_env; +pub mod machine; +mod prepare; +pub mod program; +mod socket; +pub mod stylus_backend; +mod test; +mod wasip1_stub; +mod wavmio; + +#[derive(StructOpt)] +#[structopt(name = "jit-prover")] +pub struct Opts { + #[structopt(short, long)] + binary: PathBuf, + #[structopt(long, default_value = "0")] + inbox_position: u64, + #[structopt(long, default_value = "0")] + delayed_inbox_position: u64, + #[structopt(long, default_value = "0")] + position_within_message: u64, + #[structopt(long)] + last_block_hash: Option, + #[structopt(long)] + last_send_root: Option, + #[structopt(long)] + inbox: Vec, + #[structopt(long)] + delayed_inbox: Vec, + #[structopt(long)] + preimages: Option, + #[structopt(long)] + cranelift: bool, + #[structopt(long)] + forks: bool, + #[structopt(long)] + pub debug: bool, + #[structopt(long)] + pub require_success: bool, + // JSON inputs supercede any of the command-line inputs which could + // be specified in the JSON file. + #[structopt(long)] + json_inputs: Option, +} diff --git a/arbitrator/jit/src/machine.rs b/arbitrator/jit/src/machine.rs index 2a3c5c5616..0d74c74ef6 100644 --- a/arbitrator/jit/src/machine.rs +++ b/arbitrator/jit/src/machine.rs @@ -2,8 +2,8 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::{ - arbcompress, caller_env::GoRuntimeState, program, socket, stylus_backend::CothreadHandler, - wasip1_stub, wavmio, Opts, + arbcompress, caller_env::GoRuntimeState, prepare::prepare_env, program, socket, + stylus_backend::CothreadHandler, wasip1_stub, wavmio, Opts, }; use arbutil::{Bytes32, Color, PreimageType}; use eyre::{bail, ErrReport, Result, WrapErr}; @@ -129,7 +129,9 @@ pub fn create(opts: &Opts, env: WasmEnv) -> (Instance, FunctionEnv, Sto "send_response" => func!(program::send_response), "create_stylus_config" => func!(program::create_stylus_config), "create_evm_data" => func!(program::create_evm_data), + "create_evm_data_v2" => func!(program::create_evm_data_v2), "activate" => func!(program::activate), + "activate_v2" => func!(program::activate_v2), }, }; @@ -213,72 +215,76 @@ pub struct WasmEnv { impl WasmEnv { pub fn cli(opts: &Opts) -> Result { - let mut env = WasmEnv::default(); - env.process.forks = opts.forks; - env.process.debug = opts.debug; + if let Some(json_inputs) = opts.json_inputs.clone() { + prepare_env(json_inputs, opts.debug) + } else { + let mut env = WasmEnv::default(); + env.process.forks = opts.forks; + env.process.debug = opts.debug; - let mut inbox_position = opts.inbox_position; - let mut delayed_position = opts.delayed_inbox_position; + let mut inbox_position = opts.inbox_position; + let mut delayed_position = opts.delayed_inbox_position; - for path in &opts.inbox { - let mut msg = vec![]; - File::open(path)?.read_to_end(&mut msg)?; - env.sequencer_messages.insert(inbox_position, msg); - inbox_position += 1; - } - for path in &opts.delayed_inbox { - let mut msg = vec![]; - File::open(path)?.read_to_end(&mut msg)?; - env.delayed_messages.insert(delayed_position, msg); - delayed_position += 1; - } + for path in &opts.inbox { + let mut msg = vec![]; + File::open(path)?.read_to_end(&mut msg)?; + env.sequencer_messages.insert(inbox_position, msg); + inbox_position += 1; + } + for path in &opts.delayed_inbox { + let mut msg = vec![]; + File::open(path)?.read_to_end(&mut msg)?; + env.delayed_messages.insert(delayed_position, msg); + delayed_position += 1; + } - if let Some(path) = &opts.preimages { - let mut file = BufReader::new(File::open(path)?); - let mut preimages = Vec::new(); - let filename = path.to_string_lossy(); - loop { - let mut size_buf = [0u8; 8]; - match file.read_exact(&mut size_buf) { - Ok(()) => {} - Err(err) if err.kind() == ErrorKind::UnexpectedEof => break, - Err(err) => bail!("Failed to parse {filename}: {}", err), + if let Some(path) = &opts.preimages { + let mut file = BufReader::new(File::open(path)?); + let mut preimages = Vec::new(); + let filename = path.to_string_lossy(); + loop { + let mut size_buf = [0u8; 8]; + match file.read_exact(&mut size_buf) { + Ok(()) => {} + Err(err) if err.kind() == ErrorKind::UnexpectedEof => break, + Err(err) => bail!("Failed to parse {filename}: {}", err), + } + let size = u64::from_le_bytes(size_buf) as usize; + let mut buf = vec![0u8; size]; + file.read_exact(&mut buf)?; + preimages.push(buf); + } + let keccak_preimages = env.preimages.entry(PreimageType::Keccak256).or_default(); + for preimage in preimages { + let mut hasher = Keccak256::new(); + hasher.update(&preimage); + let hash = hasher.finalize().into(); + keccak_preimages.insert(hash, preimage); } - let size = u64::from_le_bytes(size_buf) as usize; - let mut buf = vec![0u8; size]; - file.read_exact(&mut buf)?; - preimages.push(buf); - } - let keccak_preimages = env.preimages.entry(PreimageType::Keccak256).or_default(); - for preimage in preimages { - let mut hasher = Keccak256::new(); - hasher.update(&preimage); - let hash = hasher.finalize().into(); - keccak_preimages.insert(hash, preimage); } - } - fn parse_hex(arg: &Option, name: &str) -> Result { - match arg { - Some(arg) => { - let mut arg = arg.as_str(); - if arg.starts_with("0x") { - arg = &arg[2..]; + fn parse_hex(arg: &Option, name: &str) -> Result { + match arg { + Some(arg) => { + let mut arg = arg.as_str(); + if arg.starts_with("0x") { + arg = &arg[2..]; + } + let mut bytes32 = [0u8; 32]; + hex::decode_to_slice(arg, &mut bytes32) + .wrap_err_with(|| format!("failed to parse {} contents", name))?; + Ok(bytes32.into()) } - let mut bytes32 = [0u8; 32]; - hex::decode_to_slice(arg, &mut bytes32) - .wrap_err_with(|| format!("failed to parse {} contents", name))?; - Ok(bytes32.into()) + None => Ok(Bytes32::default()), } - None => Ok(Bytes32::default()), } - } - let last_block_hash = parse_hex(&opts.last_block_hash, "--last-block-hash")?; - let last_send_root = parse_hex(&opts.last_send_root, "--last-send-root")?; - env.small_globals = [opts.inbox_position, opts.position_within_message]; - env.large_globals = [last_block_hash, last_send_root]; - Ok(env) + let last_block_hash = parse_hex(&opts.last_block_hash, "--last-block-hash")?; + let last_send_root = parse_hex(&opts.last_send_root, "--last-send-root")?; + env.small_globals = [opts.inbox_position, opts.position_within_message]; + env.large_globals = [last_block_hash, last_send_root]; + Ok(env) + } } pub fn send_results(&mut self, error: Option, memory_used: Pages) { diff --git a/arbitrator/jit/src/main.rs b/arbitrator/jit/src/main.rs index e432dc215c..e19fabc250 100644 --- a/arbitrator/jit/src/main.rs +++ b/arbitrator/jit/src/main.rs @@ -1,53 +1,13 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -use crate::machine::{Escape, WasmEnv}; use arbutil::{color, Color}; use eyre::Result; -use std::path::PathBuf; +use jit::machine; +use jit::machine::{Escape, WasmEnv}; +use jit::Opts; use structopt::StructOpt; -mod arbcompress; -mod caller_env; -mod machine; -mod program; -mod socket; -mod stylus_backend; -mod test; -mod wasip1_stub; -mod wavmio; - -#[derive(StructOpt)] -#[structopt(name = "jit-prover")] -pub struct Opts { - #[structopt(short, long)] - binary: PathBuf, - #[structopt(long, default_value = "0")] - inbox_position: u64, - #[structopt(long, default_value = "0")] - delayed_inbox_position: u64, - #[structopt(long, default_value = "0")] - position_within_message: u64, - #[structopt(long)] - last_block_hash: Option, - #[structopt(long)] - last_send_root: Option, - #[structopt(long)] - inbox: Vec, - #[structopt(long)] - delayed_inbox: Vec, - #[structopt(long)] - preimages: Option, - #[structopt(long)] - cranelift: bool, - #[structopt(long)] - forks: bool, - #[structopt(long)] - debug: bool, - #[structopt(long)] - require_success: bool, -} - fn main() -> Result<()> { let opts = Opts::from_args(); let env = match WasmEnv::cli(&opts) { diff --git a/arbitrator/jit/src/prepare.rs b/arbitrator/jit/src/prepare.rs new file mode 100644 index 0000000000..62dd063b75 --- /dev/null +++ b/arbitrator/jit/src/prepare.rs @@ -0,0 +1,73 @@ +// Copyright 2022-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +use crate::machine::WasmEnv; +use arbutil::{Bytes32, PreimageType}; +use eyre::Ok; +use prover::parse_input::FileData; +use std::env; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; + +// local_target matches rawdb.LocalTarget() on the go side. +// While generating json_inputs file, one should make sure user_wasms map +// has entry for the system's arch that jit validation is being run on +pub fn local_target() -> String { + if env::consts::OS == "linux" { + match env::consts::ARCH { + "aarch64" => "arm64".to_string(), + "x86_64" => "amd64".to_string(), + _ => "host".to_string(), + } + } else { + "host".to_string() + } +} + +pub fn prepare_env(json_inputs: PathBuf, debug: bool) -> eyre::Result { + let file = File::open(json_inputs)?; + let reader = BufReader::new(file); + + let data = FileData::from_reader(reader)?; + + let mut env = WasmEnv::default(); + env.process.forks = false; // Should be set to false when using json_inputs + env.process.debug = debug; + + let block_hash: [u8; 32] = data.start_state.block_hash.try_into().unwrap(); + let block_hash: Bytes32 = block_hash.into(); + let send_root: [u8; 32] = data.start_state.send_root.try_into().unwrap(); + let send_root: Bytes32 = send_root.into(); + let bytes32_vals: [Bytes32; 2] = [block_hash, send_root]; + let u64_vals: [u64; 2] = [data.start_state.batch, data.start_state.pos_in_batch]; + env.small_globals = u64_vals; + env.large_globals = bytes32_vals; + + for batch_info in data.batch_info.iter() { + env.sequencer_messages + .insert(batch_info.number, batch_info.data_b64.clone()); + } + + if data.delayed_msg_nr != 0 && !data.delayed_msg_b64.is_empty() { + env.delayed_messages + .insert(data.delayed_msg_nr, data.delayed_msg_b64.clone()); + } + + for (ty, inner_map) in data.preimages_b64 { + let preimage_ty = PreimageType::try_from(ty as u8)?; + let map = env.preimages.entry(preimage_ty).or_default(); + for (hash, preimage) in inner_map { + map.insert(hash, preimage); + } + } + + if let Some(user_wasms) = data.user_wasms.get(&local_target()) { + for (module_hash, module_asm) in user_wasms.iter() { + env.module_asms + .insert(*module_hash, module_asm.as_vec().into()); + } + } + + Ok(env) +} diff --git a/arbitrator/jit/src/program.rs b/arbitrator/jit/src/program.rs index c608a3cf85..d80b3771c6 100644 --- a/arbitrator/jit/src/program.rs +++ b/arbitrator/jit/src/program.rs @@ -4,8 +4,9 @@ #![allow(clippy::too_many_arguments)] use crate::caller_env::JitEnv; -use crate::machine::{Escape, MaybeEscape, WasmEnvMut}; -use crate::stylus_backend::exec_wasm; +use crate::machine::{Escape, MaybeEscape, WasmEnv, WasmEnvMut}; +use crate::stylus_backend::{exec_wasm, MessageFromCothread}; +use arbutil::evm::api::Gas; use arbutil::Bytes32; use arbutil::{evm::EvmData, format::DebugBytes, heapify}; use caller_env::{GuestPtr, MemAccess}; @@ -15,9 +16,47 @@ use prover::{ machine::Module, programs::{config::PricingParams, prelude::*}, }; +use std::sync::Arc; + +const DEFAULT_STYLUS_ARBOS_VERSION: u64 = 31; -/// activates a user program pub fn activate( + env: WasmEnvMut, + wasm_ptr: GuestPtr, + wasm_size: u32, + pages_ptr: GuestPtr, + asm_estimate_ptr: GuestPtr, + init_cost_ptr: GuestPtr, + cached_init_cost_ptr: GuestPtr, + stylus_version: u16, + debug: u32, + codehash: GuestPtr, + module_hash_ptr: GuestPtr, + gas_ptr: GuestPtr, + err_buf: GuestPtr, + err_buf_len: u32, +) -> Result { + activate_v2( + env, + wasm_ptr, + wasm_size, + pages_ptr, + asm_estimate_ptr, + init_cost_ptr, + cached_init_cost_ptr, + stylus_version, + DEFAULT_STYLUS_ARBOS_VERSION, + debug, + codehash, + module_hash_ptr, + gas_ptr, + err_buf, + err_buf_len, + ) +} + +/// activates a user program +pub fn activate_v2( mut env: WasmEnvMut, wasm_ptr: GuestPtr, wasm_size: u32, @@ -25,7 +64,8 @@ pub fn activate( asm_estimate_ptr: GuestPtr, init_cost_ptr: GuestPtr, cached_init_cost_ptr: GuestPtr, - version: u16, + stylus_version: u16, + arbos_version_for_gas: u64, debug: u32, codehash: GuestPtr, module_hash_ptr: GuestPtr, @@ -40,7 +80,15 @@ pub fn activate( let page_limit = mem.read_u16(pages_ptr); let gas_left = &mut mem.read_u64(gas_ptr); - match Module::activate(&wasm, codehash, version, page_limit, debug, gas_left) { + match Module::activate( + &wasm, + codehash, + stylus_version, + arbos_version_for_gas, + page_limit, + debug, + gas_left, + ) { Ok((module, data)) => { mem.write_u64(gas_ptr, *gas_left); mem.write_u16(pages_ptr, data.footprint); @@ -83,10 +131,6 @@ pub fn new_program( let evm_data: EvmData = unsafe { *Box::from_raw(evm_data_handler as *mut EvmData) }; let config: JitConfig = unsafe { *Box::from_raw(stylus_config_handler as *mut JitConfig) }; - // buy ink - let pricing = config.stylus.pricing; - let ink = pricing.gas_to_ink(gas); - let Some(module) = exec.module_asms.get(&compiled_hash).cloned() else { return Err(Escape::Failure(format!( "module hash {:?} not found in {:?}", @@ -95,6 +139,21 @@ pub fn new_program( ))); }; + exec_program(exec, module, calldata, config, evm_data, gas) +} + +pub fn exec_program( + exec: &mut WasmEnv, + module: Arc<[u8]>, + calldata: Vec, + config: JitConfig, + evm_data: EvmData, + gas: u64, +) -> Result { + // buy ink + let pricing = config.stylus.pricing; + let ink = pricing.gas_to_ink(Gas(gas)); + let cothread = exec_wasm( module, calldata, @@ -115,7 +174,10 @@ pub fn new_program( /// returns request_id for the first request from the program pub fn start_program(mut env: WasmEnvMut, module: u32) -> Result { let (_, exec) = env.jit_env(); + start_program_with_wasm_env(exec, module) +} +pub fn start_program_with_wasm_env(exec: &mut WasmEnv, module: u32) -> Result { if exec.threads.len() as u32 != module || module == 0 { return Escape::hostio(format!( "got request for thread {module} but len is {}", @@ -132,13 +194,18 @@ pub fn start_program(mut env: WasmEnvMut, module: u32) -> Result { /// request_id MUST be last request id returned from start_program or send_response pub fn get_request(mut env: WasmEnvMut, id: u32, len_ptr: GuestPtr) -> Result { let (mut mem, exec) = env.jit_env(); + let msg = get_last_msg(exec, id)?; + mem.write_u32(len_ptr, msg.req_data.len() as u32); + Ok(msg.req_type) +} + +pub fn get_last_msg(exec: &mut WasmEnv, id: u32) -> Result { let thread = exec.threads.last_mut().unwrap(); let msg = thread.last_message()?; if msg.1 != id { return Escape::hostio("get_request id doesn't match"); }; - mem.write_u32(len_ptr, msg.0.req_data.len() as u32); - Ok(msg.0.req_type) + Ok(msg.0) } // gets data associated with last request. @@ -146,12 +213,8 @@ pub fn get_request(mut env: WasmEnvMut, id: u32, len_ptr: GuestPtr) -> Result MaybeEscape { let (mut mem, exec) = env.jit_env(); - let thread = exec.threads.last_mut().unwrap(); - let msg = thread.last_message()?; - if msg.1 != id { - return Escape::hostio("get_request id doesn't match"); - }; - mem.write_slice(data_ptr, &msg.0.req_data); + let msg = get_last_msg(exec, id)?; + mem.write_slice(data_ptr, &msg.req_data); Ok(()) } @@ -170,11 +233,21 @@ pub fn set_response( let result = mem.read_slice(result_ptr, result_len as usize); let raw_data = mem.read_slice(raw_data_ptr, raw_data_len as usize); + set_response_with_wasm_env(exec, id, gas, result, raw_data) +} + +pub fn set_response_with_wasm_env( + exec: &mut WasmEnv, + id: u32, + gas: u64, + result: Vec, + raw_data: Vec, +) -> MaybeEscape { let thread = exec.threads.last_mut().unwrap(); - thread.set_response(id, result, raw_data, gas) + thread.set_response(id, result, raw_data, Gas(gas)) } -/// sends previos response +/// sends previous response /// MUST be called right after set_response to the same id /// returns request_id for the next request pub fn send_response(mut env: WasmEnvMut, req_id: u32) -> Result { @@ -192,7 +265,10 @@ pub fn send_response(mut env: WasmEnvMut, req_id: u32) -> Result { /// removes the last created program pub fn pop(mut env: WasmEnvMut) -> MaybeEscape { let (_, exec) = env.jit_env(); + pop_with_wasm_env(exec) +} +pub fn pop_with_wasm_env(exec: &mut WasmEnv) -> MaybeEscape { match exec.threads.pop() { None => Err(Escape::Child(eyre!("no child"))), Some(mut thread) => thread.wait_done(), @@ -200,8 +276,8 @@ pub fn pop(mut env: WasmEnvMut) -> MaybeEscape { } pub struct JitConfig { - stylus: StylusConfig, - compile: CompileConfig, + pub stylus: StylusConfig, + pub compile: CompileConfig, } /// Creates a `StylusConfig` from its component parts. @@ -222,9 +298,47 @@ pub fn create_stylus_config( Ok(res as u64) } -/// Creates an `EvmData` handler from its component parts. pub fn create_evm_data( + env: WasmEnvMut, + block_basefee_ptr: GuestPtr, + chainid: u64, + block_coinbase_ptr: GuestPtr, + block_gas_limit: u64, + block_number: u64, + block_timestamp: u64, + contract_address_ptr: GuestPtr, + module_hash_ptr: GuestPtr, + msg_sender_ptr: GuestPtr, + msg_value_ptr: GuestPtr, + tx_gas_price_ptr: GuestPtr, + tx_origin_ptr: GuestPtr, + cached: u32, + reentrant: u32, +) -> Result { + create_evm_data_v2( + env, + DEFAULT_STYLUS_ARBOS_VERSION, + block_basefee_ptr, + chainid, + block_coinbase_ptr, + block_gas_limit, + block_number, + block_timestamp, + contract_address_ptr, + module_hash_ptr, + msg_sender_ptr, + msg_value_ptr, + tx_gas_price_ptr, + tx_origin_ptr, + cached, + reentrant, + ) +} + +/// Creates an `EvmData` handler from its component parts. +pub fn create_evm_data_v2( mut env: WasmEnvMut, + arbos_version: u64, block_basefee_ptr: GuestPtr, chainid: u64, block_coinbase_ptr: GuestPtr, @@ -243,6 +357,7 @@ pub fn create_evm_data( let (mut mem, _) = env.jit_env(); let evm_data = EvmData { + arbos_version, block_basefee: mem.read_bytes32(block_basefee_ptr), cached: cached != 0, chainid, diff --git a/arbitrator/jit/src/stylus_backend.rs b/arbitrator/jit/src/stylus_backend.rs index 61dbf258d4..d250780dd9 100644 --- a/arbitrator/jit/src/stylus_backend.rs +++ b/arbitrator/jit/src/stylus_backend.rs @@ -4,7 +4,8 @@ #![allow(clippy::too_many_arguments)] use crate::machine::{Escape, MaybeEscape}; -use arbutil::evm::api::VecReader; +use arbutil::benchmark::Benchmark; +use arbutil::evm::api::{Gas, Ink, VecReader}; use arbutil::evm::{ api::{EvmApiMethod, EVM_API_METHOD_REQ_OFFSET}, req::EvmApiRequestor, @@ -28,13 +29,14 @@ use stylus::{native::NativeInstance, run::RunProgram}; struct MessageToCothread { result: Vec, raw_data: Vec, - cost: u64, + cost: Gas, } #[derive(Clone)] pub struct MessageFromCothread { pub req_type: u32, pub req_data: Vec, + pub benchmark: Benchmark, } struct CothreadRequestor { @@ -47,10 +49,11 @@ impl RequestHandler for CothreadRequestor { &mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>, - ) -> (Vec, VecReader, u64) { + ) -> (Vec, VecReader, Gas) { let msg = MessageFromCothread { req_type: req_type as u32 + EVM_API_METHOD_REQ_OFFSET, req_data: req_data.as_ref().to_vec(), + benchmark: Benchmark::default(), }; if let Err(error) = self.tx.send(msg) { @@ -104,7 +107,7 @@ impl CothreadHandler { id: u32, result: Vec, raw_data: Vec, - cost: u64, + cost: Gas, ) -> MaybeEscape { let Some(msg) = self.last_request.clone() else { return Escape::hostio("trying to set response but no message pending"); @@ -131,7 +134,7 @@ pub fn exec_wasm( compile: CompileConfig, config: StylusConfig, evm_data: EvmData, - ink: u64, + ink: Ink, ) -> Result { let (tothread_tx, tothread_rx) = mpsc::sync_channel::(0); let (fromthread_tx, fromthread_rx) = mpsc::sync_channel::(0); @@ -150,7 +153,7 @@ pub fn exec_wasm( let outcome = instance.run_main(&calldata, config, ink); let ink_left = match outcome.as_ref() { - Ok(UserOutcome::OutOfStack) => 0, // take all ink when out of stack + Ok(UserOutcome::OutOfStack) => Ink(0), // take all ink when out of stack _ => instance.ink_left().into(), }; @@ -169,6 +172,7 @@ pub fn exec_wasm( let msg = MessageFromCothread { req_data: output, req_type: out_kind as u32, + benchmark: instance.env().benchmark, }; instance .env_mut() diff --git a/arbitrator/jit/src/wavmio.rs b/arbitrator/jit/src/wavmio.rs index 062d18d8e9..0ca666d3b2 100644 --- a/arbitrator/jit/src/wavmio.rs +++ b/arbitrator/jit/src/wavmio.rs @@ -8,8 +8,6 @@ use crate::{ }; use arbutil::{Color, PreimageType}; use caller_env::{GuestPtr, MemAccess}; -use sha2::Sha256; -use sha3::{Digest, Keccak256}; use std::{ io, io::{BufReader, BufWriter, ErrorKind}, @@ -170,19 +168,25 @@ pub fn resolve_preimage_impl( error!("Missing requested preimage for hash {hash_hex} in {name}") }; - // Check if preimage rehashes to the provided hash. Exclude blob preimages - let calculated_hash: [u8; 32] = match preimage_type { - PreimageType::Keccak256 => Keccak256::digest(preimage).into(), - PreimageType::Sha2_256 => Sha256::digest(preimage).into(), - PreimageType::EthVersionedHash => *hash, - }; - if calculated_hash != *hash { - error!( - "Calculated hash {} of preimage {} does not match provided hash {}", - hex::encode(calculated_hash), - hex::encode(preimage), - hex::encode(*hash) - ); + #[cfg(debug_assertions)] + { + use sha2::Sha256; + use sha3::{Digest, Keccak256}; + + // Check if preimage rehashes to the provided hash. Exclude blob preimages + let calculated_hash: [u8; 32] = match preimage_type { + PreimageType::Keccak256 => Keccak256::digest(preimage).into(), + PreimageType::Sha2_256 => Sha256::digest(preimage).into(), + PreimageType::EthVersionedHash => *hash, + }; + if calculated_hash != *hash { + error!( + "Calculated hash {} of preimage {} does not match provided hash {}", + hex::encode(calculated_hash), + hex::encode(preimage), + hex::encode(*hash) + ); + } } if offset % 32 != 0 { diff --git a/arbitrator/langs/bf b/arbitrator/langs/bf index cb5750580f..92420f8f34 160000 --- a/arbitrator/langs/bf +++ b/arbitrator/langs/bf @@ -1 +1 @@ -Subproject commit cb5750580f6990b5166ffce83de11b766a25ca31 +Subproject commit 92420f8f34b53f3c1d47047f9f894820d506c565 diff --git a/arbitrator/prover/Cargo.toml b/arbitrator/prover/Cargo.toml index 5475647765..da329b1cb5 100644 --- a/arbitrator/prover/Cargo.toml +++ b/arbitrator/prover/Cargo.toml @@ -19,10 +19,10 @@ num = "0.4" rustc-demangle = "0.1.21" serde = { version = "1.0.130", features = ["derive", "rc"] } serde_json = "1.0.67" +serde_with = { version = "3.8.1", features = ["base64"] } sha3 = "0.9.1" static_assertions = "1.1.0" structopt = "0.3.23" -serde_with = "1.12.1" parking_lot = "0.12.1" lazy_static.workspace = true itertools = "0.10.5" diff --git a/arbitrator/prover/src/binary.rs b/arbitrator/prover/src/binary.rs index aa5537476c..2260f6bf48 100644 --- a/arbitrator/prover/src/binary.rs +++ b/arbitrator/prover/src/binary.rs @@ -9,7 +9,9 @@ use crate::{ }, value::{ArbValueType, FunctionType, IntegerValType, Value}, }; -use arbutil::{math::SaturatingSum, Bytes32, Color, DebugColor}; +use arbutil::{ + evm::ARBOS_VERSION_STYLUS_CHARGING_FIXES, math::SaturatingSum, Bytes32, Color, DebugColor, +}; use eyre::{bail, ensure, eyre, Result, WrapErr}; use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet}; use nom::{ @@ -641,6 +643,7 @@ impl<'a> WasmBinary<'a> { /// Parses and instruments a user wasm pub fn parse_user( wasm: &'a [u8], + arbos_version_for_gas: u64, page_limit: u16, compile: &CompileConfig, codehash: &Bytes32, @@ -678,6 +681,10 @@ impl<'a> WasmBinary<'a> { limit!(65536, code.expr.len(), "opcodes in func body"); } + if arbos_version_for_gas >= ARBOS_VERSION_STYLUS_CHARGING_FIXES { + limit!(513, bin.imports.len(), "imports") + } + let table_entries = bin.tables.iter().map(|x| x.initial).saturating_sum(); limit!(4096, table_entries, "table entries"); diff --git a/arbitrator/prover/src/lib.rs b/arbitrator/prover/src/lib.rs index 0f537478eb..a147786086 100644 --- a/arbitrator/prover/src/lib.rs +++ b/arbitrator/prover/src/lib.rs @@ -11,6 +11,8 @@ pub mod machine; /// cbindgen:ignore pub mod memory; pub mod merkle; +pub mod parse_input; +pub mod prepare; mod print; pub mod programs; mod reinterpret; @@ -34,6 +36,7 @@ use once_cell::sync::OnceCell; use static_assertions::const_assert_eq; use std::{ ffi::CStr, + marker::PhantomData, num::NonZeroUsize, os::raw::{c_char, c_int}, path::Path, @@ -57,11 +60,67 @@ pub struct CByteArray { } #[repr(C)] -#[derive(Clone, Copy)] -pub struct RustByteArray { +pub struct RustSlice<'a> { + pub ptr: *const u8, + pub len: usize, + pub phantom: PhantomData<&'a [u8]>, +} + +impl<'a> RustSlice<'a> { + pub fn new(slice: &'a [u8]) -> Self { + if slice.is_empty() { + return Self { + ptr: ptr::null(), + len: 0, + phantom: PhantomData, + }; + } + Self { + ptr: slice.as_ptr(), + len: slice.len(), + phantom: PhantomData, + } + } +} + +#[repr(C)] +pub struct RustBytes { pub ptr: *mut u8, pub len: usize, - pub capacity: usize, + pub cap: usize, +} + +impl RustBytes { + pub unsafe fn into_vec(self) -> Vec { + Vec::from_raw_parts(self.ptr, self.len, self.cap) + } + + pub unsafe fn write(&mut self, mut vec: Vec) { + if vec.capacity() == 0 { + *self = RustBytes { + ptr: ptr::null_mut(), + len: 0, + cap: 0, + }; + return; + } + self.ptr = vec.as_mut_ptr(); + self.len = vec.len(); + self.cap = vec.capacity(); + std::mem::forget(vec); + } +} + +/// Frees the vector. Does nothing when the vector is null. +/// +/// # Safety +/// +/// Must only be called once per vec. +#[no_mangle] +pub unsafe extern "C" fn free_rust_bytes(vec: RustBytes) { + if !vec.ptr.is_null() { + drop(vec.into_vec()) + } } #[no_mangle] @@ -125,6 +184,12 @@ pub unsafe extern "C" fn arbitrator_load_wavm_binary(binary_path: *const c_char) } } +#[no_mangle] +#[cfg(feature = "native")] +pub unsafe extern "C" fn arbitrator_new_finished(gs: GlobalState) -> *mut Machine { + Box::into_raw(Box::new(Machine::new_finished(gs))) +} + unsafe fn cstr_to_string(c_str: *const c_char) -> String { CStr::from_ptr(c_str).to_string_lossy().into_owned() } @@ -402,18 +467,6 @@ pub unsafe extern "C" fn arbitrator_module_root(mach: *mut Machine) -> Bytes32 { #[no_mangle] #[cfg(feature = "native")] -pub unsafe extern "C" fn arbitrator_gen_proof(mach: *mut Machine) -> RustByteArray { - let mut proof = (*mach).serialize_proof(); - let ret = RustByteArray { - ptr: proof.as_mut_ptr(), - len: proof.len(), - capacity: proof.capacity(), - }; - std::mem::forget(proof); - ret -} - -#[no_mangle] -pub unsafe extern "C" fn arbitrator_free_proof(proof: RustByteArray) { - drop(Vec::from_raw_parts(proof.ptr, proof.len, proof.capacity)) +pub unsafe extern "C" fn arbitrator_gen_proof(mach: *mut Machine, out: *mut RustBytes) { + (*out).write((*mach).serialize_proof()); } diff --git a/arbitrator/prover/src/machine.rs b/arbitrator/prover/src/machine.rs index 358876bd25..0d39d87e77 100644 --- a/arbitrator/prover/src/machine.rs +++ b/arbitrator/prover/src/machine.rs @@ -371,13 +371,16 @@ impl Module { for import in &bin.imports { let module = import.module; let have_ty = &bin.types[import.offset as usize]; - let (forward, import_name) = match import.name.strip_prefix(Module::FORWARDING_PREFIX) { - Some(name) => (true, name), - None => (false, import.name), - }; + // allow_hostapi is only set for system modules like the + // forwarder. We restrict stripping the prefix for user modules. + let (forward, import_name) = + if allow_hostapi && import.name.starts_with(Self::FORWARDING_PREFIX) { + (true, &import.name[Self::FORWARDING_PREFIX.len()..]) + } else { + (false, import.name) + }; - let mut qualified_name = format!("{module}__{import_name}"); - qualified_name = qualified_name.replace(&['/', '.', '-'] as &[char], "_"); + let qualified_name = format!("{module}__{import_name}"); let func = if let Some(import) = available_imports.get(&qualified_name) { let call = match forward { @@ -1562,6 +1565,36 @@ impl Machine { Ok(mach) } + // new_finished returns a Machine in the Finished state at step 0. + // + // This allows the Mahine to be set up to model the final state of the + // machine at the end of the execution of a block. + pub fn new_finished(gs: GlobalState) -> Machine { + Machine { + steps: 0, + status: MachineStatus::Finished, + global_state: gs, + // The machine is in the Finished state, so nothing else really matters. + // values_stacks and frame_stacks cannot be empty for proof serialization, + // but everything else can just be entirely blank. + thread_state: ThreadState::Main, + value_stacks: vec![Vec::new()], + frame_stacks: vec![Vec::new()], + internal_stack: Default::default(), + modules: Default::default(), + modules_merkle: Default::default(), + pc: Default::default(), + stdio_output: Default::default(), + inbox_contents: Default::default(), + first_too_far: Default::default(), + preimage_resolver: PreimageResolverWrapper::new(Arc::new(|_, _, _| None)), + stylus_modules: Default::default(), + initial_hash: Default::default(), + context: Default::default(), + debug_info: Default::default(), + } + } + pub fn new_from_wavm(wavm_binary: &Path) -> Result { let mut modules: Vec = { let compressed = std::fs::read(wavm_binary)?; @@ -1813,7 +1846,12 @@ impl Machine { } #[cfg(feature = "native")] - pub fn call_user_func(&mut self, func: &str, args: Vec, ink: u64) -> Result> { + pub fn call_user_func( + &mut self, + func: &str, + args: Vec, + ink: arbutil::evm::api::Ink, + ) -> Result> { self.set_ink(ink); self.call_function("user", func, args) } @@ -2859,6 +2897,15 @@ impl Machine { let mod_merkle = self.get_modules_merkle(); out!(mod_merkle.root()); + if self.is_halted() { + // If the machine is halted, instead of serializing the module, + // serialize the global state and return. + // This is for the "kickstart" BoLD proof, but it's backwards compatible + // with the old OSP behavior which reads no further. + out!(self.global_state.serialize()); + return data; + } + // End machine serialization, serialize module let module = &self.modules[self.pc.module()]; diff --git a/arbitrator/prover/src/main.rs b/arbitrator/prover/src/main.rs index dba32e0e72..a889cc60f3 100644 --- a/arbitrator/prover/src/main.rs +++ b/arbitrator/prover/src/main.rs @@ -8,6 +8,7 @@ use eyre::{eyre, Context, Result}; use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet}; use prover::{ machine::{GlobalState, InboxIdentifier, Machine, MachineStatus, PreimageResolver, ProofInfo}, + prepare::prepare_machine, utils::{file_bytes, hash_preimage, CBytes}, wavm::Opcode, }; @@ -86,6 +87,10 @@ struct Opts { skip_until_host_io: bool, #[structopt(long)] max_steps: Option, + // JSON inputs supercede any of the command-line inputs which could + // be specified in the JSON file. + #[structopt(long)] + json_inputs: Option, } fn file_with_stub_header(path: &Path, headerlength: usize) -> Result> { @@ -135,83 +140,8 @@ fn main() -> Result<()> { } } } - let mut inbox_contents = HashMap::default(); - let mut inbox_position = opts.inbox_position; - let mut delayed_position = opts.delayed_inbox_position; - let inbox_header_len; - let delayed_header_len; - if opts.inbox_add_stub_headers { - inbox_header_len = INBOX_HEADER_LEN; - delayed_header_len = DELAYED_HEADER_LEN + 1; - } else { - inbox_header_len = 0; - delayed_header_len = 0; - } - - for path in opts.inbox { - inbox_contents.insert( - (InboxIdentifier::Sequencer, inbox_position), - file_with_stub_header(&path, inbox_header_len)?, - ); - println!("read file {:?} to seq. inbox {}", &path, inbox_position); - inbox_position += 1; - } - for path in opts.delayed_inbox { - inbox_contents.insert( - (InboxIdentifier::Delayed, delayed_position), - file_with_stub_header(&path, delayed_header_len)?, - ); - delayed_position += 1; - } - let mut preimages: HashMap> = HashMap::default(); - if let Some(path) = opts.preimages { - let mut file = BufReader::new(File::open(path)?); - loop { - let mut ty_buf = [0u8; 1]; - match file.read_exact(&mut ty_buf) { - Ok(()) => {} - Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, - Err(e) => return Err(e.into()), - } - let preimage_ty: PreimageType = ty_buf[0].try_into()?; - - let mut size_buf = [0u8; 8]; - file.read_exact(&mut size_buf)?; - let size = u64::from_le_bytes(size_buf) as usize; - let mut buf = vec![0u8; size]; - file.read_exact(&mut buf)?; - - let hash = hash_preimage(&buf, preimage_ty)?; - preimages - .entry(preimage_ty) - .or_default() - .insert(hash.into(), buf.as_slice().into()); - } - } - let preimage_resolver = - Arc::new(move |_, ty, hash| preimages.get(&ty).and_then(|m| m.get(&hash)).cloned()) - as PreimageResolver; - - let last_block_hash = decode_hex_arg(&opts.last_block_hash, "--last-block-hash")?; - let last_send_root = decode_hex_arg(&opts.last_send_root, "--last-send-root")?; - - let global_state = GlobalState { - u64_vals: [opts.inbox_position, opts.position_within_message], - bytes32_vals: [last_block_hash, last_send_root], - }; - - let mut mach = Machine::from_paths( - &opts.libraries, - &opts.binary, - true, - opts.allow_hostapi, - opts.debug_funcs, - true, - global_state, - inbox_contents, - preimage_resolver, - )?; + let mut mach = initialize_machine(&opts)?; for path in &opts.stylus_modules { let err = || eyre!("failed to read module at {}", path.to_string_lossy().red()); @@ -414,6 +344,13 @@ fn main() -> Result<()> { }); } + println!( + "End GlobalState:\n BlockHash: {:?}\n SendRoot: {:?}\n Batch: {}\n PosInBatch: {}", + mach.get_global_state().bytes32_vals[0], + mach.get_global_state().bytes32_vals[1], + mach.get_global_state().u64_vals[0], + mach.get_global_state().u64_vals[1] + ); println!("End machine status: {:?}", mach.get_status()); println!("End machine hash: {}", mach.hash()); println!("End machine stack: {:?}", mach.get_data_stack()); @@ -462,7 +399,6 @@ fn main() -> Result<()> { } } } - let opts_binary = opts.binary; let opts_libraries = opts.libraries; let format_pc = |module_num: usize, func_num: usize| -> (String, String) { @@ -543,3 +479,87 @@ fn main() -> Result<()> { } Ok(()) } + +fn initialize_machine(opts: &Opts) -> eyre::Result { + if let Some(json_inputs) = opts.json_inputs.clone() { + prepare_machine(json_inputs, opts.binary.clone()) + } else { + let mut inbox_contents = HashMap::default(); + let mut inbox_position = opts.inbox_position; + let mut delayed_position = opts.delayed_inbox_position; + let inbox_header_len; + let delayed_header_len; + if opts.inbox_add_stub_headers { + inbox_header_len = INBOX_HEADER_LEN; + delayed_header_len = DELAYED_HEADER_LEN + 1; + } else { + inbox_header_len = 0; + delayed_header_len = 0; + } + + for path in opts.inbox.clone() { + inbox_contents.insert( + (InboxIdentifier::Sequencer, inbox_position), + file_with_stub_header(&path, inbox_header_len)?, + ); + println!("read file {:?} to seq. inbox {}", &path, inbox_position); + inbox_position += 1; + } + for path in opts.delayed_inbox.clone() { + inbox_contents.insert( + (InboxIdentifier::Delayed, delayed_position), + file_with_stub_header(&path, delayed_header_len)?, + ); + delayed_position += 1; + } + + let mut preimages: HashMap> = HashMap::default(); + if let Some(path) = opts.preimages.clone() { + let mut file = BufReader::new(File::open(path)?); + loop { + let mut ty_buf = [0u8; 1]; + match file.read_exact(&mut ty_buf) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e.into()), + } + let preimage_ty: PreimageType = ty_buf[0].try_into()?; + + let mut size_buf = [0u8; 8]; + file.read_exact(&mut size_buf)?; + let size = u64::from_le_bytes(size_buf) as usize; + let mut buf = vec![0u8; size]; + file.read_exact(&mut buf)?; + + let hash = hash_preimage(&buf, preimage_ty)?; + preimages + .entry(preimage_ty) + .or_default() + .insert(hash.into(), buf.as_slice().into()); + } + } + let preimage_resolver = + Arc::new(move |_, ty, hash| preimages.get(&ty).and_then(|m| m.get(&hash)).cloned()) + as PreimageResolver; + + let last_block_hash = decode_hex_arg(&opts.last_block_hash, "--last-block-hash")?; + let last_send_root = decode_hex_arg(&opts.last_send_root, "--last-send-root")?; + + let global_state = GlobalState { + u64_vals: [opts.inbox_position, opts.position_within_message], + bytes32_vals: [last_block_hash, last_send_root], + }; + + Machine::from_paths( + &opts.libraries, + &opts.binary, + true, + opts.allow_hostapi, + opts.debug_funcs, + true, + global_state, + inbox_contents, + preimage_resolver, + ) + } +} diff --git a/arbitrator/prover/src/merkle.rs b/arbitrator/prover/src/merkle.rs index 4a1278b4cb..fbd704dfc6 100644 --- a/arbitrator/prover/src/merkle.rs +++ b/arbitrator/prover/src/merkle.rs @@ -549,8 +549,7 @@ mod test { let mut empty_node = Bytes32([ 57, 29, 211, 154, 252, 227, 18, 99, 65, 126, 203, 166, 252, 232, 32, 3, 98, 194, 254, 186, 118, 14, 139, 192, 101, 156, 55, 194, 101, 11, 11, 168, - ]) - .clone(); + ]); for _ in 0..64 { print!("Bytes32(["); for i in 0..32 { @@ -607,7 +606,7 @@ mod test { for layer in 0..64 { // empty_hash_at is just a lookup, but empty_hash is calculated iteratively. assert_eq!(empty_hash_at(ty, layer), &empty_hash); - empty_hash = hash_node(ty, &empty_hash, &empty_hash); + empty_hash = hash_node(ty, empty_hash, empty_hash); } } } diff --git a/arbitrator/prover/src/parse_input.rs b/arbitrator/prover/src/parse_input.rs new file mode 100644 index 0000000000..fa7adb4c41 --- /dev/null +++ b/arbitrator/prover/src/parse_input.rs @@ -0,0 +1,112 @@ +use arbutil::Bytes32; +use serde::Deserialize; +use serde_json; +use serde_with::base64::Base64; +use serde_with::As; +use serde_with::DisplayFromStr; +use std::{ + collections::HashMap, + io::{self, BufRead}, +}; + +/// prefixed_hex deserializes hex strings which are prefixed with `0x` +/// +/// The default hex deserializer does not support prefixed hex strings. +/// +/// It is an error to use this deserializer on a string that does not +/// begin with `0x`. +mod prefixed_hex { + use serde::{self, Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if let Some(s) = s.strip_prefix("0x") { + hex::decode(s).map_err(serde::de::Error::custom) + } else { + Err(serde::de::Error::custom("missing 0x prefix")) + } + } +} + +#[derive(Debug)] +pub struct UserWasm(Vec); + +/// UserWasm is a wrapper around Vec +/// +/// It is useful for decompressing a brotli-compressed wasm module. +/// +/// Note: The wrapped Vec is already Base64 decoded before +/// from(Vec) is called by serde. +impl UserWasm { + /// as_vec returns the decompressed wasm module as a Vec + pub fn as_vec(&self) -> Vec { + self.0.clone() + } +} + +impl AsRef<[u8]> for UserWasm { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// The Vec is compressed using brotli, and must be decompressed before use. +impl From> for UserWasm { + fn from(data: Vec) -> Self { + let decompressed = brotli::decompress(&data, brotli::Dictionary::Empty).unwrap(); + Self(decompressed) + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct BatchInfo { + pub number: u64, + #[serde(with = "As::")] + pub data_b64: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct StartState { + #[serde(with = "prefixed_hex")] + pub block_hash: Vec, + #[serde(with = "prefixed_hex")] + pub send_root: Vec, + pub batch: u64, + pub pos_in_batch: u64, +} + +/// FileData is the deserialized form of the input JSON file. +/// +/// The go JSON library in json.go uses some custom serialization and +/// compression logic that needs to be reversed when deserializing the +/// JSON in rust. +/// +/// Note: It is important to change this file whenever the go JSON +/// serialization changes. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct FileData { + pub id: u64, + pub has_delayed_msg: bool, + pub delayed_msg_nr: u64, + #[serde(with = "As::>>")] + pub preimages_b64: HashMap>>, + pub batch_info: Vec, + #[serde(with = "As::")] + pub delayed_msg_b64: Vec, + pub start_state: StartState, + #[serde(with = "As::>>")] + pub user_wasms: HashMap>, +} + +impl FileData { + pub fn from_reader(mut reader: R) -> io::Result { + let data = serde_json::from_reader(&mut reader)?; + Ok(data) + } +} diff --git a/arbitrator/bench/src/prepare.rs b/arbitrator/prover/src/prepare.rs similarity index 85% rename from arbitrator/bench/src/prepare.rs rename to arbitrator/prover/src/prepare.rs index 741a7350ac..a485267f39 100644 --- a/arbitrator/bench/src/prepare.rs +++ b/arbitrator/prover/src/prepare.rs @@ -1,13 +1,13 @@ use arbutil::{Bytes32, PreimageType}; -use prover::machine::{argument_data_to_inbox, GlobalState, Machine}; -use prover::utils::CBytes; use std::collections::HashMap; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::machine::{argument_data_to_inbox, GlobalState, Machine}; use crate::parse_input::*; +use crate::utils::CBytes; pub fn prepare_machine(preimages: PathBuf, machines: PathBuf) -> eyre::Result { let file = File::open(preimages)?; @@ -40,6 +40,15 @@ pub fn prepare_machine(preimages: PathBuf, machines: PathBuf) -> eyre::Result u64 { - gas.saturating_mul(self.ink_price.into()) + pub fn gas_to_ink(&self, gas: Gas) -> Ink { + Ink(gas.0.saturating_mul(self.ink_price.into())) } - pub fn ink_to_gas(&self, ink: u64) -> u64 { - ink / self.ink_price as u64 // never 0 + pub fn ink_to_gas(&self, ink: Ink) -> Gas { + Gas(ink.0 / self.ink_price as u64) // ink_price is never 0 } } diff --git a/arbitrator/prover/src/programs/memory.rs b/arbitrator/prover/src/programs/memory.rs index 7253b59dc4..82c4d4469d 100644 --- a/arbitrator/prover/src/programs/memory.rs +++ b/arbitrator/prover/src/programs/memory.rs @@ -1,6 +1,8 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +use arbutil::evm::api::Gas; + #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct MemoryModel { @@ -28,20 +30,20 @@ impl MemoryModel { } /// Determines the gas cost of allocating `new` pages given `open` are active and `ever` have ever been. - pub fn gas_cost(&self, new: u16, open: u16, ever: u16) -> u64 { + pub fn gas_cost(&self, new: u16, open: u16, ever: u16) -> Gas { let new_open = open.saturating_add(new); let new_ever = ever.max(new_open); // free until expansion beyond the first few if new_ever <= self.free_pages { - return 0; + return Gas(0); } let credit = |pages: u16| pages.saturating_sub(self.free_pages); let adding = credit(new_open).saturating_sub(credit(open)) as u64; let linear = adding.saturating_mul(self.page_gas.into()); let expand = Self::exp(new_ever) - Self::exp(ever); - linear.saturating_add(expand) + Gas(linear.saturating_add(expand)) } fn exp(pages: u16) -> u64 { @@ -81,14 +83,14 @@ fn test_model() { let model = MemoryModel::new(2, 1000); for jump in 1..=128 { - let mut total = 0; + let mut total = Gas(0); let mut pages = 0; while pages < 128 { let jump = jump.min(128 - pages); total += model.gas_cost(jump, pages, pages); pages += jump; } - assert_eq!(total, 31999998); + assert_eq!(total, Gas(31999998)); } for jump in 1..=128 { @@ -98,7 +100,7 @@ fn test_model() { let mut adds = 0; while ever < 128 { let jump = jump.min(128 - open); - total += model.gas_cost(jump, open, ever); + total += model.gas_cost(jump, open, ever).0; open += jump; ever = ever.max(open); @@ -114,12 +116,12 @@ fn test_model() { } // check saturation - assert_eq!(u64::MAX, model.gas_cost(129, 0, 0)); - assert_eq!(u64::MAX, model.gas_cost(u16::MAX, 0, 0)); + assert_eq!(Gas(u64::MAX), model.gas_cost(129, 0, 0)); + assert_eq!(Gas(u64::MAX), model.gas_cost(u16::MAX, 0, 0)); // check free pages let model = MemoryModel::new(128, 1000); - assert_eq!(0, model.gas_cost(128, 0, 0)); - assert_eq!(0, model.gas_cost(128, 0, 128)); - assert_eq!(u64::MAX, model.gas_cost(129, 0, 0)); + assert_eq!(Gas(0), model.gas_cost(128, 0, 0)); + assert_eq!(Gas(0), model.gas_cost(128, 0, 128)); + assert_eq!(Gas(u64::MAX), model.gas_cost(129, 0, 0)); } diff --git a/arbitrator/prover/src/programs/meter.rs b/arbitrator/prover/src/programs/meter.rs index ab069fd911..0d7b3151d7 100644 --- a/arbitrator/prover/src/programs/meter.rs +++ b/arbitrator/prover/src/programs/meter.rs @@ -9,7 +9,14 @@ use crate::{ value::FunctionType, Machine, }; -use arbutil::{evm, operator::OperatorInfo, Bytes32}; +use arbutil::{ + evm::{ + self, + api::{Gas, Ink}, + }, + operator::OperatorInfo, + Bytes32, +}; use derivative::Derivative; use eyre::Result; use fnv::FnvHashMap as HashMap; @@ -188,15 +195,15 @@ impl<'a, F: OpcodePricer> FuncMiddleware<'a> for FuncMeter<'a, F> { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MachineMeter { - Ready(u64), + Ready(Ink), Exhausted, } impl MachineMeter { - pub fn ink(self) -> u64 { + pub fn ink(self) -> Ink { match self { Self::Ready(ink) => ink, - Self::Exhausted => 0, + Self::Exhausted => Ink(0), } } @@ -210,8 +217,8 @@ impl MachineMeter { /// We don't implement `From` since it's unclear what 0 would map to #[allow(clippy::from_over_into)] -impl Into for MachineMeter { - fn into(self) -> u64 { +impl Into for MachineMeter { + fn into(self) -> Ink { self.ink() } } @@ -219,7 +226,7 @@ impl Into for MachineMeter { impl Display for MachineMeter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Ready(ink) => write!(f, "{ink} ink"), + Self::Ready(ink) => write!(f, "{} ink", ink.0), Self::Exhausted => write!(f, "exhausted"), } } @@ -241,7 +248,7 @@ pub trait MeteredMachine { fn ink_left(&self) -> MachineMeter; fn set_meter(&mut self, meter: MachineMeter); - fn set_ink(&mut self, ink: u64) { + fn set_ink(&mut self, ink: Ink) { self.set_meter(MachineMeter::Ready(ink)); } @@ -250,14 +257,14 @@ pub trait MeteredMachine { Err(OutOfInkError) } - fn ink_ready(&mut self) -> Result { + fn ink_ready(&mut self) -> Result { let MachineMeter::Ready(ink_left) = self.ink_left() else { return self.out_of_ink(); }; Ok(ink_left) } - fn buy_ink(&mut self, ink: u64) -> Result<(), OutOfInkError> { + fn buy_ink(&mut self, ink: Ink) -> Result<(), OutOfInkError> { let ink_left = self.ink_ready()?; if ink_left < ink { return self.out_of_ink(); @@ -267,7 +274,7 @@ pub trait MeteredMachine { } /// Checks if the user has enough ink, but doesn't burn any - fn require_ink(&mut self, ink: u64) -> Result<(), OutOfInkError> { + fn require_ink(&mut self, ink: Ink) -> Result<(), OutOfInkError> { let ink_left = self.ink_ready()?; if ink_left < ink { return self.out_of_ink(); @@ -277,18 +284,18 @@ pub trait MeteredMachine { /// Pays for a write into the client. fn pay_for_write(&mut self, bytes: u32) -> Result<(), OutOfInkError> { - self.buy_ink(sat_add_mul(5040, 30, bytes.saturating_sub(32))) + self.buy_ink(Ink(sat_add_mul(5040, 30, bytes.saturating_sub(32)))) } /// Pays for a read into the host. fn pay_for_read(&mut self, bytes: u32) -> Result<(), OutOfInkError> { - self.buy_ink(sat_add_mul(16381, 55, bytes.saturating_sub(32))) + self.buy_ink(Ink(sat_add_mul(16381, 55, bytes.saturating_sub(32)))) } /// Pays for both I/O and keccak. fn pay_for_keccak(&mut self, bytes: u32) -> Result<(), OutOfInkError> { let words = evm::evm_words(bytes).saturating_sub(2); - self.buy_ink(sat_add_mul(121800, 21000, words)) + self.buy_ink(Ink(sat_add_mul(121800, 21000, words))) } /// Pays for copying bytes from geth. @@ -305,14 +312,14 @@ pub trait MeteredMachine { false => break, } } - self.buy_ink(3000 + exp * 17500) + self.buy_ink(Ink(3000 + exp * 17500)) } } pub trait GasMeteredMachine: MeteredMachine { fn pricing(&self) -> PricingParams; - fn gas_left(&self) -> Result { + fn gas_left(&self) -> Result { let pricing = self.pricing(); match self.ink_left() { MachineMeter::Ready(ink) => Ok(pricing.ink_to_gas(ink)), @@ -320,13 +327,13 @@ pub trait GasMeteredMachine: MeteredMachine { } } - fn buy_gas(&mut self, gas: u64) -> Result<(), OutOfInkError> { + fn buy_gas(&mut self, gas: Gas) -> Result<(), OutOfInkError> { let pricing = self.pricing(); self.buy_ink(pricing.gas_to_ink(gas)) } /// Checks if the user has enough gas, but doesn't burn any - fn require_gas(&mut self, gas: u64) -> Result<(), OutOfInkError> { + fn require_gas(&mut self, gas: Gas) -> Result<(), OutOfInkError> { let pricing = self.pricing(); self.require_ink(pricing.gas_to_ink(gas)) } @@ -350,7 +357,7 @@ impl MeteredMachine for Machine { }}; } - let ink = || convert!(self.get_global(STYLUS_INK_LEFT)); + let ink = || Ink(convert!(self.get_global(STYLUS_INK_LEFT))); let status: u32 = convert!(self.get_global(STYLUS_INK_STATUS)); match status { @@ -362,7 +369,7 @@ impl MeteredMachine for Machine { fn set_meter(&mut self, meter: MachineMeter) { let ink = meter.ink(); let status = meter.status(); - self.set_global(STYLUS_INK_LEFT, ink.into()).unwrap(); + self.set_global(STYLUS_INK_LEFT, ink.0.into()).unwrap(); self.set_global(STYLUS_INK_STATUS, status.into()).unwrap(); } } diff --git a/arbitrator/prover/src/programs/mod.rs b/arbitrator/prover/src/programs/mod.rs index a5df2e31a8..a35308e7ff 100644 --- a/arbitrator/prover/src/programs/mod.rs +++ b/arbitrator/prover/src/programs/mod.rs @@ -8,7 +8,7 @@ use crate::{ programs::config::CompileConfig, value::{FunctionType as ArbFunctionType, Value}, }; -use arbutil::{math::SaturatingSum, Bytes32, Color}; +use arbutil::{evm::ARBOS_VERSION_STYLUS_CHARGING_FIXES, math::SaturatingSum, Bytes32, Color}; use eyre::{bail, eyre, Report, Result, WrapErr}; use fnv::FnvHashMap as HashMap; use std::fmt::Debug; @@ -418,58 +418,64 @@ impl Module { pub fn activate( wasm: &[u8], codehash: &Bytes32, - version: u16, + stylus_version: u16, + arbos_version_for_gas: u64, // must only be used for activation gas page_limit: u16, debug: bool, gas: &mut u64, ) -> Result<(Self, StylusData)> { - // converts a number of microseconds to gas - // TODO: collapse to a single value after finalizing factors - let us_to_gas = |us: u64| { - let fudge = 2; - let sync_rate = 1_000_000 / 2; - let speed = 7_000_000; - us.saturating_mul(fudge * speed) / sync_rate - }; - - macro_rules! pay { - ($us:expr) => { - let amount = us_to_gas($us); - if *gas < amount { - *gas = 0; - bail!("out of gas"); - } - *gas -= amount; + let compile = CompileConfig::version(stylus_version, debug); + let (bin, stylus_data) = + WasmBinary::parse_user(wasm, arbos_version_for_gas, page_limit, &compile, codehash) + .wrap_err("failed to parse wasm")?; + + if arbos_version_for_gas > 0 { + // converts a number of microseconds to gas + // TODO: collapse to a single value after finalizing factors + let us_to_gas = |us: u64| { + let fudge = 2; + let sync_rate = 1_000_000 / 2; + let speed = 7_000_000; + us.saturating_mul(fudge * speed) / sync_rate }; - } - - // pay for wasm - let wasm_len = wasm.len() as u64; - pay!(wasm_len.saturating_mul(31_733 / 100_000)); - - let compile = CompileConfig::version(version, debug); - let (bin, stylus_data) = WasmBinary::parse_user(wasm, page_limit, &compile, codehash) - .wrap_err("failed to parse wasm")?; - // pay for funcs - let funcs = bin.functions.len() as u64; - pay!(funcs.saturating_mul(17_263) / 100_000); - - // pay for data - let data = bin.datas.iter().map(|x| x.data.len()).saturating_sum() as u64; - pay!(data.saturating_mul(17_376) / 100_000); - - // pay for elements - let elems = bin.elements.iter().map(|x| x.range.len()).saturating_sum() as u64; - pay!(elems.saturating_mul(17_376) / 100_000); - - // pay for memory - let mem = bin.memories.first().map(|x| x.initial).unwrap_or_default(); - pay!(mem.saturating_mul(2217)); - - // pay for code - let code = bin.codes.iter().map(|x| x.expr.len()).saturating_sum() as u64; - pay!(code.saturating_mul(535) / 1_000); + macro_rules! pay { + ($us:expr) => { + let amount = us_to_gas($us); + if *gas < amount { + *gas = 0; + bail!("out of gas"); + } + *gas -= amount; + }; + } + + // pay for wasm + if arbos_version_for_gas >= ARBOS_VERSION_STYLUS_CHARGING_FIXES { + let wasm_len = wasm.len() as u64; + pay!(wasm_len.saturating_mul(31_733) / 100_000); + } + + // pay for funcs + let funcs = bin.functions.len() as u64; + pay!(funcs.saturating_mul(17_263) / 100_000); + + // pay for data + let data = bin.datas.iter().map(|x| x.data.len()).saturating_sum() as u64; + pay!(data.saturating_mul(17_376) / 100_000); + + // pay for elements + let elems = bin.elements.iter().map(|x| x.range.len()).saturating_sum() as u64; + pay!(elems.saturating_mul(17_376) / 100_000); + + // pay for memory + let mem = bin.memories.first().map(|x| x.initial).unwrap_or_default(); + pay!(mem.saturating_mul(2217)); + + // pay for code + let code = bin.codes.iter().map(|x| x.expr.len()).saturating_sum() as u64; + pay!(code.saturating_mul(535) / 1_000); + } let module = Self::from_user_binary(&bin, compile.debug.debug_funcs, Some(stylus_data)) .wrap_err("failed to build user module")?; diff --git a/arbitrator/prover/src/test.rs b/arbitrator/prover/src/test.rs index 97170441ff..4fd739342e 100644 --- a/arbitrator/prover/src/test.rs +++ b/arbitrator/prover/src/test.rs @@ -1,8 +1,6 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -#![cfg(test)] - use crate::binary; use brotli::Dictionary; use eyre::Result; @@ -64,7 +62,7 @@ pub fn test_compress() -> Result<()> { let deflate = brotli::compress(data, 11, 22, dict).unwrap(); let inflate = brotli::decompress(&deflate, dict).unwrap(); assert_eq!(hex::encode(inflate), hex::encode(data)); - assert!(&deflate != &last); + assert!(deflate != last); last = deflate; } Ok(()) diff --git a/arbitrator/prover/test-cases/dynamic.wat b/arbitrator/prover/test-cases/dynamic.wat index 8771bde87c..5de0dbdca1 100644 --- a/arbitrator/prover/test-cases/dynamic.wat +++ b/arbitrator/prover/test-cases/dynamic.wat @@ -12,7 +12,7 @@ ;; WAVM Module hash (data (i32.const 0x000) - "\a1\49\cf\81\13\ff\9c\95\f2\c8\c2\a1\42\35\75\36\7d\e8\6d\d4\22\d8\71\14\bb\9e\a4\7b\af\53\5d\d7") ;; user + "\ae\87\91\cf\6a\c4\55\ff\28\06\b9\55\d5\a7\36\e8\1b\c7\91\f7\93\8a\22\a4\08\23\25\16\37\01\48\25") ;; user (func $start (local $user i32) (local $internals i32) ;; link in user.wat i32.const 0 diff --git a/arbitrator/prover/test-cases/go/main.go b/arbitrator/prover/test-cases/go/main.go index 1f81553af2..b959454d26 100644 --- a/arbitrator/prover/test-cases/go/main.go +++ b/arbitrator/prover/test-cases/go/main.go @@ -73,7 +73,7 @@ const BYTES_PER_FIELD_ELEMENT = 32 var BLS_MODULUS, _ = new(big.Int).SetString("52435875175126190479447740508185965837690552500527637822603658699938581184513", 10) -var stylusModuleHash = common.HexToHash("a149cf8113ff9c95f2c8c2a1423575367de86dd422d87114bb9ea47baf535dd7") // user.wat +var stylusModuleHash = common.HexToHash("ae8791cf6ac455ff2806b955d5a736e81bc791f7938a22a40823251637014825") // user.wat func callStylusProgram(recurse int) { evmData := programs.EvmData{} diff --git a/arbitrator/prover/test-cases/link.wat b/arbitrator/prover/test-cases/link.wat index ef15326481..85490a40b1 100644 --- a/arbitrator/prover/test-cases/link.wat +++ b/arbitrator/prover/test-cases/link.wat @@ -30,7 +30,7 @@ (data (i32.const 0x140) "\47\f7\4f\9c\21\51\4f\52\24\ea\d3\37\5c\bf\a9\1b\1a\5f\ef\22\a5\2a\60\30\c5\52\18\90\6b\b1\51\e5") ;; iops (data (i32.const 0x160) - "\a1\49\cf\81\13\ff\9c\95\f2\c8\c2\a1\42\35\75\36\7d\e8\6d\d4\22\d8\71\14\bb\9e\a4\7b\af\53\5d\d7") ;; user + "\ae\87\91\cf\6a\c4\55\ff\28\06\b9\55\d5\a7\36\e8\1b\c7\91\f7\93\8a\22\a4\08\23\25\16\37\01\48\25") ;; user (data (i32.const 0x180) "\ee\47\08\f6\47\b2\10\88\1f\89\86\e7\e3\79\6b\b2\77\43\f1\4e\ee\cf\45\4a\9b\7c\d7\c4\5b\63\b6\d7") ;; return diff --git a/arbitrator/prover/test-cases/user.wat b/arbitrator/prover/test-cases/user.wat index 9ecb4dcc45..694d2f3ed8 100644 --- a/arbitrator/prover/test-cases/user.wat +++ b/arbitrator/prover/test-cases/user.wat @@ -22,6 +22,12 @@ i32.const 0xFFFFFF i32.load ) + (func $infinite_loop (result i32) + (loop $loop + br $loop + ) + unreachable + ) (func (export "user_entrypoint") (param $args_len i32) (result i32) ;; this func uses $args_len to select which func to call @@ -43,6 +49,12 @@ (then (call $out_of_bounds) (return)) ) + ;; reverts due to an out-of-gas error + (i32.eq (local.get $args_len) (i32.const 4)) + (if + (then (call $infinite_loop) (return)) + ) + (i32.eq (local.get $args_len) (i32.const 32)) (if (then (call $storage_load) (return)) diff --git a/arbitrator/stylus/Cargo.toml b/arbitrator/stylus/Cargo.toml index 4717bd631a..ea1d878ea0 100644 --- a/arbitrator/stylus/Cargo.toml +++ b/arbitrator/stylus/Cargo.toml @@ -21,11 +21,11 @@ thiserror = "1.0.33" bincode = "1.3.3" lazy_static.workspace = true libc = "0.2.108" -lru.workspace = true eyre = "0.6.5" rand = "0.8.5" fnv = "1.0.7" hex = "0.4.3" +clru = "0.6.2" [dev-dependencies] num-bigint = "0.4.4" diff --git a/arbitrator/stylus/src/cache.rs b/arbitrator/stylus/src/cache.rs index fa38d45419..9b788a45db 100644 --- a/arbitrator/stylus/src/cache.rs +++ b/arbitrator/stylus/src/cache.rs @@ -2,18 +2,19 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use arbutil::Bytes32; +use clru::{CLruCache, CLruCacheConfig, WeightScale}; use eyre::Result; use lazy_static::lazy_static; -use lru::LruCache; use parking_lot::Mutex; use prover::programs::config::CompileConfig; +use std::hash::RandomState; use std::{collections::HashMap, num::NonZeroUsize}; use wasmer::{Engine, Module, Store}; use crate::target_cache::target_native; lazy_static! { - static ref INIT_CACHE: Mutex = Mutex::new(InitCache::new(256)); + static ref INIT_CACHE: Mutex = Mutex::new(InitCache::new(256 * 1024 * 1024)); } macro_rules! cache { @@ -22,9 +23,24 @@ macro_rules! cache { }; } +pub struct LruCounters { + pub hits: u32, + pub misses: u32, + pub does_not_fit: u32, +} + +pub struct LongTermCounters { + pub hits: u32, + pub misses: u32, +} + pub struct InitCache { long_term: HashMap, - lru: LruCache, + long_term_size_bytes: usize, + long_term_counters: LongTermCounters, + + lru: CLruCache, + lru_counters: LruCounters, } #[derive(Clone, Copy, Hash, PartialEq, Eq)] @@ -48,11 +64,16 @@ impl CacheKey { struct CacheItem { module: Module, engine: Engine, + entry_size_estimate_bytes: usize, } impl CacheItem { - fn new(module: Module, engine: Engine) -> Self { - Self { module, engine } + fn new(module: Module, engine: Engine, entry_size_estimate_bytes: usize) -> Self { + Self { + module, + engine, + entry_size_estimate_bytes, + } } fn data(&self) -> (Module, Store) { @@ -60,39 +81,121 @@ impl CacheItem { } } +struct CustomWeightScale; +impl WeightScale for CustomWeightScale { + fn weight(&self, _key: &CacheKey, val: &CacheItem) -> usize { + // clru defines that each entry consumes (weight + 1) of the cache capacity. + // We subtract 1 since we only want to use the weight as the size of the entry. + val.entry_size_estimate_bytes.saturating_sub(1) + } +} + +#[repr(C)] +pub struct LruCacheMetrics { + pub size_bytes: u64, + pub count: u32, + pub hits: u32, + pub misses: u32, + pub does_not_fit: u32, +} + +#[repr(C)] +pub struct LongTermCacheMetrics { + pub size_bytes: u64, + pub count: u32, + pub hits: u32, + pub misses: u32, +} + +#[repr(C)] +pub struct CacheMetrics { + pub lru: LruCacheMetrics, + pub long_term: LongTermCacheMetrics, +} + +pub fn deserialize_module( + module: &[u8], + version: u16, + debug: bool, +) -> Result<(Module, Engine, usize)> { + let engine = CompileConfig::version(version, debug).engine(target_native()); + let module = unsafe { Module::deserialize_unchecked(&engine, module)? }; + + let asm_size_estimate_bytes = module.serialize()?.len(); + // add 128 bytes for the cache item overhead + let entry_size_estimate_bytes = asm_size_estimate_bytes + 128; + + Ok((module, engine, entry_size_estimate_bytes)) +} + impl InitCache { // current implementation only has one tag that stores to the long_term // future implementations might have more, but 0 is a reserved tag // that will never modify long_term state const ARBOS_TAG: u32 = 1; - fn new(size: usize) -> Self { + const DOES_NOT_FIT_MSG: &'static str = "Failed to insert into LRU cache, item too large"; + + fn new(size_bytes: usize) -> Self { Self { long_term: HashMap::new(), - lru: LruCache::new(NonZeroUsize::new(size).unwrap()), + long_term_size_bytes: 0, + long_term_counters: LongTermCounters { hits: 0, misses: 0 }, + + lru: CLruCache::with_config( + CLruCacheConfig::new(NonZeroUsize::new(size_bytes).unwrap()) + .with_scale(CustomWeightScale), + ), + lru_counters: LruCounters { + hits: 0, + misses: 0, + does_not_fit: 0, + }, } } - pub fn set_lru_size(size: u32) { + pub fn set_lru_capacity(capacity_bytes: u64) { cache!() .lru - .resize(NonZeroUsize::new(size.try_into().unwrap()).unwrap()) + .resize(NonZeroUsize::new(capacity_bytes.try_into().unwrap()).unwrap()) } /// Retrieves a cached value, updating items as necessary. - pub fn get(module_hash: Bytes32, version: u16, debug: bool) -> Option<(Module, Store)> { - let mut cache = cache!(); + /// If long_term_tag is 1 and the item is only in LRU will insert to long term cache. + pub fn get( + module_hash: Bytes32, + version: u16, + long_term_tag: u32, + debug: bool, + ) -> Option<(Module, Store)> { let key = CacheKey::new(module_hash, version, debug); + let mut cache = cache!(); // See if the item is in the long term cache if let Some(item) = cache.long_term.get(&key) { - return Some(item.data()); + let data = item.data(); + cache.long_term_counters.hits += 1; + return Some(data); + } + if long_term_tag == Self::ARBOS_TAG { + // only count misses only when we can expect to find the item in long term cache + cache.long_term_counters.misses += 1; } // See if the item is in the LRU cache, promoting if so - if let Some(item) = cache.lru.get(&key) { - return Some(item.data()); + if let Some(item) = cache.lru.peek(&key).cloned() { + cache.lru_counters.hits += 1; + if long_term_tag == Self::ARBOS_TAG { + cache.long_term_size_bytes += item.entry_size_estimate_bytes; + cache.long_term.insert(key, item.clone()); + } else { + // only calls get to move the key to the head of the LRU list + cache.lru.get(&key); + } + return Some((item.module, Store::new(item.engine))); } + cache.lru_counters.misses += 1; + None } @@ -115,23 +218,29 @@ impl InitCache { if let Some(item) = cache.lru.peek(&key).cloned() { if long_term_tag == Self::ARBOS_TAG { cache.long_term.insert(key, item.clone()); + cache.long_term_size_bytes += item.entry_size_estimate_bytes; } else { - cache.lru.promote(&key) + // only calls get to move the key to the head of the LRU list + cache.lru.get(&key); } return Ok(item.data()); } drop(cache); - let engine = CompileConfig::version(version, debug).engine(target_native()); - let module = unsafe { Module::deserialize_unchecked(&engine, module)? }; + let (module, engine, entry_size_estimate_bytes) = + deserialize_module(module, version, debug)?; - let item = CacheItem::new(module, engine); + let item = CacheItem::new(module, engine, entry_size_estimate_bytes); let data = item.data(); let mut cache = cache!(); if long_term_tag != Self::ARBOS_TAG { - cache.lru.put(key, item); + if cache.lru.put_with_weight(key, item).is_err() { + cache.lru_counters.does_not_fit += 1; + eprintln!("{}", Self::DOES_NOT_FIT_MSG); + }; } else { cache.long_term.insert(key, item); + cache.long_term_size_bytes += entry_size_estimate_bytes; } Ok(data) } @@ -144,7 +253,10 @@ impl InitCache { let key = CacheKey::new(module_hash, version, debug); let mut cache = cache!(); if let Some(item) = cache.long_term.remove(&key) { - cache.lru.put(key, item); + cache.long_term_size_bytes -= item.entry_size_estimate_bytes; + if cache.lru.put_with_weight(key, item).is_err() { + eprintln!("{}", Self::DOES_NOT_FIT_MSG); + } } } @@ -155,7 +267,49 @@ impl InitCache { let mut cache = cache!(); let cache = &mut *cache; for (key, item) in cache.long_term.drain() { - cache.lru.put(key, item); // not all will fit, just a heuristic + // not all will fit, just a heuristic + if cache.lru.put_with_weight(key, item).is_err() { + eprintln!("{}", Self::DOES_NOT_FIT_MSG); + } } + cache.long_term_size_bytes = 0; + } + + pub fn get_metrics(output: &mut CacheMetrics) { + let mut cache = cache!(); + + let lru_count = cache.lru.len(); + // adds 1 to each entry to account that we subtracted 1 in the weight calculation + output.lru.size_bytes = (cache.lru.weight() + lru_count).try_into().unwrap(); + output.lru.count = lru_count.try_into().unwrap(); + output.lru.hits = cache.lru_counters.hits; + output.lru.misses = cache.lru_counters.misses; + output.lru.does_not_fit = cache.lru_counters.does_not_fit; + + output.long_term.size_bytes = cache.long_term_size_bytes.try_into().unwrap(); + output.long_term.count = cache.long_term.len().try_into().unwrap(); + output.long_term.hits = cache.long_term_counters.hits; + output.long_term.misses = cache.long_term_counters.misses; + + // Empty counters. + // go side, which is the only consumer of this function besides tests, + // will read those counters and increment its own prometheus counters with them. + cache.lru_counters = LruCounters { + hits: 0, + misses: 0, + does_not_fit: 0, + }; + cache.long_term_counters = LongTermCounters { hits: 0, misses: 0 }; + } + + // only used for testing + pub fn clear_lru_cache() { + let mut cache = cache!(); + cache.lru.clear(); + cache.lru_counters = LruCounters { + hits: 0, + misses: 0, + does_not_fit: 0, + }; } } diff --git a/arbitrator/stylus/src/env.rs b/arbitrator/stylus/src/env.rs index 69d542070d..a2c8189029 100644 --- a/arbitrator/stylus/src/env.rs +++ b/arbitrator/stylus/src/env.rs @@ -2,8 +2,9 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use arbutil::{ + benchmark::Benchmark, evm::{ - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Ink}, EvmData, }, pricing, @@ -48,6 +49,8 @@ pub struct WasmEnv> { pub compile: CompileConfig, /// The runtime config pub config: Option, + // Used to benchmark execution blocks of code + pub benchmark: Benchmark, // Using the unused generic parameter D in a PhantomData field _data_reader_marker: PhantomData, } @@ -68,13 +71,14 @@ impl> WasmEnv { outs: vec![], memory: None, meter: None, + benchmark: Benchmark::default(), _data_reader_marker: PhantomData, } } pub fn start<'a>( env: &'a mut WasmEnvMut<'_, D, E>, - ink: u64, + ink: Ink, ) -> Result, Escape> { let mut info = Self::program(env)?; info.buy_ink(pricing::HOSTIO_INK + ink)?; @@ -88,7 +92,7 @@ impl> WasmEnv { env, memory, store, - start_ink: 0, + start_ink: Ink(0), }; if info.env.evm_data.tracing { info.start_ink = info.ink_ready()?; @@ -114,16 +118,16 @@ pub struct MeterData { } impl MeterData { - pub fn ink(&self) -> u64 { - unsafe { self.ink_left.as_ref().val.u64 } + pub fn ink(&self) -> Ink { + Ink(unsafe { self.ink_left.as_ref().val.u64 }) } pub fn status(&self) -> u32 { unsafe { self.ink_status.as_ref().val.u32 } } - pub fn set_ink(&mut self, ink: u64) { - unsafe { self.ink_left.as_mut().val = RawValue { u64: ink } } + pub fn set_ink(&mut self, ink: Ink) { + unsafe { self.ink_left.as_mut().val = RawValue { u64: ink.0 } } } pub fn set_status(&mut self, status: u32) { @@ -140,7 +144,7 @@ pub struct HostioInfo<'a, D: DataReader, E: EvmApi> { pub env: &'a mut WasmEnv, pub memory: Memory, pub store: StoreMut<'a>, - pub start_ink: u64, + pub start_ink: Ink, } impl<'a, D: DataReader, E: EvmApi> HostioInfo<'a, D, E> { diff --git a/arbitrator/stylus/src/evm_api.rs b/arbitrator/stylus/src/evm_api.rs index d267372827..7aa605dfe7 100644 --- a/arbitrator/stylus/src/evm_api.rs +++ b/arbitrator/stylus/src/evm_api.rs @@ -1,11 +1,12 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE -use crate::{GoSliceData, RustSlice}; +use crate::GoSliceData; use arbutil::evm::{ - api::{EvmApiMethod, EVM_API_METHOD_REQ_OFFSET}, + api::{EvmApiMethod, Gas, EVM_API_METHOD_REQ_OFFSET}, req::RequestHandler, }; +use prover::RustSlice; #[repr(C)] pub struct NativeRequestHandler { @@ -31,7 +32,7 @@ impl RequestHandler for NativeRequestHandler { &mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>, - ) -> (Vec, GoSliceData, u64) { + ) -> (Vec, GoSliceData, Gas) { let mut result = GoSliceData::null(); let mut raw_data = GoSliceData::null(); let mut cost = 0; @@ -45,6 +46,6 @@ impl RequestHandler for NativeRequestHandler { ptr!(raw_data), ) }; - (result.slice().to_vec(), raw_data, cost) + (result.slice().to_vec(), raw_data, Gas(cost)) } } diff --git a/arbitrator/stylus/src/host.rs b/arbitrator/stylus/src/host.rs index 1afc1b4e51..67497302a1 100644 --- a/arbitrator/stylus/src/host.rs +++ b/arbitrator/stylus/src/host.rs @@ -5,8 +5,9 @@ use crate::env::{Escape, HostioInfo, MaybeEscape, WasmEnv, WasmEnvMut}; use arbutil::{ + benchmark::Benchmark, evm::{ - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Gas, Ink}, EvmData, }, Color, @@ -46,6 +47,10 @@ where &self.evm_data } + fn benchmark(&mut self) -> &mut Benchmark { + &mut self.env.benchmark + } + fn evm_return_data_len(&mut self) -> &mut u32 { &mut self.evm_data.return_data_len } @@ -82,7 +87,7 @@ where println!("{} {text}", "Stylus says:".yellow()); } - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: u64) { + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: Ink) { let start_ink = self.start_ink; self.evm_api .capture_hostio(name, args, outs, start_ink, end_ink); @@ -168,7 +173,7 @@ pub(crate) fn call_contract>( ) -> Result { hostio!( env, - call_contract(contract, data, data_len, value, gas, ret_len) + call_contract(contract, data, data_len, value, Gas(gas), ret_len) ) } @@ -182,7 +187,7 @@ pub(crate) fn delegate_call_contract>( ) -> Result { hostio!( env, - delegate_call_contract(contract, data, data_len, gas, ret_len) + delegate_call_contract(contract, data, data_len, Gas(gas), ret_len) ) } @@ -196,7 +201,7 @@ pub(crate) fn static_call_contract>( ) -> Result { hostio!( env, - static_call_contract(contract, data, data_len, gas, ret_len) + static_call_contract(contract, data, data_len, Gas(gas), ret_len) ) } @@ -334,13 +339,13 @@ pub(crate) fn contract_address>( pub(crate) fn evm_gas_left>( mut env: WasmEnvMut, ) -> Result { - hostio!(env, evm_gas_left()) + hostio!(env, evm_gas_left()).map(|g| g.0) } pub(crate) fn evm_ink_left>( mut env: WasmEnvMut, ) -> Result { - hostio!(env, evm_ink_left()) + hostio!(env, evm_ink_left()).map(|i| i.0) } pub(crate) fn math_div>( @@ -464,3 +469,13 @@ pub(crate) fn console_tee, T: Into + Copy>( } pub(crate) fn null_host>(_: WasmEnvMut) {} + +pub(crate) fn start_benchmark>( + mut env: WasmEnvMut, +) -> MaybeEscape { + hostio!(env, start_benchmark()) +} + +pub(crate) fn end_benchmark>(mut env: WasmEnvMut) -> MaybeEscape { + hostio!(env, end_benchmark()) +} diff --git a/arbitrator/stylus/src/lib.rs b/arbitrator/stylus/src/lib.rs index a252b60a01..c73c4b2c2e 100644 --- a/arbitrator/stylus/src/lib.rs +++ b/arbitrator/stylus/src/lib.rs @@ -3,7 +3,7 @@ use arbutil::{ evm::{ - api::DataReader, + api::{DataReader, Gas, Ink}, req::EvmApiRequestor, user::{UserOutcome, UserOutcomeKind}, EvmData, @@ -11,13 +11,16 @@ use arbutil::{ format::DebugBytes, Bytes32, }; -use cache::InitCache; +use cache::{deserialize_module, CacheMetrics, InitCache}; use evm_api::NativeRequestHandler; use eyre::ErrReport; use native::NativeInstance; -use prover::programs::{prelude::*, StylusData}; +use prover::{ + programs::{prelude::*, StylusData}, + RustBytes, +}; use run::RunProgram; -use std::{marker::PhantomData, mem, ptr}; +use std::ptr; use target_cache::{target_cache_get, target_cache_set}; pub use brotli; @@ -76,52 +79,15 @@ impl DataReader for GoSliceData { } } -#[repr(C)] -pub struct RustSlice<'a> { - ptr: *const u8, - len: usize, - phantom: PhantomData<&'a [u8]>, -} - -impl<'a> RustSlice<'a> { - fn new(slice: &'a [u8]) -> Self { - Self { - ptr: slice.as_ptr(), - len: slice.len(), - phantom: PhantomData, - } - } -} - -#[repr(C)] -pub struct RustBytes { - ptr: *mut u8, - len: usize, - cap: usize, +unsafe fn write_err(output: &mut RustBytes, err: ErrReport) -> UserOutcomeKind { + output.write(err.debug_bytes()); + UserOutcomeKind::Failure } -impl RustBytes { - unsafe fn into_vec(self) -> Vec { - Vec::from_raw_parts(self.ptr, self.len, self.cap) - } - - unsafe fn write(&mut self, mut vec: Vec) { - self.ptr = vec.as_mut_ptr(); - self.len = vec.len(); - self.cap = vec.capacity(); - mem::forget(vec); - } - - unsafe fn write_err(&mut self, err: ErrReport) -> UserOutcomeKind { - self.write(err.debug_bytes()); - UserOutcomeKind::Failure - } - - unsafe fn write_outcome(&mut self, outcome: UserOutcome) -> UserOutcomeKind { - let (status, outs) = outcome.into_data(); - self.write(outs); - status - } +unsafe fn write_outcome(output: &mut RustBytes, outcome: UserOutcome) -> UserOutcomeKind { + let (status, outs) = outcome.into_data(); + output.write(outs); + status } /// "activates" a user wasm. @@ -139,7 +105,8 @@ impl RustBytes { pub unsafe extern "C" fn stylus_activate( wasm: GoSliceData, page_limit: u16, - version: u16, + stylus_version: u16, + arbos_version_for_gas: u64, debug: bool, output: *mut RustBytes, codehash: *const Bytes32, @@ -153,9 +120,17 @@ pub unsafe extern "C" fn stylus_activate( let codehash = &*codehash; let gas = &mut *gas; - let (module, info) = match native::activate(wasm, codehash, version, page_limit, debug, gas) { + let (module, info) = match native::activate( + wasm, + codehash, + stylus_version, + arbos_version_for_gas, + page_limit, + debug, + gas, + ) { Ok(val) => val, - Err(err) => return output.write_err(err), + Err(err) => return write_err(output, err), }; *module_hash = module.hash(); @@ -185,16 +160,16 @@ pub unsafe extern "C" fn stylus_compile( let output = &mut *output; let name = match String::from_utf8(name.slice().to_vec()) { Ok(val) => val, - Err(err) => return output.write_err(err.into()), + Err(err) => return write_err(output, err.into()), }; let target = match target_cache_get(&name) { Ok(val) => val, - Err(err) => return output.write_err(err), + Err(err) => return write_err(output, err), }; let asm = match native::compile(wasm, version, debug, target) { Ok(val) => val, - Err(err) => return output.write_err(err), + Err(err) => return write_err(output, err), }; output.write(asm); @@ -209,7 +184,7 @@ pub unsafe extern "C" fn wat_to_wasm(wat: GoSliceData, output: *mut RustBytes) - let output = &mut *output; let wasm = match wasmer::wat2wasm(wat.slice()) { Ok(val) => val, - Err(err) => return output.write_err(err.into()), + Err(err) => return write_err(output, err.into()), }; output.write(wasm.into_owned()); UserOutcomeKind::Success @@ -232,16 +207,16 @@ pub unsafe extern "C" fn stylus_target_set( let output = &mut *output; let name = match String::from_utf8(name.slice().to_vec()) { Ok(val) => val, - Err(err) => return output.write_err(err.into()), + Err(err) => return write_err(output, err.into()), }; let desc_str = match String::from_utf8(description.slice().to_vec()) { Ok(val) => val, - Err(err) => return output.write_err(err.into()), + Err(err) => return write_err(output, err.into()), }; if let Err(err) = target_cache_set(name, desc_str, native) { - return output.write_err(err); + return write_err(output, err); }; UserOutcomeKind::Success @@ -270,7 +245,7 @@ pub unsafe extern "C" fn stylus_call( let evm_api = EvmApiRequestor::new(req_handler); let pricing = config.pricing; let output = &mut *output; - let ink = pricing.gas_to_ink(*gas); + let ink = pricing.gas_to_ink(Gas(*gas)); // Safety: module came from compile_user_wasm and we've paid for memory expansion let instance = unsafe { @@ -289,21 +264,21 @@ pub unsafe extern "C" fn stylus_call( }; let status = match instance.run_main(&calldata, config, ink) { - Err(e) | Ok(UserOutcome::Failure(e)) => output.write_err(e.wrap_err("call failed")), - Ok(outcome) => output.write_outcome(outcome), + Err(e) | Ok(UserOutcome::Failure(e)) => write_err(output, e.wrap_err("call failed")), + Ok(outcome) => write_outcome(output, outcome), }; let ink_left = match status { - UserOutcomeKind::OutOfStack => 0, // take all gas when out of stack + UserOutcomeKind::OutOfStack => Ink(0), // take all gas when out of stack _ => instance.ink_left().into(), }; - *gas = pricing.ink_to_gas(ink_left); + *gas = pricing.ink_to_gas(ink_left).0; status } -/// resize lru +/// set lru cache capacity #[no_mangle] -pub extern "C" fn stylus_cache_lru_resize(size: u32) { - InitCache::set_lru_size(size); +pub extern "C" fn stylus_set_cache_lru_capacity(capacity_bytes: u64) { + InitCache::set_lru_capacity(capacity_bytes); } /// Caches an activated user program. @@ -343,14 +318,41 @@ pub extern "C" fn stylus_reorg_vm(_block: u64, arbos_tag: u32) { InitCache::clear_long_term(arbos_tag); } -/// Frees the vector. Does nothing when the vector is null. +/// Gets cache metrics. /// /// # Safety /// -/// Must only be called once per vec. +/// `output` must not be null. +#[no_mangle] +pub unsafe extern "C" fn stylus_get_cache_metrics(output: *mut CacheMetrics) { + let output = &mut *output; + InitCache::get_metrics(output); +} + +/// Clears lru cache. +/// Only used for testing purposes. #[no_mangle] -pub unsafe extern "C" fn stylus_drop_vec(vec: RustBytes) { - if !vec.ptr.is_null() { - mem::drop(vec.into_vec()) +pub extern "C" fn stylus_clear_lru_cache() { + InitCache::clear_lru_cache() +} + +/// Clears long term cache (for arbos_tag = 1) +/// Only used for testing purposes. +#[no_mangle] +pub extern "C" fn stylus_clear_long_term_cache() { + InitCache::clear_long_term(1); +} + +/// Gets entry size in bytes. +/// Only used for testing purposes. +#[no_mangle] +pub extern "C" fn stylus_get_entry_size_estimate_bytes( + module: GoSliceData, + version: u16, + debug: bool, +) -> u64 { + match deserialize_module(module.slice(), version, debug) { + Err(error) => panic!("tried to get invalid asm!: {error}"), + Ok((_, _, entry_size_estimate_bytes)) => entry_size_estimate_bytes.try_into().unwrap(), } } diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index cc1d191fe2..a31df1034c 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -8,7 +8,7 @@ use crate::{ }; use arbutil::{ evm::{ - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Ink}, EvmData, }, operator::OperatorCode, @@ -121,13 +121,12 @@ impl> NativeInstance { let compile = CompileConfig::version(version, debug); let env = WasmEnv::new(compile, None, evm, evm_data); let module_hash = env.evm_data.module_hash; - - if let Some((module, store)) = InitCache::get(module_hash, version, debug) { - return Self::from_module(module, store, env); - } if !env.evm_data.cached { long_term_tag = 0; } + if let Some((module, store)) = InitCache::get(module_hash, version, long_term_tag, debug) { + return Self::from_module(module, store, env); + } let (module, store) = InitCache::insert(module_hash, module, version, long_term_tag, debug)?; Self::from_module(module, store, env) @@ -213,6 +212,8 @@ impl> NativeInstance { imports.define("console", "tee_f32", func!(host::console_tee::)); imports.define("console", "tee_f64", func!(host::console_tee::)); imports.define("debug", "null_host", func!(host::null_host)); + imports.define("debug", "start_benchmark", func!(host::start_benchmark)); + imports.define("debug", "end_benchmark", func!(host::end_benchmark)); } let instance = Instance::new(&mut store, &module, &imports)?; let exports = &instance.exports; @@ -271,7 +272,7 @@ impl> NativeInstance { global.set(store, value.into()).map_err(ErrReport::msg) } - pub fn call_func(&mut self, func: TypedFunction<(), R>, ink: u64) -> Result + pub fn call_func(&mut self, func: TypedFunction<(), R>, ink: Ink) -> Result where R: WasmTypeList, { @@ -430,6 +431,8 @@ pub fn module(wasm: &[u8], compile: CompileConfig, target: Target) -> Result Result Result<(ProverModule, StylusData)> { - let (module, stylus_data) = - ProverModule::activate(wasm, codehash, version, page_limit, debug, gas)?; + let (module, stylus_data) = ProverModule::activate( + wasm, + codehash, + stylus_version, + arbos_version_for_gas, + page_limit, + debug, + gas, + )?; Ok((module, stylus_data)) } diff --git a/arbitrator/stylus/src/run.rs b/arbitrator/stylus/src/run.rs index 8e673a25e5..6cbb0cfb42 100644 --- a/arbitrator/stylus/src/run.rs +++ b/arbitrator/stylus/src/run.rs @@ -4,18 +4,18 @@ #![allow(clippy::redundant_closure_call)] use crate::{env::Escape, native::NativeInstance}; -use arbutil::evm::api::{DataReader, EvmApi}; +use arbutil::evm::api::{DataReader, EvmApi, Ink}; use arbutil::evm::user::UserOutcome; use eyre::{eyre, Result}; use prover::machine::Machine; use prover::programs::{prelude::*, STYLUS_ENTRY_POINT}; pub trait RunProgram { - fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: u64) -> Result; + fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: Ink) -> Result; } impl RunProgram for Machine { - fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: u64) -> Result { + fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: Ink) -> Result { macro_rules! call { ($module:expr, $func:expr, $args:expr) => { call!($module, $func, $args, |error| UserOutcome::Failure(error)) @@ -65,7 +65,7 @@ impl RunProgram for Machine { } impl> RunProgram for NativeInstance { - fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: u64) -> Result { + fn run_main(&mut self, args: &[u8], config: StylusConfig, ink: Ink) -> Result { use UserOutcome::*; self.set_ink(ink); diff --git a/arbitrator/stylus/src/test/api.rs b/arbitrator/stylus/src/test/api.rs index 5d9f625e5e..7a5af6f89f 100644 --- a/arbitrator/stylus/src/test/api.rs +++ b/arbitrator/stylus/src/test/api.rs @@ -4,7 +4,7 @@ use crate::{native, run::RunProgram}; use arbutil::{ evm::{ - api::{EvmApi, VecReader}, + api::{EvmApi, Gas, Ink, VecReader}, user::UserOutcomeKind, EvmData, }, @@ -68,24 +68,24 @@ impl TestEvmApi { } impl EvmApi for TestEvmApi { - fn get_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64) { + fn get_bytes32(&mut self, key: Bytes32, _evm_api_gas_to_use: Gas) -> (Bytes32, Gas) { let storage = &mut self.storage.lock(); let storage = storage.get_mut(&self.program).unwrap(); let value = storage.get(&key).cloned().unwrap_or_default(); - (value, 2100) // pretend worst case + (value, Gas(2100)) // pretend worst case } - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 { + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas { let storage = &mut self.storage.lock(); let storage = storage.get_mut(&self.program).unwrap(); storage.insert(key, value); - 0 + Gas(0) } - fn flush_storage_cache(&mut self, _clear: bool, _gas_left: u64) -> Result { + fn flush_storage_cache(&mut self, _clear: bool, _gas_left: Gas) -> Result { let storage = &mut self.storage.lock(); let storage = storage.get_mut(&self.program).unwrap(); - Ok(22100 * storage.len() as u64) // pretend worst case + Ok(Gas(22100) * storage.len() as u64) // pretend worst case } fn get_transient_bytes32(&mut self, _key: Bytes32) -> Bytes32 { @@ -102,10 +102,10 @@ impl EvmApi for TestEvmApi { &mut self, contract: Bytes20, calldata: &[u8], - _gas_left: u64, - gas_req: u64, + _gas_left: Gas, + gas_req: Gas, _value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { let compile = self.compile.clone(); let evm_data = self.evm_data; let config = *self.configs.lock().get(&contract).unwrap(); @@ -122,7 +122,7 @@ impl EvmApi for TestEvmApi { let (status, outs) = outcome.into_data(); let outs_len = outs.len() as u32; - let ink_left: u64 = native.ink_left().into(); + let ink_left: Ink = native.ink_left().into(); let gas_left = config.pricing.ink_to_gas(ink_left); *self.write_result.lock() = outs; (outs_len, gas - gas_left, status) @@ -132,9 +132,9 @@ impl EvmApi for TestEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + _gas_left: Gas, + _gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { todo!("delegate call not yet supported") } @@ -142,9 +142,9 @@ impl EvmApi for TestEvmApi { &mut self, contract: Bytes20, calldata: &[u8], - gas_left: u64, - gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + gas_left: Gas, + gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { println!("note: overriding static call with call"); self.contract_call(contract, calldata, gas_left, gas_req, Bytes32::default()) } @@ -153,8 +153,8 @@ impl EvmApi for TestEvmApi { &mut self, _code: Vec, _endowment: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!("create1 not supported") } @@ -163,8 +163,8 @@ impl EvmApi for TestEvmApi { _code: Vec, _endowment: Bytes32, _salt: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!("create2 not supported") } @@ -176,19 +176,19 @@ impl EvmApi for TestEvmApi { Ok(()) // pretend a log was emitted } - fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn account_code(&mut self, _address: Bytes20, _gas_left: u64) -> (VecReader, u64) { + fn account_code(&mut self, _address: Bytes20, _gas_left: Gas) -> (VecReader, Gas) { unimplemented!() } - fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn add_pages(&mut self, new: u16) -> u64 { + fn add_pages(&mut self, new: u16) -> Gas { let model = MemoryModel::new(2, 1000); let (open, ever) = *self.pages.lock(); @@ -203,8 +203,8 @@ impl EvmApi for TestEvmApi { _name: &str, _args: &[u8], _outs: &[u8], - _start_ink: u64, - _end_ink: u64, + _start_ink: Ink, + _end_ink: Ink, ) { unimplemented!() } diff --git a/arbitrator/stylus/src/test/mod.rs b/arbitrator/stylus/src/test/mod.rs index 00c9c62ae4..3fd0faede8 100644 --- a/arbitrator/stylus/src/test/mod.rs +++ b/arbitrator/stylus/src/test/mod.rs @@ -3,7 +3,10 @@ use crate::{env::WasmEnv, native::NativeInstance, run::RunProgram, test::api::TestEvmApi}; use arbutil::{ - evm::{api::VecReader, user::UserOutcome}, + evm::{ + api::{Ink, VecReader}, + user::UserOutcome, + }, Bytes20, Bytes32, Color, }; use eyre::{bail, Result}; @@ -41,7 +44,7 @@ impl TestInstance { }; let mut native = Self::new_from_store(path, store, imports)?; native.set_meter_data(); - native.set_ink(u64::MAX); + native.set_ink(Ink(u64::MAX)); native.set_stack(u32::MAX); Ok(native) } @@ -107,8 +110,8 @@ fn expensive_add(op: &Operator, _tys: &SigMap) -> u64 { } } -pub fn random_ink(min: u64) -> u64 { - rand::thread_rng().gen_range(min..=u64::MAX) +pub fn random_ink(min: u64) -> Ink { + Ink(rand::thread_rng().gen_range(min..=u64::MAX)) } pub fn random_bytes20() -> Bytes20 { @@ -135,7 +138,7 @@ fn uniform_cost_config() -> StylusConfig { stylus_config } -fn test_configs() -> (CompileConfig, StylusConfig, u64) { +fn test_configs() -> (CompileConfig, StylusConfig, Ink) { ( test_compile_config(), uniform_cost_config(), @@ -165,12 +168,12 @@ fn new_test_machine(path: &str, compile: &CompileConfig) -> Result { Arc::new(|_, _, _| panic!("tried to read preimage")), Some(stylus_data), )?; - mach.set_ink(u64::MAX); + mach.set_ink(Ink(u64::MAX)); mach.set_stack(u32::MAX); Ok(mach) } -fn run_native(native: &mut TestInstance, args: &[u8], ink: u64) -> Result> { +fn run_native(native: &mut TestInstance, args: &[u8], ink: Ink) -> Result> { let config = native.env().config.expect("no config"); match native.run_main(args, config, ink)? { UserOutcome::Success(output) => Ok(output), @@ -182,7 +185,7 @@ fn run_machine( machine: &mut Machine, args: &[u8], config: StylusConfig, - ink: u64, + ink: Ink, ) -> Result> { match machine.run_main(args, config, ink)? { UserOutcome::Success(output) => Ok(output), diff --git a/arbitrator/stylus/src/test/native.rs b/arbitrator/stylus/src/test/native.rs index 503e5875fe..672bdd179c 100644 --- a/arbitrator/stylus/src/test/native.rs +++ b/arbitrator/stylus/src/test/native.rs @@ -16,7 +16,7 @@ use crate::{ use arbutil::{ crypto, evm::{ - api::EvmApi, + api::{EvmApi, Gas, Ink}, user::{UserOutcome, UserOutcomeKind}, }, format, Bytes20, Bytes32, Color, @@ -48,8 +48,8 @@ fn test_ink() -> Result<()> { macro_rules! exhaust { ($ink:expr) => { - native.set_ink($ink); - assert_eq!(native.ink_left(), MachineMeter::Ready($ink)); + native.set_ink(Ink($ink)); + assert_eq!(native.ink_left(), MachineMeter::Ready(Ink($ink))); assert!(add_one.call(&mut native.store, 32).is_err()); assert_eq!(native.ink_left(), MachineMeter::Exhausted); }; @@ -59,12 +59,12 @@ fn test_ink() -> Result<()> { exhaust!(50); exhaust!(99); - let mut ink_left = 500; + let mut ink_left = Ink(500); native.set_ink(ink_left); - while ink_left > 0 { + while ink_left > Ink(0) { assert_eq!(native.ink_left(), MachineMeter::Ready(ink_left)); assert_eq!(add_one.call(&mut native.store, 64)?, 65); - ink_left -= 100; + ink_left -= Ink(100); } assert!(add_one.call(&mut native.store, 32).is_err()); assert_eq!(native.ink_left(), MachineMeter::Exhausted); @@ -198,7 +198,7 @@ fn test_import_export_safety() -> Result<()> { let mut bin = bin?; assert!(bin.clone().instrument(&compile, codehash).is_err()); compile.debug.debug_info = false; - assert!(bin.instrument(&compile, &codehash).is_err()); + assert!(bin.instrument(&compile, codehash).is_err()); if both { assert!(TestInstance::new_test(file, compile).is_err()); @@ -268,7 +268,7 @@ fn test_heap() -> Result<()> { assert_eq!(pages, 128); let used = config.pricing.ink_to_gas(ink - native.ink_ready()?); - ensure!((used as i64 - 32_000_000).abs() < 3_000, "wrong ink"); + ensure!((used.0 as i64 - 32_000_000).abs() < 3_000, "wrong ink"); assert_eq!(native.memory_size(), Pages(128)); if step == extra { @@ -283,7 +283,7 @@ fn test_heap() -> Result<()> { // the cost should exceed a maximum u32, consuming more gas than can ever be bought let (mut native, _) = TestInstance::new_with_evm("tests/memory2.wat", &compile, config)?; - let outcome = native.run_main(&[], config, config.pricing.ink_to_gas(u32::MAX.into()))?; + let outcome = native.run_main(&[], config, config.pricing.gas_to_ink(Gas(u32::MAX.into())))?; assert_eq!(outcome.kind(), UserOutcomeKind::OutOfInk); // ensure we reject programs with excessive footprints @@ -381,7 +381,7 @@ fn test_storage() -> Result<()> { let (mut native, mut evm) = TestInstance::new_with_evm(filename, &compile, config)?; run_native(&mut native, &store_args, ink)?; - assert_eq!(evm.get_bytes32(key.into()).0, Bytes32(value)); + assert_eq!(evm.get_bytes32(key.into(), Gas(0)).0, Bytes32(value)); assert_eq!(run_native(&mut native, &load_args, ink)?, value); let mut machine = Machine::from_user_path(Path::new(filename), &compile)?; @@ -465,7 +465,7 @@ fn test_calls() -> Result<()> { run_native(&mut native, &args, ink)?; for (key, value) in slots { - assert_eq!(evm.get_bytes32(key).0, value); + assert_eq!(evm.get_bytes32(key, Gas(0)).0, value); } Ok(()) } diff --git a/arbitrator/stylus/src/test/wavm.rs b/arbitrator/stylus/src/test/wavm.rs index e707cf490a..729cfebf2f 100644 --- a/arbitrator/stylus/src/test/wavm.rs +++ b/arbitrator/stylus/src/test/wavm.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::test::{new_test_machine, test_compile_config}; +use arbutil::evm::api::Ink; use eyre::Result; use prover::{programs::prelude::*, Machine}; @@ -15,8 +16,8 @@ fn test_ink() -> Result<()> { macro_rules! exhaust { ($ink:expr) => { - machine.set_ink($ink); - assert_eq!(machine.ink_left(), MachineMeter::Ready($ink)); + machine.set_ink(Ink($ink)); + assert_eq!(machine.ink_left(), MachineMeter::Ready(Ink($ink))); assert!(call(machine, 32).is_err()); assert_eq!(machine.ink_left(), MachineMeter::Exhausted); }; @@ -26,12 +27,12 @@ fn test_ink() -> Result<()> { exhaust!(50); exhaust!(99); - let mut ink_left = 500; + let mut ink_left = Ink(500); machine.set_ink(ink_left); - while ink_left > 0 { + while ink_left > Ink(0) { assert_eq!(machine.ink_left(), MachineMeter::Ready(ink_left)); assert_eq!(call(machine, 64)?, vec![65_u32.into()]); - ink_left -= 100; + ink_left -= Ink(100); } assert!(call(machine, 32).is_err()); assert_eq!(machine.ink_left(), MachineMeter::Exhausted); diff --git a/arbitrator/stylus/tests/erc20/Cargo.lock b/arbitrator/stylus/tests/erc20/Cargo.lock index c3e215978d..f5e1e0b15e 100644 --- a/arbitrator/stylus/tests/erc20/Cargo.lock +++ b/arbitrator/stylus/tests/erc20/Cargo.lock @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags", "errno", diff --git a/arbitrator/stylus/tests/hostio-test/Cargo.lock b/arbitrator/stylus/tests/hostio-test/Cargo.lock new file mode 100644 index 0000000000..1e726910b1 --- /dev/null +++ b/arbitrator/stylus/tests/hostio-test/Cargo.lock @@ -0,0 +1,636 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloy-primitives" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e416903084d3392ebd32d94735c395d6709415b76c7728e594d3f996f2b03e65" +dependencies = [ + "bytes", + "cfg-if 1.0.0", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "ruint", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ceeffdacf9dd0910404d743d07273776fd17b85f9cb17b49a97e5c6055ce9" +dependencies = [ + "dunce", + "heck", + "proc-macro2", + "quote", + "syn 2.0.77", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f347cb6bb307b3802ec455ef43ce00f5e590e0ceca3d2f3b070f5ee367e235" +dependencies = [ + "alloy-primitives", + "alloy-sol-macro", + "const-hex", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-hex" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "hex", + "proptest", + "serde", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.77", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hostio-test" +version = "0.1.0" +dependencies = [ + "mini-alloc", + "stylus-sdk", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "mini-alloc" +version = "0.4.2" +dependencies = [ + "cfg-if 1.0.0", + "wee_alloc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "ruint" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" +dependencies = [ + "proptest", + "rand", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "stylus-proc" +version = "0.4.2" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "cfg-if 1.0.0", + "convert_case 0.6.0", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "sha3", + "syn 1.0.109", + "syn-solidity", +] + +[[package]] +name = "stylus-sdk" +version = "0.4.2" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "cfg-if 1.0.0", + "derivative", + "hex", + "keccak-const", + "lazy_static", + "stylus-proc", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f995d2140b0f751dbe94365be2591edbf3d1b75dcfaeac14183abbd2ff07bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/arbitrator/stylus/tests/hostio-test/Cargo.toml b/arbitrator/stylus/tests/hostio-test/Cargo.toml new file mode 100644 index 0000000000..da7bbce7a3 --- /dev/null +++ b/arbitrator/stylus/tests/hostio-test/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "hostio-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +stylus-sdk = { path = "../../../langs/rust/stylus-sdk", features = ["debug", "hostio"] } +mini-alloc.path = "../../../langs/rust/mini-alloc" + +[profile.release] +codegen-units = 1 +strip = true +lto = true +panic = "abort" +opt-level = "s" + +[workspace] diff --git a/arbitrator/stylus/tests/hostio-test/src/main.rs b/arbitrator/stylus/tests/hostio-test/src/main.rs new file mode 100644 index 0000000000..47b46daad2 --- /dev/null +++ b/arbitrator/stylus/tests/hostio-test/src/main.rs @@ -0,0 +1,240 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +#![no_main] + +use stylus_sdk::{ + abi::Bytes, + alloy_primitives::{Address, B256, U256}, + block, console, contract, evm, hostio, msg, + prelude::*, + stylus_proc::entrypoint, + tx, + types::AddressVM, +}; +extern crate alloc; + +#[cfg(target_arch = "wasm32")] +#[global_allocator] +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; + +sol_storage! { + #[entrypoint] + pub struct HostioTest { + } +} + +type Result = std::result::Result>; + +// These are not available as hostios in the sdk, so we import them directly. +#[link(wasm_import_module = "vm_hooks")] +extern "C" { + fn math_div(value: *mut u8, divisor: *const u8); + fn math_mod(value: *mut u8, modulus: *const u8); + fn math_pow(value: *mut u8, exponent: *const u8); + fn math_add_mod(value: *mut u8, addend: *const u8, modulus: *const u8); + fn math_mul_mod(value: *mut u8, multiplier: *const u8, modulus: *const u8); + fn transient_load_bytes32(key: *const u8, dest: *mut u8); + fn transient_store_bytes32(key: *const u8, value: *const u8); + fn exit_early(status: u32); +} + +#[external] +impl HostioTest { + fn exit_early() -> Result<()> { + unsafe { + exit_early(0); + } + Ok(()) + } + + fn transient_load_bytes32(key: B256) -> Result { + let mut result = B256::ZERO; + unsafe { + transient_load_bytes32(key.as_ptr(), result.as_mut_ptr()); + } + Ok(result) + } + + fn transient_store_bytes32(key: B256, value: B256) { + unsafe { + transient_store_bytes32(key.as_ptr(), value.as_ptr()); + } + } + + fn return_data_size() -> Result { + unsafe { Ok(hostio::return_data_size().try_into().unwrap()) } + } + + fn emit_log(data: Bytes, n: i8, t1: B256, t2: B256, t3: B256, t4: B256) -> Result<()> { + let topics = &[t1, t2, t3, t4]; + evm::raw_log(&topics[0..n as usize], data.as_slice())?; + Ok(()) + } + + fn account_balance(account: Address) -> Result { + Ok(account.balance()) + } + + fn account_code(account: Address) -> Result> { + let mut size = 10000; + let mut code = vec![0; size]; + unsafe { + size = hostio::account_code(account.as_ptr(), 0, size, code.as_mut_ptr()); + } + code.resize(size, 0); + Ok(code) + } + + fn account_code_size(account: Address) -> Result { + Ok(account.code_size().try_into().unwrap()) + } + + fn account_codehash(account: Address) -> Result { + Ok(account.codehash()) + } + + fn evm_gas_left() -> Result { + Ok(evm::gas_left().try_into().unwrap()) + } + + fn evm_ink_left() -> Result { + Ok(tx::ink_to_gas(evm::ink_left()).try_into().unwrap()) + } + + fn block_basefee() -> Result { + Ok(block::basefee()) + } + + fn chainid() -> Result { + Ok(block::chainid().try_into().unwrap()) + } + + fn block_coinbase() -> Result
{ + Ok(block::coinbase()) + } + + fn block_gas_limit() -> Result { + Ok(block::gas_limit().try_into().unwrap()) + } + + fn block_number() -> Result { + Ok(block::number().try_into().unwrap()) + } + + fn block_timestamp() -> Result { + Ok(block::timestamp().try_into().unwrap()) + } + + fn contract_address() -> Result
{ + Ok(contract::address()) + } + + fn math_div(a: U256, b: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + unsafe { + math_div(a_bytes.as_mut_ptr(), b_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_mod(a: U256, b: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + unsafe { + math_mod(a_bytes.as_mut_ptr(), b_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_pow(a: U256, b: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + unsafe { + math_pow(a_bytes.as_mut_ptr(), b_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_add_mod(a: U256, b: U256, c: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + let c_bytes: B256 = c.into(); + unsafe { + math_add_mod(a_bytes.as_mut_ptr(), b_bytes.as_ptr(), c_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_mul_mod(a: U256, b: U256, c: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + let c_bytes: B256 = c.into(); + unsafe { + math_mul_mod(a_bytes.as_mut_ptr(), b_bytes.as_ptr(), c_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn msg_sender() -> Result
{ + Ok(msg::sender()) + } + + fn msg_value() -> Result { + Ok(msg::value()) + } + + fn keccak(preimage: Bytes) -> Result { + let mut result = B256::ZERO; + unsafe { + hostio::native_keccak256(preimage.as_ptr(), preimage.len(), result.as_mut_ptr()); + } + Ok(result) + } + + fn tx_gas_price() -> Result { + Ok(tx::gas_price()) + } + + fn tx_ink_price() -> Result { + Ok(tx::ink_to_gas(tx::ink_price().into()).try_into().unwrap()) + } + + fn tx_origin() -> Result
{ + Ok(tx::origin()) + } + + fn storage_cache_bytes32() { + let key = B256::ZERO; + let val = B256::ZERO; + unsafe { + hostio::storage_cache_bytes32(key.as_ptr(), val.as_ptr()); + } + } + + fn pay_for_memory_grow(pages: U256) { + let pages: u16 = pages.try_into().unwrap(); + unsafe { + hostio::pay_for_memory_grow(pages); + } + } + + fn write_result_empty() { + } + + fn write_result(size: U256) -> Result> { + let size: usize = size.try_into().unwrap(); + let data = vec![0; size]; + Ok(data) + } + + fn read_args_no_args() { + } + + fn read_args_one_arg(_arg1: U256) { + } + + fn read_args_three_args(_arg1: U256, _arg2: U256, _arg3: U256) { + } +} diff --git a/arbitrator/stylus/tests/write-result-len.wat b/arbitrator/stylus/tests/write-result-len.wat new file mode 100644 index 0000000000..4c9ad35087 --- /dev/null +++ b/arbitrator/stylus/tests/write-result-len.wat @@ -0,0 +1,24 @@ +;; Copyright 2024, Offchain Labs, Inc. +;; For license information, see https://github.com/nitro/blob/master/LICENSE + +(module + (import "vm_hooks" "read_args" (func $read_args (param i32))) + (import "vm_hooks" "write_result" (func $write_result (param i32 i32))) + (memory (export "memory") 2 2) + (func $main (export "user_entrypoint") (param $args_len i32) (result i32) + (local $len i32) + + ;; write args to 0x0 + (call $read_args (i32.const 0)) + + ;; treat first 4 bytes as size to write + (i32.load (i32.const 0)) + local.set $len + + ;; call write + (call $write_result (i32.const 0) (local.get $len)) + + ;; return success + i32.const 0 + ) +) diff --git a/arbitrator/tools/stylus_benchmark/Cargo.lock b/arbitrator/tools/stylus_benchmark/Cargo.lock new file mode 100644 index 0000000000..fe3ff5e908 --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/Cargo.lock @@ -0,0 +1,2670 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli 0.31.1", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "allocator-api2" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "arbutil" +version = "0.1.0" +dependencies = [ + "digest 0.10.7", + "eyre", + "fnv", + "hex", + "num-traits", + "num_enum", + "ruint2", + "serde", + "sha2 0.10.8", + "sha3 0.10.8", + "siphasher", + "tiny-keccak", + "wasmparser", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "blst" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "brotli" +version = "0.1.0" +dependencies = [ + "lazy_static", + "num_enum", + "wasmer", + "wee_alloc", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "c-kzg" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a4bc5367b6284358d2a6a6a1dc2d92ec4b86034561c3b9d3341909752fd848" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "serde", +] + +[[package]] +name = "caller-env" +version = "0.1.0" +dependencies = [ + "brotli", + "num_enum", + "rand", + "rand_pcg", + "wasmer", +] + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "corosensei" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80128832c58ea9cbd041d2a759ec449224487b2c1e400453d99d244eead87a8e" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "libc", + "scopeguard", + "windows-sys 0.33.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2ab4512dfd3a6f4be184403a195f76e81a8a9f9e6c898e19d2dc3ce20e0115" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b022ed2a5913a38839dfbafe6cf135342661293b08049843362df4301261dc" +dependencies = [ + "arrayvec", + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-egraph", + "cranelift-entity", + "cranelift-isle", + "gimli 0.26.2", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639307b45434ad112a98f8300c0f0ab085cbefcd767efcdef9ef19d4c0756e74" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278e52e29c53fcf32431ef08406c295699a70306d05a0715c5b1bf50e33a9ab7" + +[[package]] +name = "cranelift-egraph" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624b54323b06e675293939311943ba82d323bb340468ce1889be5da7932c8d73" +dependencies = [ + "cranelift-entity", + "fxhash", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "log", + "smallvec", +] + +[[package]] +name = "cranelift-entity" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a59bcbca89c3f1b70b93ab3cbba5e5e0cbf3e63dadb23c7525cb142e21a9d4c" + +[[package]] +name = "cranelift-frontend" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d70abacb8cfef3dc8ff7e8836e9c1d70f7967dfdac824a4cd5e30223415aca6" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393bc73c451830ff8dbb3a07f61843d6cb41a084f9996319917c0b291ed785bb" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.87", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + +[[package]] +name = "dynasm" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add9a102807b524ec050363f09e06f1504214b0e1c7797f64261c891022dce8b" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dynasmrt" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fba5a42bd76a17cad4bfa00de168ee1cbfa06a5e8ce992ae880218c05641a9" +dependencies = [ + "byteorder", + "dynasm", + "memmap2 0.5.10", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive 0.7.0", +] + +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive 1.4.0", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "enumset" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +dependencies = [ + "fallible-iterator", + "indexmap 1.9.3", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.1", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jit" +version = "0.1.0" +dependencies = [ + "arbutil", + "brotli", + "caller-env", + "eyre", + "hex", + "libc", + "parking_lot", + "prover", + "rand", + "rand_pcg", + "sha2 0.9.9", + "sha3 0.9.1", + "structopt", + "stylus", + "thiserror", + "wasmer", + "wasmer-compiler-cranelift", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.1", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "more-asserts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom-leb128" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a73b6c3a9ecfff12ad50dedba051ef838d2f478d938bb3e6b3842431028e62" +dependencies = [ + "arrayvec", + "nom", + "num-traits", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prover" +version = "0.1.0" +dependencies = [ + "arbutil", + "bincode", + "bitvec", + "brotli", + "c-kzg", + "derivative", + "digest 0.9.0", + "enum-iterator 2.1.0", + "eyre", + "fnv", + "hex", + "itertools", + "lazy_static", + "libc", + "lru", + "nom", + "nom-leb128", + "num", + "num-derive", + "num-traits", + "once_cell", + "parking_lot", + "rustc-demangle", + "serde", + "serde_json", + "serde_with", + "sha2 0.9.9", + "sha3 0.9.1", + "smallvec", + "static_assertions", + "structopt", + "wasmer", + "wasmer-compiler-singlepass", + "wasmer-types", + "wasmparser", + "wat", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regalloc2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300d4fbfb40c1c66a78ba3ddd41c1110247cf52f97b87d0f2fc9209bd49b030c" +dependencies = [ + "fxhash", + "log", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ruint2" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b066b8e4fcea7fae86b6932d2449670b6b5545b7e8407841b2d3a916ff2a9f86" +dependencies = [ + "derive_more", + "ruint2-macro", + "rustc_version", + "thiserror", +] + +[[package]] +name = "ruint2-macro" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dc553bc0cf4512a8b96caa2e21ed5f6e4b66bf28a1bd08fd9eb07c0b39b28c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap 2.34.0", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.87", +] + +[[package]] +name = "stylus" +version = "0.1.0" +dependencies = [ + "arbutil", + "bincode", + "brotli", + "caller-env", + "clru", + "derivative", + "eyre", + "fnv", + "hex", + "lazy_static", + "libc", + "parking_lot", + "prover", + "rand", + "thiserror", + "user-host-trait", + "wasmer", + "wasmer-compiler-cranelift", + "wasmer-compiler-singlepass", + "wasmer-types", + "wasmer-vm", +] + +[[package]] +name = "stylus_benchmark" +version = "0.1.0" +dependencies = [ + "arbutil", + "clap 4.5.21", + "eyre", + "jit", + "prover", + "strum", + "strum_macros", + "stylus", + "wasmer", + "wasmer-compiler-cranelift", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "user-host-trait" +version = "0.1.0" +dependencies = [ + "arbutil", + "caller-env", + "eyre", + "prover", + "ruint2", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wasm-encoder" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba64e81215916eaeb48fee292f29401d69235d62d8b8fd92a7b2844ec5ae5f7" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasmer" +version = "4.2.8" +dependencies = [ + "bytes", + "cfg-if 1.0.0", + "derivative", + "indexmap 1.9.3", + "js-sys", + "more-asserts", + "rustc-demangle", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "target-lexicon", + "thiserror", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-compiler-cranelift", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "wat", + "winapi", +] + +[[package]] +name = "wasmer-compiler" +version = "4.2.8" +dependencies = [ + "backtrace", + "bytes", + "cfg-if 1.0.0", + "enum-iterator 0.7.0", + "enumset", + "lazy_static", + "leb128", + "memmap2 0.5.10", + "more-asserts", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "thiserror", + "wasmer-types", + "wasmer-vm", + "wasmparser", + "winapi", +] + +[[package]] +name = "wasmer-compiler-cranelift" +version = "4.2.8" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "gimli 0.26.2", + "more-asserts", + "rayon", + "smallvec", + "target-lexicon", + "tracing", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-compiler-singlepass" +version = "4.2.8" +dependencies = [ + "byteorder", + "dynasm", + "dynasmrt", + "enumset", + "gimli 0.26.2", + "lazy_static", + "more-asserts", + "smallvec", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-derive" +version = "4.2.8" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wasmer-types" +version = "4.2.8" +dependencies = [ + "bytecheck", + "enum-iterator 0.7.0", + "enumset", + "indexmap 1.9.3", + "more-asserts", + "rkyv", + "target-lexicon", + "thiserror", +] + +[[package]] +name = "wasmer-vm" +version = "4.2.8" +dependencies = [ + "backtrace", + "cc", + "cfg-if 1.0.0", + "corosensei", + "crossbeam-queue", + "dashmap", + "derivative", + "enum-iterator 0.7.0", + "fnv", + "indexmap 1.9.3", + "lazy_static", + "libc", + "mach", + "memoffset", + "more-asserts", + "region", + "scopeguard", + "thiserror", + "wasmer-types", + "winapi", +] + +[[package]] +name = "wasmparser" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" +dependencies = [ + "bitflags 2.6.0", + "indexmap 2.6.0", + "semver", +] + +[[package]] +name = "wast" +version = "64.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a259b226fd6910225aa7baeba82f9d9933b6d00f2ce1b49b80fa4214328237cc" +dependencies = [ + "leb128", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53253d920ab413fca1c7dc2161d601c79b4fdf631d0ba51dd4343bf9b556c3f6" +dependencies = [ + "wast", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43dbb096663629518eb1dfa72d80243ca5a6aca764cae62a2df70af760a9be75" +dependencies = [ + "windows_aarch64_msvc 0.33.0", + "windows_i686_gnu 0.33.0", + "windows_i686_msvc 0.33.0", + "windows_x86_64_gnu 0.33.0", + "windows_x86_64_msvc 0.33.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd761fd3eb9ab8cc1ed81e56e567f02dd82c4c837e48ac3b2181b9ffc5060807" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab0cf703a96bab2dc0c02c0fa748491294bf9b7feb27e1f4f96340f208ada0e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfdbe89cc9ad7ce618ba34abc34bbb6c36d99e96cae2245b7943cd75ee773d0" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4dd9b0c0e9ece7bb22e84d70d01b71c6d6248b81a3c60d11869451b4cb24784" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1e4aa646495048ec7f3ffddc411e1d829c026a2ec62b39da15c1055e406eaa" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/arbitrator/tools/stylus_benchmark/Cargo.toml b/arbitrator/tools/stylus_benchmark/Cargo.toml new file mode 100644 index 0000000000..e193fc0ca8 --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "stylus_benchmark" +version = "0.1.0" +edition = "2021" + +[dependencies] +eyre = "0.6.12" +jit = { path = "../../jit/" } +wasmer = { path = "../wasmer/lib/api/" } +wasmer-compiler-cranelift = { path = "../wasmer/lib/compiler-cranelift/" } +arbutil = { path = "../../arbutil/" } +prover = { path = "../../prover/", default-features = false, features = ["native"] } +stylus = { path = "../../stylus/", default-features = false } +clap = { version = "4.4.8", features = ["derive"] } +strum = "0.26" +strum_macros = "0.26" diff --git a/arbitrator/tools/stylus_benchmark/src/benchmark.rs b/arbitrator/tools/stylus_benchmark/src/benchmark.rs new file mode 100644 index 0000000000..43f7b7553a --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/src/benchmark.rs @@ -0,0 +1,105 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use arbutil::evm::{api::Ink, EvmData}; +use core::time::Duration; +use jit::machine::WasmEnv; +use jit::program::JitConfig; +use prover::programs::{config::CompileConfig, config::PricingParams, prelude::StylusConfig}; +use std::str; +use stylus::native; +use wasmer::Target; + +const EVM_API_METHOD_REQ_OFFSET: u32 = 0x10000000; + +const NUMBER_OF_BENCHMARK_RUNS: u32 = 7; +const NUMBER_OF_TOP_AND_BOTTOM_RUNS_TO_DISCARD: u32 = 2; + +fn check_result(req_type: u32, req_data: &Vec) { + let _ = match str::from_utf8(req_data) { + Ok(v) => v, + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }; + + match req_type { + 0 => return, + 1 => panic!("ErrExecutionReverted user revert"), + 2 => panic!("ErrExecutionReverted user failure"), + 3 => panic!("ErrOutOfGas user out of ink"), + 4 => panic!("ErrDepth user out of stack"), + _ => panic!("ErrExecutionReverted user unknown"), + } +} + +fn run(compiled_module: Vec) -> (Duration, Ink) { + let calldata = Vec::from([0u8; 32]); + let evm_data = EvmData::default(); + let config = JitConfig { + stylus: StylusConfig { + version: 2, + max_depth: 10000, + pricing: PricingParams { ink_price: 1 }, + }, + compile: CompileConfig::version(2, true), + }; + + let exec = &mut WasmEnv::default(); + + let module = jit::program::exec_program( + exec, + compiled_module.into(), + calldata, + config, + evm_data, + u64::MAX, + ) + .unwrap(); + + let req_id = jit::program::start_program_with_wasm_env(exec, module).unwrap(); + let msg = jit::program::get_last_msg(exec, req_id).unwrap(); + if msg.req_type < EVM_API_METHOD_REQ_OFFSET { + let _ = jit::program::pop_with_wasm_env(exec); + + let req_data = msg.req_data[8..].to_vec(); + check_result(msg.req_type, &req_data); + } else { + panic!("unsupported request type {:?}", msg.req_type); + } + + (msg.benchmark.elapsed_total, msg.benchmark.ink_total) +} + +pub fn benchmark(wat: Vec) -> eyre::Result<()> { + let wasm = wasmer::wat2wasm(&wat)?; + + let compiled_module = native::compile(&wasm, 2, true, Target::default())?; + + let mut durations: Vec = Vec::new(); + let mut ink_spent = Ink(0); + for i in 0..NUMBER_OF_BENCHMARK_RUNS { + print!("Run {:?}, ", i); + let (duration_run, ink_spent_run) = run(compiled_module.clone()); + durations.push(duration_run); + ink_spent = ink_spent_run; + println!( + "duration: {:?}, ink_spent: {:?}", + duration_run, ink_spent_run + ); + } + + // discard top and bottom runs + durations.sort(); + let l = NUMBER_OF_TOP_AND_BOTTOM_RUNS_TO_DISCARD as usize; + let r = NUMBER_OF_BENCHMARK_RUNS as usize - NUMBER_OF_TOP_AND_BOTTOM_RUNS_TO_DISCARD as usize; + durations = durations[l..r].to_vec(); + + let avg_duration = durations.iter().sum::() / (r - l) as u32; + let avg_ink_spent_per_micro_second = ink_spent.0 / avg_duration.as_micros() as u64; + println!("After discarding top and bottom runs: "); + println!( + "avg_duration: {:?}, avg_ink_spent_per_micro_second: {:?}", + avg_duration, avg_ink_spent_per_micro_second + ); + + Ok(()) +} diff --git a/arbitrator/tools/stylus_benchmark/src/main.rs b/arbitrator/tools/stylus_benchmark/src/main.rs new file mode 100644 index 0000000000..4b8971ecab --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/src/main.rs @@ -0,0 +1,44 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +mod benchmark; +mod scenario; + +use clap::Parser; +use scenario::Scenario; +use std::path::PathBuf; +use strum::IntoEnumIterator; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long)] + output_wat_dir_path: Option, + + #[arg(short, long)] + scenario: Option, +} + +fn handle_scenario(scenario: Scenario, output_wat_dir_path: Option) -> eyre::Result<()> { + println!("Benchmarking {}", scenario); + let wat = scenario::generate_wat(scenario, output_wat_dir_path); + benchmark::benchmark(wat) +} + +fn main() -> eyre::Result<()> { + let args = Args::parse(); + + match args.scenario { + Some(scenario) => handle_scenario(scenario, args.output_wat_dir_path), + None => { + println!("No scenario specified, benchmarking all scenarios\n"); + for scenario in Scenario::iter() { + let benchmark_result = handle_scenario(scenario, args.output_wat_dir_path.clone()); + if let Err(err) = benchmark_result { + return Err(err); + } + } + Ok(()) + } + } +} diff --git a/arbitrator/tools/stylus_benchmark/src/scenario.rs b/arbitrator/tools/stylus_benchmark/src/scenario.rs new file mode 100644 index 0000000000..348678ed69 --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/src/scenario.rs @@ -0,0 +1,128 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use strum_macros::{Display, EnumIter, EnumString}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, EnumString, Display, EnumIter)] +pub enum Scenario { + #[strum(serialize = "add_i32")] + AddI32, + #[strum(serialize = "xor_i32")] + XorI32, +} + +// Programs to be benchmarked have a loop in which several similar operations are executed. +// The number of operations per loop is chosen to be large enough so the overhead related to the loop is negligible, +// but not too large to avoid a big program size. +// Keeping a small program size is important to better use CPU cache, trying to keep the code in the cache. + +fn write_wat_beginning(wat: &mut Vec) { + wat.write_all(b"(module\n").unwrap(); + wat.write_all(b" (import \"debug\" \"start_benchmark\" (func $start_benchmark))\n") + .unwrap(); + wat.write_all(b" (import \"debug\" \"end_benchmark\" (func $end_benchmark))\n") + .unwrap(); + wat.write_all(b" (memory (export \"memory\") 0 0)\n") + .unwrap(); + wat.write_all(b" (global $ops_counter (mut i32) (i32.const 0))\n") + .unwrap(); + wat.write_all(b" (func (export \"user_entrypoint\") (param i32) (result i32)\n") + .unwrap(); + + wat.write_all(b" call $start_benchmark\n").unwrap(); + + wat.write_all(b" (loop $loop\n").unwrap(); +} + +fn write_wat_end( + wat: &mut Vec, + number_of_loop_iterations: usize, + number_of_ops_per_loop_iteration: usize, +) { + let number_of_ops = number_of_loop_iterations * number_of_ops_per_loop_iteration; + + // update ops_counter + wat.write_all(b" global.get $ops_counter\n") + .unwrap(); + wat.write_all( + format!( + " i32.const {}\n", + number_of_ops_per_loop_iteration + ) + .as_bytes(), + ) + .unwrap(); + wat.write_all(b" i32.add\n").unwrap(); + wat.write_all(b" global.set $ops_counter\n") + .unwrap(); + + // check if we need to continue looping + wat.write_all(b" global.get $ops_counter\n") + .unwrap(); + wat.write_all(format!(" i32.const {}\n", number_of_ops).as_bytes()) + .unwrap(); + wat.write_all(b" i32.lt_s\n").unwrap(); + wat.write_all(b" br_if $loop)\n").unwrap(); + + wat.write_all(b" call $end_benchmark\n").unwrap(); + + wat.write_all(b" i32.const 0)\n").unwrap(); + wat.write_all(b")").unwrap(); +} + +fn wat(write_wat_ops: fn(&mut Vec, usize)) -> Vec { + let number_of_loop_iterations = 200_000; + let number_of_ops_per_loop_iteration = 2000; + + let mut wat = Vec::new(); + + write_wat_beginning(&mut wat); + + write_wat_ops(&mut wat, number_of_ops_per_loop_iteration); + + write_wat_end( + &mut wat, + number_of_loop_iterations, + number_of_ops_per_loop_iteration, + ); + + wat.to_vec() +} + +fn write_add_i32_wat_ops(wat: &mut Vec, number_of_ops_per_loop_iteration: usize) { + wat.write_all(b" i32.const 0\n").unwrap(); + for _ in 0..number_of_ops_per_loop_iteration { + wat.write_all(b" i32.const 1\n").unwrap(); + wat.write_all(b" i32.add\n").unwrap(); + } + wat.write_all(b" drop\n").unwrap(); +} + +fn write_xor_i32_wat_ops(wat: &mut Vec, number_of_ops_per_loop_iteration: usize) { + wat.write_all(b" i32.const 1231\n").unwrap(); + for _ in 0..number_of_ops_per_loop_iteration { + wat.write_all(b" i32.const 12312313\n").unwrap(); + wat.write_all(b" i32.xor\n").unwrap(); + } + wat.write_all(b" drop\n").unwrap(); +} + +pub fn generate_wat(scenario: Scenario, output_wat_dir_path: Option) -> Vec { + let wat = match scenario { + Scenario::AddI32 => wat(write_add_i32_wat_ops), + Scenario::XorI32 => wat(write_xor_i32_wat_ops), + }; + + // print wat to file if needed + if let Some(output_wat_dir_path) = output_wat_dir_path { + let mut output_wat_path = output_wat_dir_path; + output_wat_path.push(format!("{}.wat", scenario)); + let mut file = File::create(output_wat_path).unwrap(); + file.write_all(&wat).unwrap(); + } + + wat +} diff --git a/arbitrator/wasm-libraries/Cargo.lock b/arbitrator/wasm-libraries/Cargo.lock index 7620ff538b..a5a066e5c9 100644 --- a/arbitrator/wasm-libraries/Cargo.lock +++ b/arbitrator/wasm-libraries/Cargo.lock @@ -31,6 +31,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -91,6 +106,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bincode" version = "1.3.3" @@ -203,6 +224,15 @@ dependencies = [ "rand_pcg", ] +[[package]] +name = "cc" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -215,6 +245,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "clap" version = "2.34.0" @@ -236,6 +279,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -261,38 +310,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - [[package]] name = "darling" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -305,29 +330,29 @@ dependencies = [ "ident_case", "proc-macro2", "quote", + "strsim 0.11.1", "syn 2.0.72", ] [[package]] name = "darling_macro" -version = "0.13.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core 0.13.4", + "darling_core", "quote", - "syn 1.0.109", + "syn 2.0.72", ] [[package]] -name = "darling_macro" -version = "0.20.10" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ - "darling_core 0.20.10", - "quote", - "syn 2.0.72", + "powerfmt", + "serde", ] [[package]] @@ -434,7 +459,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" dependencies = [ - "darling 0.20.10", + "darling", "proc-macro2", "quote", "syn 2.0.72", @@ -548,6 +573,29 @@ dependencies = [ "caller-env", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -568,6 +616,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -578,6 +627,7 @@ checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -595,6 +645,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "keccak" version = "0.1.5" @@ -632,6 +691,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "lru" version = "0.12.4" @@ -719,6 +784,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -832,6 +903,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -1115,24 +1192,32 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.14.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.3.0", "serde", + "serde_derive", + "serde_json", "serde_with_macros", + "time", ] [[package]] name = "serde_with_macros" -version = "1.5.2" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ - "darling 0.13.4", + "darling", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.72", ] [[package]] @@ -1181,6 +1266,12 @@ dependencies = [ "keccak", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simdutf8" version = "0.1.4" @@ -1216,9 +1307,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "structopt" @@ -1307,6 +1398,37 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -1445,6 +1567,61 @@ dependencies = [ "wee_alloc", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.72", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + [[package]] name = "wasm-encoder" version = "0.215.0" @@ -1535,6 +1712,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/arbitrator/wasm-libraries/forward/src/main.rs b/arbitrator/wasm-libraries/forward/src/main.rs index 05a949e8aa..f978a8723b 100644 --- a/arbitrator/wasm-libraries/forward/src/main.rs +++ b/arbitrator/wasm-libraries/forward/src/main.rs @@ -191,7 +191,8 @@ fn forward_stub(file: &mut File) -> Result<()> { "{s};; allows user_host to request a trap\n\ {s}(global $trap (mut i32) (i32.const 0))\n\ {s}(func $check unreachable)\n\ - {s}(func (export \"forward__set_trap\") unreachable)" + {s};; stub for the forward__set_trap function\n\ + {s}(func $forward__set_trap unreachable)" ); wln!("{s};; user linkage"); diff --git a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs index 37af85c382..25163e25bc 100644 --- a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs +++ b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs @@ -2,13 +2,14 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use arbutil::{ + benchmark::Benchmark, crypto, evm::{ self, - api::{DataReader, EvmApi}, + api::{DataReader, EvmApi, Gas, Ink}, storage::StorageCache, user::UserOutcomeKind, - EvmData, + EvmData, ARBOS_VERSION_STYLUS_CHARGING_FIXES, }, pricing::{self, EVM_API_INK, HOSTIO_INK, PTR_INK}, Bytes20, Bytes32, @@ -21,6 +22,7 @@ use prover::{ }; use ruint2::Uint; use std::fmt::Display; +use std::time::Instant; macro_rules! be { ($int:expr) => { @@ -68,6 +70,7 @@ pub trait UserHost: GasMeteredMachine { fn evm_api(&mut self) -> &mut Self::A; fn evm_data(&self) -> &EvmData; + fn benchmark(&mut self) -> &mut Benchmark; fn evm_return_data_len(&mut self) -> &mut u32; fn read_slice(&self, ptr: GuestPtr, len: u32) -> Result, Self::MemoryErr>; @@ -88,7 +91,7 @@ pub trait UserHost: GasMeteredMachine { } fn say(&self, text: D); - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: u64); + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], end_ink: Ink); fn write_bytes20(&self, ptr: GuestPtr, src: Bytes20) -> Result<(), Self::MemoryErr> { self.write_slice(ptr, &src.0) @@ -143,11 +146,20 @@ pub trait UserHost: GasMeteredMachine { /// [`SLOAD`]: https://www.evm.codes/#54 fn storage_load_bytes32(&mut self, key: GuestPtr, dest: GuestPtr) -> Result<(), Self::Err> { self.buy_ink(HOSTIO_INK + 2 * PTR_INK)?; - self.require_gas(evm::COLD_SLOAD_GAS + EVM_API_INK + StorageCache::REQUIRED_ACCESS_GAS)?; // cache-miss case + let arbos_version = self.evm_data().arbos_version; + // require for cache-miss case, preserve wrong behavior for old arbos + let evm_api_gas_to_use = if arbos_version < ARBOS_VERSION_STYLUS_CHARGING_FIXES { + Gas(EVM_API_INK.0) + } else { + self.pricing().ink_to_gas(EVM_API_INK) + }; + self.require_gas( + evm::COLD_SLOAD_GAS + StorageCache::REQUIRED_ACCESS_GAS + evm_api_gas_to_use, + )?; let key = self.read_bytes32(key)?; - let (value, gas_cost) = self.evm_api().get_bytes32(key); + let (value, gas_cost) = self.evm_api().get_bytes32(key, evm_api_gas_to_use); self.buy_gas(gas_cost)?; self.write_bytes32(dest, value)?; trace!("storage_load_bytes32", self, key, value) @@ -185,7 +197,10 @@ pub trait UserHost: GasMeteredMachine { self.require_gas(evm::SSTORE_SENTRY_GAS)?; // see operations_acl_arbitrum.go let gas_left = self.gas_left()?; - self.evm_api().flush_storage_cache(clear, gas_left)?; + let gas_cost = self.evm_api().flush_storage_cache(clear, gas_left)?; + if self.evm_data().arbos_version >= ARBOS_VERSION_STYLUS_CHARGING_FIXES { + self.buy_gas(gas_cost)?; + } trace!("storage_flush_cache", self, [be!(clear as u8)], &[]) } @@ -241,7 +256,7 @@ pub trait UserHost: GasMeteredMachine { data: GuestPtr, data_len: u32, value: GuestPtr, - gas: u64, + gas: Gas, ret_len: GuestPtr, ) -> Result { let value = Some(value); @@ -270,7 +285,7 @@ pub trait UserHost: GasMeteredMachine { contract: GuestPtr, data: GuestPtr, data_len: u32, - gas: u64, + gas: Gas, ret_len: GuestPtr, ) -> Result { let call = |api: &mut Self::A, contract, data: &_, left, req, _| { @@ -300,7 +315,7 @@ pub trait UserHost: GasMeteredMachine { contract: GuestPtr, data: GuestPtr, data_len: u32, - gas: u64, + gas: Gas, ret_len: GuestPtr, ) -> Result { let call = |api: &mut Self::A, contract, data: &_, left, req, _| { @@ -317,7 +332,7 @@ pub trait UserHost: GasMeteredMachine { calldata: GuestPtr, calldata_len: u32, value: Option, - gas: u64, + gas: Gas, return_data_len: GuestPtr, call: F, name: &str, @@ -327,10 +342,10 @@ pub trait UserHost: GasMeteredMachine { &mut Self::A, Address, &[u8], - u64, - u64, + Gas, + Gas, Option, - ) -> (u32, u64, UserOutcomeKind), + ) -> (u32, Gas, UserOutcomeKind), { self.buy_ink(HOSTIO_INK + 3 * PTR_INK + EVM_API_INK)?; self.pay_for_read(calldata_len)?; @@ -453,12 +468,12 @@ pub trait UserHost: GasMeteredMachine { salt: Option, contract: GuestPtr, revert_data_len: GuestPtr, - cost: u64, + cost: Ink, call: F, name: &str, ) -> Result<(), Self::Err> where - F: FnOnce(&mut Self::A, Vec, Bytes32, Option, u64) -> (Result
, u32, u64), + F: FnOnce(&mut Self::A, Vec, Bytes32, Option, Gas) -> (Result
, u32, Gas), { self.buy_ink(HOSTIO_INK + cost)?; self.pay_for_read(code_len)?; @@ -733,7 +748,7 @@ pub trait UserHost: GasMeteredMachine { /// equivalent to that of the EVM's [`GAS`] opcode. /// /// [`GAS`]: https://www.evm.codes/#5a - fn evm_gas_left(&mut self) -> Result { + fn evm_gas_left(&mut self) -> Result { self.buy_ink(HOSTIO_INK)?; let gas = self.gas_left()?; trace!("evm_gas_left", self, &[], be!(gas), gas) @@ -745,7 +760,7 @@ pub trait UserHost: GasMeteredMachine { /// /// [`GAS`]: https://www.evm.codes/#5a /// [`Ink and Gas`]: https://developer.arbitrum.io/TODO - fn evm_ink_left(&mut self) -> Result { + fn evm_ink_left(&mut self) -> Result { self.buy_ink(HOSTIO_INK)?; let ink = self.ink_ready()?; trace!("evm_ink_left", self, &[], be!(ink), ink) @@ -924,7 +939,7 @@ pub trait UserHost: GasMeteredMachine { fn pay_for_memory_grow(&mut self, pages: u16) -> Result<(), Self::Err> { if pages == 0 { self.buy_ink(HOSTIO_INK)?; - return Ok(()); + return trace!("pay_for_memory_grow", self, be!(pages), &[]); } let gas_cost = self.evm_api().add_pages(pages); // no sentry needed since the work happens after the hostio self.buy_gas(gas_cost)?; @@ -950,4 +965,38 @@ pub trait UserHost: GasMeteredMachine { self.say(value.into()); Ok(value) } + + // Initializes benchmark data related to a code block. + // A code block is defined by the instructions between start_benchmark and end_benchmark calls. + // If start_benchmark is called multiple times without end_benchmark being called, + // then only the last start_benchmark before end_benchmark will be used. + // It is possible to have multiple code blocks benchmarked in the same program. + fn start_benchmark(&mut self) -> Result<(), Self::Err> { + let ink_curr = self.ink_ready()?; + + let benchmark = self.benchmark(); + benchmark.timer = Some(Instant::now()); + benchmark.ink_start = Some(ink_curr); + + Ok(()) + } + + // Updates cumulative benchmark data related to a code block. + // If end_benchmark is called without a corresponding start_benchmark nothing will happen. + fn end_benchmark(&mut self) -> Result<(), Self::Err> { + let ink_curr = self.ink_ready()?; + + let benchmark = self.benchmark(); + if let Some(timer) = benchmark.timer { + benchmark.elapsed_total = benchmark.elapsed_total.saturating_add(timer.elapsed()); + + let code_block_ink = benchmark.ink_start.unwrap().saturating_sub(ink_curr); + benchmark.ink_total = benchmark.ink_total.saturating_add(code_block_ink); + + benchmark.timer = None; + benchmark.ink_start = None; + }; + + Ok(()) + } } diff --git a/arbitrator/wasm-libraries/user-host/src/host.rs b/arbitrator/wasm-libraries/user-host/src/host.rs index abe55b8c12..5ec2ece2c3 100644 --- a/arbitrator/wasm-libraries/user-host/src/host.rs +++ b/arbitrator/wasm-libraries/user-host/src/host.rs @@ -2,7 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::program::Program; -use arbutil::evm::user::UserOutcomeKind; +use arbutil::evm::{api::Gas, user::UserOutcomeKind}; use caller_env::GuestPtr; use user_host_trait::UserHost; @@ -77,7 +77,14 @@ pub unsafe extern "C" fn user_host__call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(call_contract(contract, data, data_len, value, gas, ret_len)) + hostio!(call_contract( + contract, + data, + data_len, + value, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -89,7 +96,11 @@ pub unsafe extern "C" fn user_host__delegate_call_contract( ret_len: GuestPtr, ) -> u8 { hostio!(delegate_call_contract( - contract, data, data_len, gas, ret_len + contract, + data, + data_len, + Gas(gas), + ret_len )) } @@ -101,7 +112,13 @@ pub unsafe extern "C" fn user_host__static_call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(static_call_contract(contract, data, data_len, gas, ret_len)) + hostio!(static_call_contract( + contract, + data, + data_len, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -207,12 +224,12 @@ pub unsafe extern "C" fn user_host__contract_address(ptr: GuestPtr) { #[no_mangle] pub unsafe extern "C" fn user_host__evm_gas_left() -> u64 { - hostio!(evm_gas_left()) + hostio!(evm_gas_left()).0 } #[no_mangle] pub unsafe extern "C" fn user_host__evm_ink_left() -> u64 { - hostio!(evm_ink_left()) + hostio!(evm_ink_left()).0 } #[no_mangle] diff --git a/arbitrator/wasm-libraries/user-host/src/ink.rs b/arbitrator/wasm-libraries/user-host/src/ink.rs index e01e616e07..bde7cfc1c0 100644 --- a/arbitrator/wasm-libraries/user-host/src/ink.rs +++ b/arbitrator/wasm-libraries/user-host/src/ink.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::program::Program; +use arbutil::evm::api::Ink; use prover::programs::{ config::PricingParams, prelude::{GasMeteredMachine, MachineMeter, MeteredMachine}, @@ -18,7 +19,7 @@ impl MeteredMachine for Program { fn ink_left(&self) -> MachineMeter { unsafe { match user_ink_status() { - 0 => MachineMeter::Ready(user_ink_left()), + 0 => MachineMeter::Ready(Ink(user_ink_left())), _ => MachineMeter::Exhausted, } } @@ -26,7 +27,7 @@ impl MeteredMachine for Program { fn set_meter(&mut self, meter: MachineMeter) { unsafe { - user_set_ink(meter.ink(), meter.status()); + user_set_ink(meter.ink().0, meter.status()); } } } diff --git a/arbitrator/wasm-libraries/user-host/src/link.rs b/arbitrator/wasm-libraries/user-host/src/link.rs index 428611167d..cb9f046cdb 100644 --- a/arbitrator/wasm-libraries/user-host/src/link.rs +++ b/arbitrator/wasm-libraries/user-host/src/link.rs @@ -3,7 +3,11 @@ use crate::program::Program; use arbutil::{ - evm::{user::UserOutcomeKind, EvmData}, + evm::{ + api::{Gas, Ink}, + user::UserOutcomeKind, + EvmData, + }, format::DebugBytes, heapify, Bytes20, Bytes32, }; @@ -37,14 +41,15 @@ struct MemoryLeaf([u8; 32]); /// /// pages_ptr: starts pointing to max allowed pages, returns number of pages used #[no_mangle] -pub unsafe extern "C" fn programs__activate( +pub unsafe extern "C" fn programs__activate_v2( wasm_ptr: GuestPtr, wasm_size: usize, pages_ptr: GuestPtr, asm_estimate_ptr: GuestPtr, init_cost_ptr: GuestPtr, cached_init_cost_ptr: GuestPtr, - version: u16, + stylus_version: u16, + arbos_version_for_gas: u64, debug: u32, codehash: GuestPtr, module_hash_ptr: GuestPtr, @@ -58,7 +63,15 @@ pub unsafe extern "C" fn programs__activate( let page_limit = STATIC_MEM.read_u16(pages_ptr); let gas_left = &mut STATIC_MEM.read_u64(gas_ptr); - match Module::activate(&wasm, codehash, version, page_limit, debug, gas_left) { + match Module::activate( + &wasm, + codehash, + stylus_version, + arbos_version_for_gas, + page_limit, + debug, + gas_left, + ) { Ok((module, data)) => { STATIC_MEM.write_u64(gas_ptr, *gas_left); STATIC_MEM.write_u16(pages_ptr, data.footprint); @@ -111,11 +124,11 @@ pub unsafe extern "C" fn programs__new_program( // buy ink let pricing = config.pricing; - let ink = pricing.gas_to_ink(gas); + let ink = pricing.gas_to_ink(Gas(gas)); // link the program and ready its instrumentation let module = wavm_link_module(&MemoryLeaf(*module_hash)); - program_set_ink(module, ink); + program_set_ink(module, ink.0); program_set_stack(module, config.max_depth); // provide arguments @@ -166,7 +179,7 @@ pub unsafe extern "C" fn programs__set_response( id, STATIC_MEM.read_slice(result_ptr, result_len), STATIC_MEM.read_slice(raw_data_ptr, raw_data_len), - gas, + Gas(gas), ); } @@ -198,7 +211,7 @@ pub unsafe extern "C" fn program_internal__set_done(mut status: UserOutcomeKind) let program = Program::current(); let module = program.module; let mut outs = program.outs.as_slice(); - let mut ink_left = program_ink_left(module); + let mut ink_left = Ink(program_ink_left(module)); // apply any early exit codes if let Some(early) = program.early_exit { @@ -209,12 +222,12 @@ pub unsafe extern "C" fn program_internal__set_done(mut status: UserOutcomeKind) if program_ink_status(module) != 0 { status = OutOfInk; outs = &[]; - ink_left = 0; + ink_left = Ink(0); } if program_stack_left(module) == 0 { status = OutOfStack; outs = &[]; - ink_left = 0; + ink_left = Ink(0); } let gas_left = program.config.pricing.ink_to_gas(ink_left); @@ -242,7 +255,8 @@ pub unsafe extern "C" fn programs__create_stylus_config( /// Creates an `EvmData` handler from its component parts. /// #[no_mangle] -pub unsafe extern "C" fn programs__create_evm_data( +pub unsafe extern "C" fn programs__create_evm_data_v2( + arbos_version: u64, block_basefee_ptr: GuestPtr, chainid: u64, block_coinbase_ptr: GuestPtr, @@ -259,6 +273,7 @@ pub unsafe extern "C" fn programs__create_evm_data( reentrant: u32, ) -> u64 { let evm_data = EvmData { + arbos_version, block_basefee: read_bytes32(block_basefee_ptr), cached: cached != 0, chainid, diff --git a/arbitrator/wasm-libraries/user-host/src/program.rs b/arbitrator/wasm-libraries/user-host/src/program.rs index 4199a691f7..a2973ce56f 100644 --- a/arbitrator/wasm-libraries/user-host/src/program.rs +++ b/arbitrator/wasm-libraries/user-host/src/program.rs @@ -2,8 +2,9 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use arbutil::{ + benchmark::Benchmark, evm::{ - api::{EvmApiMethod, VecReader, EVM_API_METHOD_REQ_OFFSET}, + api::{EvmApiMethod, Gas, Ink, VecReader, EVM_API_METHOD_REQ_OFFSET}, req::{EvmApiRequestor, RequestHandler}, user::UserOutcomeKind, EvmData, @@ -49,7 +50,7 @@ static mut LAST_REQUEST_ID: u32 = 0x10000; #[derive(Clone)] pub(crate) struct UserHostRequester { data: Option>, - answer: Option<(Vec, VecReader, u64)>, + answer: Option<(Vec, VecReader, Gas)>, req_type: u32, id: u32, } @@ -75,6 +76,8 @@ pub(crate) struct Program { pub evm_api: EvmApiRequestor, /// EVM Context info. pub evm_data: EvmData, + // Used to benchmark execution blocks of code + pub benchmark: Benchmark, /// WAVM module index. pub module: u32, /// Call configuration. @@ -95,7 +98,7 @@ impl UserHostRequester { req_id: u32, result: Vec, raw_data: Vec, - gas: u64, + gas: Gas, ) { self.answer = Some((result, VecReader::new(raw_data), gas)); if req_id != self.id { @@ -130,7 +133,7 @@ impl UserHostRequester { } #[no_mangle] - unsafe fn send_request(&mut self, req_type: u32, data: Vec) -> (Vec, VecReader, u64) { + unsafe fn send_request(&mut self, req_type: u32, data: Vec) -> (Vec, VecReader, Gas) { let req_id = self.set_request(req_type, &data); compiler_fence(Ordering::SeqCst); @@ -149,7 +152,7 @@ impl RequestHandler for UserHostRequester { &mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>, - ) -> (Vec, VecReader, u64) { + ) -> (Vec, VecReader, Gas) { unsafe { self.send_request( req_type as u32 + EVM_API_METHOD_REQ_OFFSET, @@ -167,6 +170,7 @@ impl Program { outs: vec![], evm_api: EvmApiRequestor::new(UserHostRequester::default()), evm_data, + benchmark: Benchmark::default(), module, config, early_exit: None, @@ -237,6 +241,10 @@ impl UserHost for Program { &self.evm_data } + fn benchmark(&mut self) -> &mut Benchmark { + &mut self.benchmark + } + fn evm_return_data_len(&mut self) -> &mut u32 { &mut self.evm_data.return_data_len } @@ -265,7 +273,7 @@ impl UserHost for Program { println!("{} {text}", "Stylus says:".yellow()); } - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: u64) { + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: Ink) { let args = hex::encode(args); let outs = hex::encode(outs); println!("Error: unexpected hostio tracing info for {name} while proving: {args}, {outs}"); diff --git a/arbitrator/wasm-libraries/user-test/src/host.rs b/arbitrator/wasm-libraries/user-test/src/host.rs index f2912eaae3..f1b4506414 100644 --- a/arbitrator/wasm-libraries/user-test/src/host.rs +++ b/arbitrator/wasm-libraries/user-test/src/host.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use crate::program::Program; +use arbutil::evm::api::Gas; use caller_env::GuestPtr; use user_host_trait::UserHost; @@ -63,7 +64,14 @@ pub unsafe extern "C" fn vm_hooks__call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(call_contract(contract, data, data_len, value, gas, ret_len)) + hostio!(call_contract( + contract, + data, + data_len, + value, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -75,7 +83,11 @@ pub unsafe extern "C" fn vm_hooks__delegate_call_contract( ret_len: GuestPtr, ) -> u8 { hostio!(delegate_call_contract( - contract, data, data_len, gas, ret_len + contract, + data, + data_len, + Gas(gas), + ret_len )) } @@ -87,7 +99,13 @@ pub unsafe extern "C" fn vm_hooks__static_call_contract( gas: u64, ret_len: GuestPtr, ) -> u8 { - hostio!(static_call_contract(contract, data, data_len, gas, ret_len)) + hostio!(static_call_contract( + contract, + data, + data_len, + Gas(gas), + ret_len + )) } #[no_mangle] @@ -189,12 +207,12 @@ pub unsafe extern "C" fn vm_hooks__contract_address(ptr: GuestPtr) { #[no_mangle] pub unsafe extern "C" fn vm_hooks__evm_gas_left() -> u64 { - hostio!(evm_gas_left()) + hostio!(evm_gas_left()).0 } #[no_mangle] pub unsafe extern "C" fn vm_hooks__evm_ink_left() -> u64 { - hostio!(evm_ink_left()) + hostio!(evm_ink_left()).0 } #[no_mangle] diff --git a/arbitrator/wasm-libraries/user-test/src/ink.rs b/arbitrator/wasm-libraries/user-test/src/ink.rs index fca658e59b..72ecfadd96 100644 --- a/arbitrator/wasm-libraries/user-test/src/ink.rs +++ b/arbitrator/wasm-libraries/user-test/src/ink.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::{program::Program, CONFIG}; +use arbutil::evm::api::Ink; use prover::programs::{ config::PricingParams, prelude::{GasMeteredMachine, MachineMeter, MeteredMachine}, @@ -18,7 +19,7 @@ impl MeteredMachine for Program { fn ink_left(&self) -> MachineMeter { unsafe { match user_ink_status() { - 0 => MachineMeter::Ready(user_ink_left()), + 0 => MachineMeter::Ready(Ink(user_ink_left())), _ => MachineMeter::Exhausted, } } @@ -26,7 +27,7 @@ impl MeteredMachine for Program { fn set_meter(&mut self, meter: MachineMeter) { unsafe { - user_set_ink(meter.ink(), meter.status()); + user_set_ink(meter.ink().0, meter.status()); } } } diff --git a/arbitrator/wasm-libraries/user-test/src/program.rs b/arbitrator/wasm-libraries/user-test/src/program.rs index c56ea52ad0..99252a38f0 100644 --- a/arbitrator/wasm-libraries/user-test/src/program.rs +++ b/arbitrator/wasm-libraries/user-test/src/program.rs @@ -3,8 +3,9 @@ use crate::{ARGS, EVER_PAGES, EVM_DATA, KEYS, LOGS, OPEN_PAGES, OUTS}; use arbutil::{ + benchmark::Benchmark, evm::{ - api::{EvmApi, VecReader}, + api::{EvmApi, Gas, Ink, VecReader}, user::UserOutcomeKind, EvmData, }, @@ -28,6 +29,7 @@ impl From for eyre::ErrReport { /// Mock type representing a `user_host::Program` pub struct Program { evm_api: MockEvmApi, + benchmark: Benchmark, } #[allow(clippy::unit_arg)] @@ -52,6 +54,10 @@ impl UserHost for Program { &EVM_DATA } + fn benchmark(&mut self) -> &mut Benchmark { + &mut self.benchmark + } + fn evm_return_data_len(&mut self) -> &mut u32 { unimplemented!() } @@ -80,7 +86,7 @@ impl UserHost for Program { println!("{} {text}", "Stylus says:".yellow()); } - fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: u64) { + fn trace(&mut self, name: &str, args: &[u8], outs: &[u8], _end_ink: Ink) { let args = hex::encode(args); let outs = hex::encode(outs); println!("Error: unexpected hostio tracing info for {name} while proving: {args}, {outs}"); @@ -91,6 +97,7 @@ impl Program { pub fn current() -> Self { Self { evm_api: MockEvmApi, + benchmark: Benchmark::default(), } } @@ -102,18 +109,18 @@ impl Program { pub struct MockEvmApi; impl EvmApi for MockEvmApi { - fn get_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64) { + fn get_bytes32(&mut self, key: Bytes32, _evm_api_gas_to_use: Gas) -> (Bytes32, Gas) { let value = KEYS.lock().get(&key).cloned().unwrap_or_default(); - (value, 2100) // pretend worst case + (value, Gas(2100)) // pretend worst case } - fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 { + fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Gas { KEYS.lock().insert(key, value); - 0 + Gas(0) } - fn flush_storage_cache(&mut self, _clear: bool, _gas_left: u64) -> Result { - Ok(22100 * KEYS.lock().len() as u64) // pretend worst case + fn flush_storage_cache(&mut self, _clear: bool, _gas_left: Gas) -> Result { + Ok(Gas(22100) * KEYS.lock().len() as u64) // pretend worst case } fn get_transient_bytes32(&mut self, _key: Bytes32) -> Bytes32 { @@ -130,10 +137,10 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, + _gas_left: Gas, + _gas_req: Gas, _value: Bytes32, - ) -> (u32, u64, UserOutcomeKind) { + ) -> (u32, Gas, UserOutcomeKind) { unimplemented!() } @@ -141,9 +148,9 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + _gas_left: Gas, + _gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { unimplemented!() } @@ -151,9 +158,9 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas_left: u64, - _gas_req: u64, - ) -> (u32, u64, UserOutcomeKind) { + _gas_left: Gas, + _gas_req: Gas, + ) -> (u32, Gas, UserOutcomeKind) { unimplemented!() } @@ -161,8 +168,8 @@ impl EvmApi for MockEvmApi { &mut self, _code: Vec, _endowment: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!() } @@ -171,8 +178,8 @@ impl EvmApi for MockEvmApi { _code: Vec, _endowment: Bytes32, _salt: Bytes32, - _gas: u64, - ) -> (Result, u32, u64) { + _gas: Gas, + ) -> (Result, u32, Gas) { unimplemented!() } @@ -185,19 +192,19 @@ impl EvmApi for MockEvmApi { Ok(()) } - fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_balance(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn account_code(&mut self, _address: Bytes20, _gas_left: u64) -> (VecReader, u64) { + fn account_code(&mut self, _address: Bytes20, _gas_left: Gas) -> (VecReader, Gas) { unimplemented!() } - fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, u64) { + fn account_codehash(&mut self, _address: Bytes20) -> (Bytes32, Gas) { unimplemented!() } - fn add_pages(&mut self, pages: u16) -> u64 { + fn add_pages(&mut self, pages: u16) -> Gas { let model = MemoryModel::new(2, 1000); unsafe { let (open, ever) = (OPEN_PAGES, EVER_PAGES); @@ -212,8 +219,8 @@ impl EvmApi for MockEvmApi { _name: &str, _args: &[u8], _outs: &[u8], - _start_ink: u64, - _end_ink: u64, + _start_ink: Ink, + _end_ink: Ink, ) { unimplemented!() } diff --git a/arbnode/api.go b/arbnode/api.go index 228ad51cf8..55dc92434f 100644 --- a/arbnode/api.go +++ b/arbnode/api.go @@ -7,9 +7,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/staker" "github.com/offchainlabs/nitro/validator" + "github.com/offchainlabs/nitro/validator/server_api" ) type BlockValidatorAPI struct { @@ -54,3 +57,8 @@ func (a *BlockValidatorDebugAPI) ValidateMessageNumber( result.Valid = valid return result, err } + +func (a *BlockValidatorDebugAPI) ValidationInputsAt(ctx context.Context, msgNum hexutil.Uint64, target ethdb.WasmTarget, +) (server_api.InputJSON, error) { + return a.val.ValidationInputsAt(ctx, arbutil.MessageIndex(msgNum), target) +} diff --git a/arbnode/batch_poster.go b/arbnode/batch_poster.go index 71239efdbb..70c5952042 100644 --- a/arbnode/batch_poster.go +++ b/arbnode/batch_poster.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/bold/solgen/go/bridgegen" "github.com/offchainlabs/nitro/arbnode/dataposter" "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/arbnode/redislock" @@ -44,7 +45,6 @@ import ( "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/execution" - "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/util" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/blobs" @@ -80,8 +80,10 @@ var ( const ( batchPosterSimpleRedisLockKey = "node.batch-poster.redis-lock.simple-lock-key" - sequencerBatchPostMethodName = "addSequencerL2BatchFromOrigin0" - sequencerBatchPostWithBlobsMethodName = "addSequencerL2BatchFromBlobs" + sequencerBatchPostMethodName = "addSequencerL2BatchFromOrigin0" + sequencerBatchPostWithBlobsMethodName = "addSequencerL2BatchFromBlobs" + sequencerBatchPostDelayProofMethodName = "addSequencerL2BatchFromOriginDelayProof" + sequencerBatchPostWithBlobsDelayProofMethodName = "addSequencerL2BatchFromBlobsDelayProof" ) type batchPosterPosition struct { @@ -98,7 +100,6 @@ type BatchPoster struct { arbOSVersionGetter execution.FullExecutionClient config BatchPosterConfigFetcher seqInbox *bridgegen.SequencerInbox - bridge *bridgegen.Bridge syncMonitor *SyncMonitor seqInboxABI *abi.ABI seqInboxAddr common.Address @@ -121,7 +122,7 @@ type BatchPoster struct { nextRevertCheckBlock int64 // the last parent block scanned for reverting batches postedFirstBatch bool // indicates if batch poster has posted the first batch - accessList func(SequencerInboxAccs, AfterDelayedMessagesRead int) types.AccessList + accessList func(SequencerInboxAccs, AfterDelayedMessagesRead uint64) types.AccessList } type l1BlockBound int @@ -168,10 +169,12 @@ type BatchPosterConfig struct { L1BlockBound string `koanf:"l1-block-bound" reload:"hot"` L1BlockBoundBypass time.Duration `koanf:"l1-block-bound-bypass" reload:"hot"` UseAccessLists bool `koanf:"use-access-lists" reload:"hot"` - GasEstimateBaseFeeMultipleBips arbmath.Bips `koanf:"gas-estimate-base-fee-multiple-bips"` + GasEstimateBaseFeeMultipleBips arbmath.UBips `koanf:"gas-estimate-base-fee-multiple-bips"` Dangerous BatchPosterDangerousConfig `koanf:"dangerous"` ReorgResistanceMargin time.Duration `koanf:"reorg-resistance-margin" reload:"hot"` CheckBatchCorrectness bool `koanf:"check-batch-correctness"` + MaxEmptyBatchDelay time.Duration `koanf:"max-empty-batch-delay"` + DelayBufferThresholdMargin uint64 `koanf:"delay-buffer-threshold-margin"` gasRefunder common.Address l1BlockBound l1BlockBound @@ -203,11 +206,15 @@ func (c *BatchPosterConfig) Validate() error { type BatchPosterConfigFetcher func() *BatchPosterConfig +func DangerousBatchPosterConfigAddOptions(prefix string, f *pflag.FlagSet) { + f.Bool(prefix+".allow-posting-first-batch-when-sequencer-message-count-mismatch", DefaultBatchPosterConfig.Dangerous.AllowPostingFirstBatchWhenSequencerMessageCountMismatch, "allow posting the first batch even if sequence number doesn't match chain (useful after force-inclusion)") +} + func BatchPosterConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Bool(prefix+".enable", DefaultBatchPosterConfig.Enable, "enable posting batches to l1") f.Bool(prefix+".disable-dap-fallback-store-data-on-chain", DefaultBatchPosterConfig.DisableDapFallbackStoreDataOnChain, "If unable to batch to DA provider, disable fallback storing data on chain") - f.Int(prefix+".max-size", DefaultBatchPosterConfig.MaxSize, "maximum batch size") - f.Int(prefix+".max-4844-batch-size", DefaultBatchPosterConfig.Max4844BatchSize, "maximum 4844 blob enabled batch size") + f.Int(prefix+".max-size", DefaultBatchPosterConfig.MaxSize, "maximum estimated compressed batch size") + f.Int(prefix+".max-4844-batch-size", DefaultBatchPosterConfig.Max4844BatchSize, "maximum estimated compressed 4844 blob enabled batch size") f.Duration(prefix+".max-delay", DefaultBatchPosterConfig.MaxDelay, "maximum batch posting delay") f.Bool(prefix+".wait-for-max-delay", DefaultBatchPosterConfig.WaitForMaxDelay, "wait for the max batch delay, even if the batch is full") f.Duration(prefix+".poll-interval", DefaultBatchPosterConfig.PollInterval, "how long to wait after no batches are ready to be posted before checking again") @@ -225,9 +232,12 @@ func BatchPosterConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Uint64(prefix+".gas-estimate-base-fee-multiple-bips", uint64(DefaultBatchPosterConfig.GasEstimateBaseFeeMultipleBips), "for gas estimation, use this multiple of the basefee (measured in basis points) as the max fee per gas") f.Duration(prefix+".reorg-resistance-margin", DefaultBatchPosterConfig.ReorgResistanceMargin, "do not post batch if its within this duration from layer 1 minimum bounds. Requires l1-block-bound option not be set to \"ignore\"") f.Bool(prefix+".check-batch-correctness", DefaultBatchPosterConfig.CheckBatchCorrectness, "setting this to true will run the batch against an inbox multiplexer and verifies that it produces the correct set of messages") + f.Duration(prefix+".max-empty-batch-delay", DefaultBatchPosterConfig.MaxEmptyBatchDelay, "maximum empty batch posting delay, batch poster will only be able to post an empty batch if this time period building a batch has passed") + f.Uint64(prefix+".delay-buffer-threshold-margin", DefaultBatchPosterConfig.DelayBufferThresholdMargin, "the number of blocks to post the batch before reaching the delay buffer threshold") redislock.AddConfigOptions(prefix+".redis-lock", f) dataposter.DataPosterConfigAddOptions(prefix+".data-poster", f, dataposter.DefaultDataPosterConfig) genericconf.WalletConfigAddOptions(prefix+".parent-chain-wallet", f, DefaultBatchPosterConfig.ParentChainWallet.Pathname) + DangerousBatchPosterConfigAddOptions(prefix+".dangerous", f) } var DefaultBatchPosterConfig = BatchPosterConfig{ @@ -253,9 +263,11 @@ var DefaultBatchPosterConfig = BatchPosterConfig{ L1BlockBoundBypass: time.Hour, UseAccessLists: true, RedisLock: redislock.DefaultCfg, - GasEstimateBaseFeeMultipleBips: arbmath.OneInBips * 3 / 2, + GasEstimateBaseFeeMultipleBips: arbmath.OneInUBips * 3 / 2, ReorgResistanceMargin: 10 * time.Minute, CheckBatchCorrectness: true, + MaxEmptyBatchDelay: 3 * 24 * time.Hour, + DelayBufferThresholdMargin: 25, // 5 minutes considering 12-second blocks } var DefaultBatchPosterL1WalletConfig = genericconf.WalletConfig{ @@ -278,15 +290,16 @@ var TestBatchPosterConfig = BatchPosterConfig{ DASRetentionPeriod: daprovider.DefaultDASRetentionPeriod, GasRefunderAddress: "", ExtraBatchGas: 10_000, - Post4844Blobs: true, + Post4844Blobs: false, IgnoreBlobPrice: false, DataPoster: dataposter.TestDataPosterConfig, ParentChainWallet: DefaultBatchPosterL1WalletConfig, L1BlockBound: "", L1BlockBoundBypass: time.Hour, UseAccessLists: true, - GasEstimateBaseFeeMultipleBips: arbmath.OneInBips * 3 / 2, + GasEstimateBaseFeeMultipleBips: arbmath.OneInUBips * 3 / 2, CheckBatchCorrectness: true, + DelayBufferThresholdMargin: 0, } type BatchPosterOpts struct { @@ -309,10 +322,7 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e if err != nil { return nil, err } - bridge, err := bridgegen.NewBridge(opts.DeployInfo.Bridge, opts.L1Reader.Client()) - if err != nil { - return nil, err - } + if err = opts.Config().Validate(); err != nil { return nil, err } @@ -340,7 +350,6 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e arbOSVersionGetter: opts.VersionGetter, syncMonitor: opts.SyncMonitor, config: opts.Config, - bridge: bridge, seqInbox: seqInbox, seqInboxABI: seqInboxABI, seqInboxAddr: opts.DeployInfo.SequencerInbox, @@ -374,7 +383,7 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e } // Dataposter sender may be external signer address, so we should initialize // access list after initializing dataposter. - b.accessList = func(SequencerInboxAccs, AfterDelayedMessagesRead int) types.AccessList { + b.accessList = func(SequencerInboxAccs, AfterDelayedMessagesRead uint64) types.AccessList { if !b.config().UseAccessLists || opts.L1Reader.IsParentChainArbitrum() { // Access lists cost gas instead of saving gas when posting to L2s, // because data is expensive in comparison to computation. @@ -433,8 +442,8 @@ type AccessListOpts struct { BridgeAddr common.Address DataPosterAddr common.Address GasRefunderAddr common.Address - SequencerInboxAccs int - AfterDelayedMessagesRead int + SequencerInboxAccs uint64 + AfterDelayedMessagesRead uint64 } // AccessList returns access list (contracts, storage slots) for batchposter. @@ -476,12 +485,12 @@ func AccessList(opts *AccessListOpts) types.AccessList { }, } - for _, v := range []struct{ slotIdx, val int }{ + for _, v := range []struct{ slotIdx, val uint64 }{ {7, opts.SequencerInboxAccs - 1}, // - sequencerInboxAccs[sequencerInboxAccs.length - 1]; (keccak256(7, sequencerInboxAccs.length - 1)) {7, opts.SequencerInboxAccs}, // - sequencerInboxAccs.push(...); (keccak256(7, sequencerInboxAccs.length)) {6, opts.AfterDelayedMessagesRead - 1}, // - delayedInboxAccs[afterDelayedMessagesRead - 1]; (keccak256(6, afterDelayedMessagesRead - 1)) } { - sb := arbutil.SumBytes(arbutil.PaddedKeccak256([]byte{byte(v.slotIdx)}), big.NewInt(int64(v.val)).Bytes()) + sb := arbutil.SumBytes(arbutil.PaddedKeccak256([]byte{byte(v.slotIdx)}), new(big.Int).SetUint64(v.val).Bytes()) l[1].StorageKeys = append(l[1].StorageKeys, common.Hash(sb)) } @@ -603,9 +612,12 @@ func (b *BatchPoster) pollForL1PriceData(ctx context.Context) { l1GasPrice = blobFeePerByte.Uint64() / 16 } } + // #nosec G115 blobGasUsedGauge.Update(int64(*h.BlobGasUsed)) } + // #nosec G115 blockGasUsedGauge.Update(int64(h.GasUsed)) + // #nosec G115 blockGasLimitGauge.Update(int64(h.GasLimit)) suggestedTipCap, err := b.l1Reader.Client().SuggestGasTipCap(ctx) if err != nil { @@ -613,6 +625,7 @@ func (b *BatchPoster) pollForL1PriceData(ctx context.Context) { } else { suggestedTipCapGauge.Update(suggestedTipCap.Int64()) } + // #nosec G115 l1GasPriceGauge.Update(int64(l1GasPrice)) case <-ctx.Done(): return @@ -712,12 +725,15 @@ type batchSegments struct { } type buildingBatch struct { - segments *batchSegments - startMsgCount arbutil.MessageIndex - msgCount arbutil.MessageIndex - haveUsefulMessage bool - use4844 bool - muxBackend *simulatedMuxBackend + segments *batchSegments + startMsgCount arbutil.MessageIndex + msgCount arbutil.MessageIndex + haveUsefulMessage bool + use4844 bool + muxBackend *simulatedMuxBackend + firstDelayedMsg *arbostypes.MessageWithMetadata + firstNonDelayedMsg *arbostypes.MessageWithMetadata + firstUsefulMsg *arbostypes.MessageWithMetadata } func newBatchSegments(firstDelayed uint64, config *BatchPosterConfig, backlog uint64, use4844 bool) *batchSegments { @@ -954,41 +970,45 @@ func (b *BatchPoster) encodeAddBatch( l2MessageData []byte, delayedMsg uint64, use4844 bool, + delayProof *bridgegen.DelayProof, ) ([]byte, []kzg4844.Blob, error) { - methodName := sequencerBatchPostMethodName + var methodName string if use4844 { - methodName = sequencerBatchPostWithBlobsMethodName + if delayProof != nil { + methodName = sequencerBatchPostWithBlobsDelayProofMethodName + } else { + methodName = sequencerBatchPostWithBlobsMethodName + } + } else if delayProof != nil { + methodName = sequencerBatchPostDelayProofMethodName + } else { + methodName = sequencerBatchPostMethodName } method, ok := b.seqInboxABI.Methods[methodName] if !ok { return nil, nil, errors.New("failed to find add batch method") } - var calldata []byte + var args []any var kzgBlobs []kzg4844.Blob var err error + args = append(args, seqNum) if use4844 { kzgBlobs, err = blobs.EncodeBlobs(l2MessageData) if err != nil { return nil, nil, fmt.Errorf("failed to encode blobs: %w", err) } - // EIP4844 transactions to the sequencer inbox will not use transaction calldata for L2 info. - calldata, err = method.Inputs.Pack( - seqNum, - new(big.Int).SetUint64(delayedMsg), - b.config().gasRefunder, - new(big.Int).SetUint64(uint64(prevMsgNum)), - new(big.Int).SetUint64(uint64(newMsgNum)), - ) } else { - calldata, err = method.Inputs.Pack( - seqNum, - l2MessageData, - new(big.Int).SetUint64(delayedMsg), - b.config().gasRefunder, - new(big.Int).SetUint64(uint64(prevMsgNum)), - new(big.Int).SetUint64(uint64(newMsgNum)), - ) + // EIP4844 transactions to the sequencer inbox will not use transaction calldata for L2 info. + args = append(args, l2MessageData) + } + args = append(args, new(big.Int).SetUint64(delayedMsg)) + args = append(args, b.config().gasRefunder) + args = append(args, new(big.Int).SetUint64(uint64(prevMsgNum))) + args = append(args, new(big.Int).SetUint64(uint64(newMsgNum))) + if delayProof != nil { + args = append(args, delayProof) } + calldata, err := method.Inputs.Pack(args...) if err != nil { return nil, nil, err } @@ -1014,7 +1034,17 @@ func estimateGas(client rpc.ClientInterface, ctx context.Context, params estimat return uint64(gas), err } -func (b *BatchPoster) estimateGas(ctx context.Context, sequencerMessage []byte, delayedMessages uint64, realData []byte, realBlobs []kzg4844.Blob, realNonce uint64, realAccessList types.AccessList) (uint64, error) { +func (b *BatchPoster) estimateGas( + ctx context.Context, + sequencerMessage []byte, + delayedMessages uint64, + realData []byte, + realBlobs []kzg4844.Blob, + realNonce uint64, + realAccessList types.AccessList, + delayProof *bridgegen.DelayProof, +) (uint64, error) { + config := b.config() rpcClient := b.l1Reader.Client() rawRpcClient := rpcClient.Client() @@ -1031,7 +1061,7 @@ func (b *BatchPoster) estimateGas(ctx context.Context, sequencerMessage []byte, if err != nil { return 0, err } - maxFeePerGas := arbmath.BigMulByBips(latestHeader.BaseFee, config.GasEstimateBaseFeeMultipleBips) + maxFeePerGas := arbmath.BigMulByUBips(latestHeader.BaseFee, config.GasEstimateBaseFeeMultipleBips) if useNormalEstimation { _, realBlobHashes, err := blobs.ComputeCommitmentsAndHashes(realBlobs) if err != nil { @@ -1056,7 +1086,7 @@ func (b *BatchPoster) estimateGas(ctx context.Context, sequencerMessage []byte, // However, we set nextMsgNum to 1 because it is necessary for a correct estimation for the final to be non-zero. // Because we're likely estimating against older state, this might not be the actual next message, // but the gas used should be the same. - data, kzgBlobs, err := b.encodeAddBatch(abi.MaxUint256, 0, 1, sequencerMessage, delayedMessages, len(realBlobs) > 0) + data, kzgBlobs, err := b.encodeAddBatch(abi.MaxUint256, 0, 1, sequencerMessage, delayedMessages, len(realBlobs) > 0, delayProof) if err != nil { return 0, err } @@ -1127,7 +1157,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) if err != nil { return false, err } - if arbOSVersion >= 20 { + if arbOSVersion >= params.ArbosVersion_20 { if config.IgnoreBlobPrice { use4844 = true } else { @@ -1172,11 +1202,6 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) // There's nothing after the newest batch, therefore batch posting was not required return false, nil } - firstMsg, err := b.streamer.GetMessage(batchPosition.MessageCount) - if err != nil { - return false, err - } - firstMsgTime := time.Unix(int64(firstMsg.Message.Header.Timestamp), 0) lastPotentialMsg, err := b.streamer.GetMessage(msgCount - 1) if err != nil { @@ -1184,7 +1209,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) } config := b.config() - forcePostBatch := config.MaxDelay <= 0 || time.Since(firstMsgTime) >= config.MaxDelay + forcePostBatch := config.MaxDelay <= 0 var l1BoundMaxBlockNumber uint64 = math.MaxUint64 var l1BoundMaxTimestamp uint64 = math.MaxUint64 @@ -1245,7 +1270,9 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) l1BoundMinTimestamp = arbmath.SaturatingUSub(latestHeader.Time, arbmath.BigToUintSaturating(maxTimeVariationDelaySeconds)) if config.L1BlockBoundBypass > 0 { + // #nosec G115 blockNumberWithPadding := arbmath.SaturatingUAdd(latestBlockNumber, uint64(config.L1BlockBoundBypass/ethPosBlockTime)) + // #nosec G115 timestampWithPadding := arbmath.SaturatingUAdd(latestHeader.Time, uint64(config.L1BlockBoundBypass/time.Second)) l1BoundMinBlockNumberWithBypass = arbmath.SaturatingUSub(blockNumberWithPadding, arbmath.BigToUintSaturating(maxTimeVariationDelayBlocks)) l1BoundMinTimestampWithBypass = arbmath.SaturatingUSub(timestampWithPadding, arbmath.BigToUintSaturating(maxTimeVariationDelaySeconds)) @@ -1294,6 +1321,9 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) forcePostBatch = true } b.building.haveUsefulMessage = true + if b.building.firstUsefulMsg == nil { + b.building.firstUsefulMsg = msg + } break } if config.CheckBatchCorrectness { @@ -1302,16 +1332,60 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) b.building.muxBackend.delayedInbox = append(b.building.muxBackend.delayedInbox, msg) } } - if msg.Message.Header.Kind != arbostypes.L1MessageType_BatchPostingReport { + // #nosec G115 + timeSinceMsg := time.Since(time.Unix(int64(msg.Message.Header.Timestamp), 0)) + if (msg.Message.Header.Kind != arbostypes.L1MessageType_BatchPostingReport) || (timeSinceMsg >= config.MaxEmptyBatchDelay) { b.building.haveUsefulMessage = true + if b.building.firstUsefulMsg == nil { + b.building.firstUsefulMsg = msg + } + } + if isDelayed { + if b.building.firstDelayedMsg == nil { + b.building.firstDelayedMsg = msg + } + } else if b.building.firstNonDelayedMsg == nil { + b.building.firstNonDelayedMsg = msg } b.building.msgCount++ } - if hasL1Bound && config.ReorgResistanceMargin > 0 { - firstMsgBlockNumber := firstMsg.Message.Header.BlockNumber - firstMsgTimeStamp := firstMsg.Message.Header.Timestamp + firstUsefulMsgTime := time.Now() + if b.building.firstUsefulMsg != nil { + // #nosec G115 + firstUsefulMsgTime = time.Unix(int64(b.building.firstUsefulMsg.Message.Header.Timestamp), 0) + if time.Since(firstUsefulMsgTime) >= config.MaxDelay { + forcePostBatch = true + } + } + + delayBuffer, err := GetDelayBufferConfig(ctx, b.seqInbox) + if err != nil { + return false, err + } + if delayBuffer.Enabled && b.building.firstDelayedMsg != nil { + latestHeader, err := b.l1Reader.LastHeader(ctx) + if err != nil { + return false, err + } + latestBlock := latestHeader.Number.Uint64() + firstDelayedMsgBlock := b.building.firstDelayedMsg.Message.Header.BlockNumber + threasholdLimit := firstDelayedMsgBlock + delayBuffer.Threshold - b.config().DelayBufferThresholdMargin + if latestBlock >= threasholdLimit { + log.Info("force post batch because of the delay buffer", + "firstDelayedMsgBlock", firstDelayedMsgBlock, + "threshold", delayBuffer.Threshold, + "latestBlock", latestBlock) + forcePostBatch = true + } + } + + if b.building.firstNonDelayedMsg != nil && hasL1Bound && config.ReorgResistanceMargin > 0 { + firstMsgBlockNumber := b.building.firstNonDelayedMsg.Message.Header.BlockNumber + firstMsgTimeStamp := b.building.firstNonDelayedMsg.Message.Header.Timestamp + // #nosec G115 batchNearL1BoundMinBlockNumber := firstMsgBlockNumber <= arbmath.SaturatingUAdd(l1BoundMinBlockNumber, uint64(config.ReorgResistanceMargin/ethPosBlockTime)) + // #nosec G115 batchNearL1BoundMinTimestamp := firstMsgTimeStamp <= arbmath.SaturatingUAdd(l1BoundMinTimestamp, uint64(config.ReorgResistanceMargin/time.Second)) if batchNearL1BoundMinTimestamp || batchNearL1BoundMinBlockNumber { log.Error( @@ -1356,6 +1430,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) batchPosterDAFailureCounter.Inc(1) return false, fmt.Errorf("%w: nonce changed from %d to %d while creating batch", storage.ErrStorageRace, nonce, gotNonce) } + // #nosec G115 sequencerMsg, err = b.dapWriter.Store(ctx, sequencerMsg, uint64(time.Now().Add(config.DASRetentionPeriod).Unix()), config.DisableDapFallbackStoreDataOnChain) if err != nil { batchPosterDAFailureCounter.Inc(1) @@ -1396,14 +1471,22 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) prevMessageCount = 0 } - data, kzgBlobs, err := b.encodeAddBatch(new(big.Int).SetUint64(batchPosition.NextSeqNum), prevMessageCount, b.building.msgCount, sequencerMsg, b.building.segments.delayedMsg, b.building.use4844) + var delayProof *bridgegen.DelayProof + if delayBuffer.Enabled && b.building.firstDelayedMsg != nil { + delayProof, err = GenDelayProof(ctx, b.building.firstDelayedMsg, b.inbox) + if err != nil { + return false, fmt.Errorf("failed to generate delay proof: %w", err) + } + } + + data, kzgBlobs, err := b.encodeAddBatch(new(big.Int).SetUint64(batchPosition.NextSeqNum), prevMessageCount, b.building.msgCount, sequencerMsg, b.building.segments.delayedMsg, b.building.use4844, delayProof) if err != nil { return false, err } if len(kzgBlobs)*params.BlobTxBlobGasPerBlob > params.MaxBlobGasPerBlock { return false, fmt.Errorf("produced %v blobs for batch but a block can only hold %v (compressed batch was %v bytes long)", len(kzgBlobs), params.MaxBlobGasPerBlock/params.BlobTxBlobGasPerBlob, len(sequencerMsg)) } - accessList := b.accessList(int(batchPosition.NextSeqNum), int(b.building.segments.delayedMsg)) + accessList := b.accessList(batchPosition.NextSeqNum, b.building.segments.delayedMsg) // On restart, we may be trying to estimate gas for a batch whose successor has // already made it into pending state, if not latest state. // In that case, we might get a revert with `DelayedBackwards()`. @@ -1411,7 +1494,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) // In theory, this might reduce gas usage, but only by a factor that's already // accounted for in `config.ExtraBatchGas`, as that same factor can appear if a user // posts a new delayed message that we didn't see while gas estimating. - gasLimit, err := b.estimateGas(ctx, sequencerMsg, lastPotentialMsg.DelayedMessagesRead, data, kzgBlobs, nonce, accessList) + gasLimit, err := b.estimateGas(ctx, sequencerMsg, lastPotentialMsg.DelayedMessagesRead, data, kzgBlobs, nonce, accessList, delayProof) if err != nil { return false, err } @@ -1439,7 +1522,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) b.building.muxBackend.delayedInboxStart = batchPosition.DelayedMessageCount b.building.muxBackend.SetPositionWithinMessage(0) simMux := arbstate.NewInboxMultiplexer(b.building.muxBackend, batchPosition.DelayedMessageCount, dapReaders, daprovider.KeysetValidate) - log.Info("Begin checking the correctness of batch against inbox multiplexer", "startMsgSeqNum", batchPosition.MessageCount, "endMsgSeqNum", b.building.msgCount-1) + log.Debug("Begin checking the correctness of batch against inbox multiplexer", "startMsgSeqNum", batchPosition.MessageCount, "endMsgSeqNum", b.building.msgCount-1) for i := batchPosition.MessageCount; i < b.building.msgCount; i++ { msg, err := simMux.Pop(ctx) if err != nil { @@ -1458,7 +1541,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) } tx, err := b.dataPoster.PostTransaction(ctx, - firstMsgTime, + firstUsefulMsgTime, nonce, newMeta, b.seqInboxAddr, @@ -1505,6 +1588,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) messagesPerBatch = 1 } backlog := uint64(unpostedMessages) / messagesPerBatch + // #nosec G115 batchPosterEstimatedBatchBacklogGauge.Update(int64(backlog)) if backlog > 10 { logLevel := log.Warn diff --git a/arbnode/dataposter/data_poster.go b/arbnode/dataposter/data_poster.go index 5630a52947..a977b9fc08 100644 --- a/arbnode/dataposter/data_poster.go +++ b/arbnode/dataposter/data_poster.go @@ -20,12 +20,18 @@ import ( "time" "github.com/Knetic/govaluate" + "github.com/holiman/uint256" + "github.com/redis/go-redis/v9" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -33,22 +39,18 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/go-redis/redis/v8" - "github.com/holiman/uint256" + "github.com/offchainlabs/nitro/arbnode/dataposter/dbstorage" "github.com/offchainlabs/nitro/arbnode/dataposter/noop" + redisstorage "github.com/offchainlabs/nitro/arbnode/dataposter/redis" "github.com/offchainlabs/nitro/arbnode/dataposter/slice" "github.com/offchainlabs/nitro/arbnode/dataposter/storage" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/blobs" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/util/signature" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/spf13/pflag" - - redisstorage "github.com/offchainlabs/nitro/arbnode/dataposter/redis" ) var ( @@ -69,7 +71,7 @@ var ( type DataPoster struct { stopwaiter.StopWaiter headerReader *headerreader.HeaderReader - client arbutil.L1Interface + client *ethclient.Client auth *bind.TransactOpts signer signerFn config ConfigFetcher @@ -359,6 +361,7 @@ func (p *DataPoster) canPostWithNonce(ctx context.Context, nextNonce uint64, thi if err != nil { return fmt.Errorf("getting nonce of a dataposter sender: %w", err) } + // #nosec G115 latestUnconfirmedNonceGauge.Update(int64(unconfirmedNonce)) if nextNonce >= cfg.MaxMempoolTransactions+unconfirmedNonce { return fmt.Errorf("%w: transaction nonce: %d, unconfirmed nonce: %d, max mempool size: %d", ErrExceedsMaxMempoolSize, nextNonce, unconfirmedNonce, cfg.MaxMempoolTransactions) @@ -371,6 +374,7 @@ func (p *DataPoster) canPostWithNonce(ctx context.Context, nextNonce uint64, thi if err != nil { return fmt.Errorf("getting nonce of a dataposter sender: %w", err) } + // #nosec G115 latestUnconfirmedNonceGauge.Update(int64(unconfirmedNonce)) if unconfirmedNonce > nextNonce { return fmt.Errorf("latest on-chain nonce %v is greater than to next nonce %v", unconfirmedNonce, nextNonce) @@ -525,6 +529,7 @@ func (p *DataPoster) feeAndTipCaps(ctx context.Context, nonce uint64, gasLimit u if err != nil { return nil, nil, nil, fmt.Errorf("failed to get latest nonce %v blocks ago (block %v): %w", config.NonceRbfSoftConfs, softConfBlock, err) } + // #nosec G115 latestSoftConfirmedNonceGauge.Update(int64(softConfNonce)) suggestedTip, err := p.client.SuggestGasTipCap(ctx) @@ -635,11 +640,11 @@ func (p *DataPoster) feeAndTipCaps(ctx context.Context, nonce uint64, gasLimit u if config.MaxFeeBidMultipleBips > 0 { // Limit the fee caps to be no greater than max(MaxFeeBidMultipleBips, minRbf) - maxNonBlobFee := arbmath.BigMulByBips(currentNonBlobFee, config.MaxFeeBidMultipleBips) + maxNonBlobFee := arbmath.BigMulByUBips(currentNonBlobFee, config.MaxFeeBidMultipleBips) if lastTx != nil { maxNonBlobFee = arbmath.BigMax(maxNonBlobFee, arbmath.BigMulByBips(lastTx.GasFeeCap(), minRbfIncrease)) } - maxBlobFee := arbmath.BigMulByBips(currentBlobFee, config.MaxFeeBidMultipleBips) + maxBlobFee := arbmath.BigMulByUBips(currentBlobFee, config.MaxFeeBidMultipleBips) if lastTx != nil && lastTx.BlobGasFeeCap() != nil { maxBlobFee = arbmath.BigMax(maxBlobFee, arbmath.BigMulByBips(lastTx.BlobGasFeeCap(), minRbfIncrease)) } @@ -707,13 +712,23 @@ func (p *DataPoster) feeAndTipCaps(ctx context.Context, nonce uint64, gasLimit u return newBaseFeeCap, newTipCap, newBlobFeeCap, nil } -func (p *DataPoster) PostSimpleTransaction(ctx context.Context, nonce uint64, to common.Address, calldata []byte, gasLimit uint64, value *big.Int) (*types.Transaction, error) { - return p.PostTransaction(ctx, time.Now(), nonce, nil, to, calldata, gasLimit, value, nil, nil) +func (p *DataPoster) PostSimpleTransaction(ctx context.Context, to common.Address, calldata []byte, gasLimit uint64, value *big.Int) (*types.Transaction, error) { + p.mutex.Lock() + defer p.mutex.Unlock() + nonce, _, _, _, err := p.getNextNonceAndMaybeMeta(ctx, 1) + if err != nil { + return nil, err + } + return p.postTransactionWithMutex(ctx, time.Now(), nonce, nil, to, calldata, gasLimit, value, nil, nil) } func (p *DataPoster) PostTransaction(ctx context.Context, dataCreatedAt time.Time, nonce uint64, meta []byte, to common.Address, calldata []byte, gasLimit uint64, value *big.Int, kzgBlobs []kzg4844.Blob, accessList types.AccessList) (*types.Transaction, error) { p.mutex.Lock() defer p.mutex.Unlock() + return p.postTransactionWithMutex(ctx, dataCreatedAt, nonce, meta, to, calldata, gasLimit, value, kzgBlobs, accessList) +} + +func (p *DataPoster) postTransactionWithMutex(ctx context.Context, dataCreatedAt time.Time, nonce uint64, meta []byte, to common.Address, calldata []byte, gasLimit uint64, value *big.Int, kzgBlobs []kzg4844.Blob, accessList types.AccessList) (*types.Transaction, error) { if p.config().DisableNewTx { return nil, fmt.Errorf("posting new transaction is disabled") @@ -1052,6 +1067,7 @@ func (p *DataPoster) updateNonce(ctx context.Context) error { } return nil } + // #nosec G115 latestFinalizedNonceGauge.Update(int64(nonce)) log.Info("Data poster transactions confirmed", "previousNonce", p.nonce, "newNonce", nonce, "previousL1Block", p.lastBlock, "newL1Block", header.Number) if len(p.errorCount) > 0 { @@ -1083,7 +1099,7 @@ func (p *DataPoster) updateBalance(ctx context.Context) error { return nil } -const maxConsecutiveIntermittentErrors = 10 +const maxConsecutiveIntermittentErrors = 20 func (p *DataPoster) maybeLogError(err error, tx *storage.QueuedTransaction, msg string) { nonce := tx.FullTx.Nonce() @@ -1092,10 +1108,17 @@ func (p *DataPoster) maybeLogError(err error, tx *storage.QueuedTransaction, msg return } logLevel := log.Error - if errors.Is(err, storage.ErrStorageRace) { + isStorageRace := errors.Is(err, storage.ErrStorageRace) + if isStorageRace || strings.Contains(err.Error(), txpool.ErrFutureReplacePending.Error()) { p.errorCount[nonce]++ if p.errorCount[nonce] <= maxConsecutiveIntermittentErrors { - logLevel = log.Debug + if isStorageRace { + logLevel = log.Debug + } else { + logLevel = log.Info + } + } else if isStorageRace { + logLevel = log.Warn } } else { delete(p.errorCount, nonce) @@ -1132,6 +1155,7 @@ func (p *DataPoster) Start(ctxIn context.Context) { log.Warn("Failed to get latest nonce", "err", err) return minWait } + // #nosec G115 latestUnconfirmedNonceGauge.Update(int64(unconfirmedNonce)) // We use unconfirmedNonce here to replace-by-fee transactions that aren't in a block, // excluding those that are in an unconfirmed block. If a reorg occurs, we'll continue @@ -1154,7 +1178,9 @@ func (p *DataPoster) Start(ctxIn context.Context) { confirmedNonce := unconfirmedNonce - 1 confirmedMeta, err := p.queue.Get(ctx, confirmedNonce) if err == nil && confirmedMeta != nil { + // #nosec G115 totalQueueWeightGauge.Update(int64(arbmath.SaturatingUSub(latestCumulativeWeight, confirmedMeta.CumulativeWeight()))) + // #nosec G115 totalQueueLengthGauge.Update(int64(arbmath.SaturatingUSub(latestNonce, confirmedNonce))) } else { log.Error("Failed to fetch latest confirmed tx from queue", "confirmedNonce", confirmedNonce, "err", err, "confirmedMeta", confirmedMeta) @@ -1234,7 +1260,7 @@ type DataPosterConfig struct { MinBlobTxTipCapGwei float64 `koanf:"min-blob-tx-tip-cap-gwei" reload:"hot"` MaxTipCapGwei float64 `koanf:"max-tip-cap-gwei" reload:"hot"` MaxBlobTxTipCapGwei float64 `koanf:"max-blob-tx-tip-cap-gwei" reload:"hot"` - MaxFeeBidMultipleBips arbmath.Bips `koanf:"max-fee-bid-multiple-bips" reload:"hot"` + MaxFeeBidMultipleBips arbmath.UBips `koanf:"max-fee-bid-multiple-bips" reload:"hot"` NonceRbfSoftConfs uint64 `koanf:"nonce-rbf-soft-confs" reload:"hot"` AllocateMempoolBalance bool `koanf:"allocate-mempool-balance" reload:"hot"` UseDBStorage bool `koanf:"use-db-storage"` @@ -1338,7 +1364,7 @@ var DefaultDataPosterConfig = DataPosterConfig{ MinBlobTxTipCapGwei: 1, // default geth minimum, and relays aren't likely to accept lower values given propagation time MaxTipCapGwei: 1.2, MaxBlobTxTipCapGwei: 1, // lower than normal because 4844 rbf is a minimum of a 2x - MaxFeeBidMultipleBips: arbmath.OneInBips * 10, + MaxFeeBidMultipleBips: arbmath.OneInUBips * 10, NonceRbfSoftConfs: 1, AllocateMempoolBalance: true, UseDBStorage: true, @@ -1373,7 +1399,7 @@ var TestDataPosterConfig = DataPosterConfig{ MinBlobTxTipCapGwei: 1, MaxTipCapGwei: 5, MaxBlobTxTipCapGwei: 1, - MaxFeeBidMultipleBips: arbmath.OneInBips * 10, + MaxFeeBidMultipleBips: arbmath.OneInUBips * 10, NonceRbfSoftConfs: 1, AllocateMempoolBalance: true, UseDBStorage: false, diff --git a/arbnode/dataposter/dataposter_test.go b/arbnode/dataposter/dataposter_test.go index 7f2f61c07e..dc5df1a6c4 100644 --- a/arbnode/dataposter/dataposter_test.go +++ b/arbnode/dataposter/dataposter_test.go @@ -2,22 +2,25 @@ package dataposter import ( "context" + "errors" "fmt" "math/big" "testing" "time" "github.com/Knetic/govaluate" - "github.com/ethereum/go-ethereum" + "github.com/google/go-cmp/cmp" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" - "github.com/google/go-cmp/cmp" - "github.com/holiman/uint256" + "github.com/offchainlabs/nitro/arbnode/dataposter/externalsignertest" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -152,46 +155,36 @@ func TestMaxFeeCapFormulaCalculation(t *testing.T) { } } -type stubL1Client struct { +type stubL1ClientInner struct { senderNonce uint64 suggestedGasTipCap *big.Int - - // Define most of the required methods that aren't used by feeAndTipCaps - backends.SimulatedBackend -} - -func (c *stubL1Client) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - return c.senderNonce, nil -} - -func (c *stubL1Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return c.suggestedGasTipCap, nil -} - -// Not used but we need to define -func (c *stubL1Client) BlockNumber(ctx context.Context) (uint64, error) { - return 0, nil -} - -func (c *stubL1Client) CallContractAtHash(ctx context.Context, msg ethereum.CallMsg, blockHash common.Hash) ([]byte, error) { - return []byte{}, nil } -func (c *stubL1Client) CodeAtHash(ctx context.Context, address common.Address, blockHash common.Hash) ([]byte, error) { - return []byte{}, nil +func (c *stubL1ClientInner) CallContext(ctx_in context.Context, result interface{}, method string, args ...interface{}) error { + switch method { + case "eth_getTransactionCount": + ptr, ok := result.(*hexutil.Uint64) + if !ok { + return errors.New("result is not a *hexutil.Uint64") + } + *ptr = hexutil.Uint64(c.senderNonce) + case "eth_maxPriorityFeePerGas": + ptr, ok := result.(*hexutil.Big) + if !ok { + return errors.New("result is not a *hexutil.Big") + } + *ptr = hexutil.Big(*c.suggestedGasTipCap) + } + return nil } -func (c *stubL1Client) ChainID(ctx context.Context) (*big.Int, error) { +func (c *stubL1ClientInner) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error) { return nil, nil } - -func (c *stubL1Client) Client() rpc.ClientInterface { +func (c *stubL1ClientInner) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { return nil } - -func (c *stubL1Client) TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) { - return common.Address{}, nil -} +func (c *stubL1ClientInner) Close() {} func TestFeeAndTipCaps_EnoughBalance_NoBacklog_NoUnconfirmed_BlobTx(t *testing.T) { conf := func() *DataPosterConfig { @@ -204,7 +197,7 @@ func TestFeeAndTipCaps_EnoughBalance_NoBacklog_NoUnconfirmed_BlobTx(t *testing.T MinBlobTxTipCapGwei: 1, MaxTipCapGwei: 5, MaxBlobTxTipCapGwei: 10, - MaxFeeBidMultipleBips: arbmath.OneInBips * 10, + MaxFeeBidMultipleBips: arbmath.OneInUBips * 10, AllocateMempoolBalance: true, UrgencyGwei: 2., @@ -223,10 +216,10 @@ func TestFeeAndTipCaps_EnoughBalance_NoBacklog_NoUnconfirmed_BlobTx(t *testing.T extraBacklog: func() uint64 { return 0 }, balance: big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(10)), usingNoOpStorage: false, - client: &stubL1Client{ + client: ethclient.NewClient(&stubL1ClientInner{ senderNonce: 1, suggestedGasTipCap: big.NewInt(2 * params.GWei), - }, + }), auth: &bind.TransactOpts{ From: common.Address{}, }, @@ -335,7 +328,7 @@ func TestFeeAndTipCaps_RBF_RisingBlobFee_FallingBaseFee(t *testing.T) { MinBlobTxTipCapGwei: 1, MaxTipCapGwei: 5, MaxBlobTxTipCapGwei: 10, - MaxFeeBidMultipleBips: arbmath.OneInBips * 10, + MaxFeeBidMultipleBips: arbmath.OneInUBips * 10, AllocateMempoolBalance: true, UrgencyGwei: 2., @@ -354,10 +347,10 @@ func TestFeeAndTipCaps_RBF_RisingBlobFee_FallingBaseFee(t *testing.T) { extraBacklog: func() uint64 { return 0 }, balance: big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(10)), usingNoOpStorage: false, - client: &stubL1Client{ + client: ethclient.NewClient(&stubL1ClientInner{ senderNonce: 1, suggestedGasTipCap: big.NewInt(2 * params.GWei), - }, + }), auth: &bind.TransactOpts{ From: common.Address{}, }, diff --git a/arbnode/dataposter/dbstorage/storage.go b/arbnode/dataposter/dbstorage/storage.go index 97055193a6..88989cf757 100644 --- a/arbnode/dataposter/dbstorage/storage.go +++ b/arbnode/dataposter/dbstorage/storage.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/util/dbutil" ) @@ -42,7 +43,7 @@ func (s *Storage) FetchContents(_ context.Context, startingIndex uint64, maxResu var res []*storage.QueuedTransaction it := s.db.NewIterator([]byte(""), idxToKey(startingIndex)) defer it.Release() - for i := 0; i < int(maxResults); i++ { + for i := uint64(0); i < maxResults; i++ { if !it.Next() { break } @@ -95,11 +96,11 @@ func (s *Storage) PruneAll(ctx context.Context) error { if err != nil { return fmt.Errorf("pruning all keys: %w", err) } - until, err := strconv.Atoi(string(idx)) + until, err := strconv.ParseUint(string(idx), 10, 64) if err != nil { return fmt.Errorf("converting last item index bytes to integer: %w", err) } - return s.Prune(ctx, uint64(until+1)) + return s.Prune(ctx, until+1) } func (s *Storage) Prune(ctx context.Context, until uint64) error { diff --git a/arbnode/dataposter/externalsignertest/externalsignertest.go b/arbnode/dataposter/externalsignertest/externalsignertest.go index 554defc764..51ccec1903 100644 --- a/arbnode/dataposter/externalsignertest/externalsignertest.go +++ b/arbnode/dataposter/externalsignertest/externalsignertest.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/offchainlabs/nitro/util/testhelpers" ) diff --git a/arbnode/dataposter/redis/redisstorage.go b/arbnode/dataposter/redis/redisstorage.go index 8b6dcf65ac..364f9fc85c 100644 --- a/arbnode/dataposter/redis/redisstorage.go +++ b/arbnode/dataposter/redis/redisstorage.go @@ -9,7 +9,8 @@ import ( "errors" "fmt" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" + "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/util/signature" ) @@ -196,7 +197,7 @@ func (s *Storage) Put(ctx context.Context, index uint64, prev, new *storage.Queu if err != nil { return err } - if err := pipe.ZAdd(ctx, s.key, &redis.Z{ + if err := pipe.ZAdd(ctx, s.key, redis.Z{ Score: float64(index), Member: string(signedItem), }).Err(); err != nil { diff --git a/arbnode/dataposter/slice/slicestorage.go b/arbnode/dataposter/slice/slicestorage.go index 69de7564a3..8685ed6f54 100644 --- a/arbnode/dataposter/slice/slicestorage.go +++ b/arbnode/dataposter/slice/slicestorage.go @@ -89,8 +89,8 @@ func (s *Storage) Put(_ context.Context, index uint64, prev, new *storage.Queued } s.queue = append(s.queue, newEnc) } else if index >= s.firstNonce { - queueIdx := int(index - s.firstNonce) - if queueIdx > len(s.queue) { + queueIdx := index - s.firstNonce + if queueIdx > uint64(len(s.queue)) { return fmt.Errorf("attempted to set out-of-bounds index %v in queue starting at %v of length %v", index, s.firstNonce, len(s.queue)) } prevEnc, err := s.encDec().Encode(prev) diff --git a/arbnode/dataposter/storage/storage.go b/arbnode/dataposter/storage/storage.go index 8e5a7e1798..dfd4c2745c 100644 --- a/arbnode/dataposter/storage/storage.go +++ b/arbnode/dataposter/storage/storage.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" + "github.com/offchainlabs/nitro/arbutil" ) diff --git a/arbnode/dataposter/storage/time.go b/arbnode/dataposter/storage/time.go index aa15f29170..82f8a3dbf5 100644 --- a/arbnode/dataposter/storage/time.go +++ b/arbnode/dataposter/storage/time.go @@ -34,11 +34,13 @@ func (b *RlpTime) DecodeRLP(s *rlp.Stream) error { if err != nil { return err } + // #nosec G115 *b = RlpTime(time.Unix(int64(enc.Seconds), int64(enc.Nanos))) return nil } func (b RlpTime) EncodeRLP(w io.Writer) error { + // #nosec G115 return rlp.Encode(w, rlpTimeEncoding{ Seconds: uint64(time.Time(b).Unix()), Nanos: uint64(time.Time(b).Nanosecond()), diff --git a/arbnode/dataposter/storage_test.go b/arbnode/dataposter/storage_test.go index e2aa321e0d..cd4e4babae 100644 --- a/arbnode/dataposter/storage_test.go +++ b/arbnode/dataposter/storage_test.go @@ -9,12 +9,14 @@ import ( "path" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" + "github.com/offchainlabs/nitro/arbnode/dataposter/dbstorage" "github.com/offchainlabs/nitro/arbnode/dataposter/redis" "github.com/offchainlabs/nitro/arbnode/dataposter/slice" @@ -72,24 +74,29 @@ func newRedisStorage(ctx context.Context, t *testing.T, encF storage.EncoderDeco func valueOf(t *testing.T, i int) *storage.QueuedTransaction { t.Helper() + // #nosec G115 meta, err := rlp.EncodeToBytes(storage.BatchPosterPosition{DelayedMessageCount: uint64(i)}) if err != nil { t.Fatalf("Encoding batch poster position, error: %v", err) } return &storage.QueuedTransaction{ FullTx: types.NewTransaction( + // #nosec G115 uint64(i), common.Address{}, big.NewInt(int64(i)), + // #nosec G115 uint64(i), big.NewInt(int64(i)), []byte{byte(i)}), Meta: meta, DeprecatedData: types.DynamicFeeTx{ - ChainID: big.NewInt(int64(i)), - Nonce: uint64(i), - GasTipCap: big.NewInt(int64(i)), - GasFeeCap: big.NewInt(int64(i)), + ChainID: big.NewInt(int64(i)), + // #nosec G115 + Nonce: uint64(i), + GasTipCap: big.NewInt(int64(i)), + GasFeeCap: big.NewInt(int64(i)), + // #nosec G115 Gas: uint64(i), Value: big.NewInt(int64(i)), Data: []byte{byte(i % 8)}, @@ -113,6 +120,7 @@ func values(t *testing.T, from, to int) []*storage.QueuedTransaction { func initStorage(ctx context.Context, t *testing.T, s QueueStorage) QueueStorage { t.Helper() for i := 0; i < 20; i++ { + // #nosec G115 if err := s.Put(ctx, uint64(i), nil, valueOf(t, i)); err != nil { t.Fatalf("Error putting a key/value: %v", err) } @@ -153,6 +161,7 @@ func TestPruneAll(t *testing.T) { s := newLevelDBStorage(t, func() storage.EncoderDecoderInterface { return &storage.EncoderDecoder{} }) ctx := context.Background() for i := 0; i < 20; i++ { + // #nosec G115 if err := s.Put(ctx, uint64(i), nil, valueOf(t, i)); err != nil { t.Fatalf("Error putting a key/value: %v", err) } @@ -236,6 +245,7 @@ func TestLast(t *testing.T) { ctx := context.Background() for i := 0; i < cnt; i++ { val := valueOf(t, i) + // #nosec G115 if err := s.Put(ctx, uint64(i), nil, val); err != nil { t.Fatalf("Error putting a key/value: %v", err) } @@ -255,6 +265,7 @@ func TestLast(t *testing.T) { for i := 0; i < cnt-1; i++ { prev := valueOf(t, i) newVal := valueOf(t, cnt+i) + // #nosec G115 if err := s.Put(ctx, uint64(i), prev, newVal); err != nil { t.Fatalf("Error putting a key/value: %v, prev: %v, new: %v", err, prev, newVal) } @@ -362,6 +373,7 @@ func TestLength(t *testing.T) { if err != nil { t.Fatalf("Length() unexpected error: %v", err) } + // #nosec G115 if want := arbmath.MaxInt(0, 20-int(tc.pruneFrom)); got != want { t.Errorf("Length() = %d want %d", got, want) } diff --git a/arbnode/dataposter/testdata/client.crt b/arbnode/dataposter/testdata/client.crt index 3d494be820..9171094ba5 100644 --- a/arbnode/dataposter/testdata/client.crt +++ b/arbnode/dataposter/testdata/client.crt @@ -1,28 +1,25 @@ -----BEGIN CERTIFICATE----- -MIIE0jCCA7qgAwIBAgIUPaBB3/hHMpZfGB3VOw1+mHG4LnUwDQYJKoZIhvcNAQEL +MIIEIjCCAwqgAwIBAgIUV1axsouzA9h1Vgr2cPv17AvUrKswDQYJKoZIhvcNAQEL BQAwgYMxCzAJBgNVBAYTAkNIMQswCQYDVQQIDAJaSDEPMA0GA1UEBwwGWnVyaWNo MRYwFAYDVQQKDA1PZmZjaGFpbiBMYWJzMRIwEAYDVQQDDAlsb2NhbGhvc3QxKjAo -BgkqhkiG9w0BCQEWG25vdGFiaWdkZWFsQG9mZmNoYWlubGFicy5jaDAeFw0yMzEw -MTYxNDU2MjhaFw0yNDEwMTUxNDU2MjhaMIGDMQswCQYDVQQGEwJDSDELMAkGA1UE -CAwCWkgxDzANBgNVBAcMBlp1cmljaDEWMBQGA1UECgwNT2ZmY2hhaW4gTGFiczES -MBAGA1UEAwwJbG9jYWxob3N0MSowKAYJKoZIhvcNAQkBFhtub3RhYmlnZGVhbEBv -ZmZjaGFpbmxhYnMuY2gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1 -1asfUzv07QTVwlM4o3g51ilIFEApPkpdQej/GIItLEVRQW+GI9jYuEM07wdwMhSH -JPFNbZB3dmBuqDLx13hY03ufyeY+nab0/sO6x13kXChvIqgPRyJtkEAoYkMM3W0D -S6HeL/6DFoTQ2xAlZb/7i/9deuUwDL3MNVSjPCm9PjFzSOFgAQQud2uUT7aENGuG -Whw3oXz9gU/8gv3keLzcIa2PHyEW5M7jeGSYMjfW3wr0d+Z5mSNRc/U6kncKi06c -QrMKrgFfF7a5kHgxUL7bRCGgCMemXe7VfrW6oKT11JcLWDKhe+uo6bNXUptek55H -HfQi6x8cbM46/h3riZA3AgMBAAGjggE6MIIBNjAdBgNVHQ4EFgQUQD2BOems0+JQ -br234cW5noMmXRIwga0GA1UdIwSBpTCBoqGBiaSBhjCBgzELMAkGA1UEBhMCQ0gx -CzAJBgNVBAgMAlpIMQ8wDQYDVQQHDAZadXJpY2gxFjAUBgNVBAoMDU9mZmNoYWlu -IExhYnMxEjAQBgNVBAMMCWxvY2FsaG9zdDEqMCgGCSqGSIb3DQEJARYbbm90YWJp -Z2RlYWxAb2ZmY2hhaW5sYWJzLmNoghQ9oEHf+Ecyll8YHdU7DX6YcbgudTAJBgNV -HRMEAjAAMAsGA1UdDwQEAwIFoDAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4w -LjAuMTAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh -dGUwDQYJKoZIhvcNAQELBQADggEBAF4EVkOZZeMIvv0JViP7NsmIl2ke/935x6Hd -hQiLUw13XHYXzMa5/8Y5fnKjttBODpFoQlwjgI18vzuYzItYMBc2cabQJcpfG+Wq -M3m/wl1TC2XOuHj1E4RA/nU3tslntahtXG+vkks9RN+f9irHUhDRR6AGSnSB2Gi/ -B2OGmXn7S4Qge8+fGHAjN+tlu+tOoEWP6R3if/a9UIe5EGM8QTe4zw6lr+iPrOhC -M94pK5IEWn5IIGhr3zJIYkm/Dp+rFqhV1sqPOjjFLVCA7KJ3jVVVHlcm4Xa/+fyk -CIm7/VAmnbeUNlMbkXNOfQMeku8Iwsu80pvf3kjhU/PgO/5oojk= +BgkqhkiG9w0BCQEWG25vdGFiaWdkZWFsQG9mZmNoYWlubGFicy5jaDAgFw0yNDEw +MTYwMzI1NDdaGA8yMTI0MDkyMjAzMjU0N1owgYMxCzAJBgNVBAYTAkNIMQswCQYD +VQQIDAJaSDEPMA0GA1UEBwwGWnVyaWNoMRYwFAYDVQQKDA1PZmZjaGFpbiBMYWJz +MRIwEAYDVQQDDAlsb2NhbGhvc3QxKjAoBgkqhkiG9w0BCQEWG25vdGFiaWdkZWFs +QG9mZmNoYWlubGFicy5jaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKU5q5iwYV4/gPeWcyys561pTGV4pk+sRY2q0znsZFOYcxrjaXEjj2HGNkvH1rKy +8Cv1ZoFW+1ejQZeLtd0qL9v5fDkdLsmCZaIYI5Bvo2CfY6KLUZ5c1q2K2GZgQk8i +eSbqBXq+F/EwziDfheXkhDoAE05hOg684titb21eJ0ZK7f7Koam7cmbQI0lqUCrt +MLp0cJzWnfW0SpCzahnCZ5h31BZeZIRLOxsTvg5N1wOivrdWLXGVbprNCGGhVg0E +ZxhwI00pU/E/K4mcKjtPy/5fqe71jH7/iLYNmhRp6PrA78GilxTT79rro8ooantD +GyQbm+Qkk2tMHHum3GOcjuECAwEAAaOBiTCBhjAdBgNVHQ4EFgQUIUFF6jA0NkRA +1kZhJKH0W/9zJWIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwHwYDVR0RBBgwFoIJ +bG9jYWxob3N0ggkxMjcuMC4wLjEwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu +ZXJhdGVkIENlcnRpZmljYXRlMA0GCSqGSIb3DQEBCwUAA4IBAQCkfknujeFa0yf4 +YX/3ltP9itq4hLtAYnQF7M/uC86QdyDPsrNqhvj54qC0BnR5wGeZP3c144J2mAUr +4j4Y/ztgFVBR4rLyatHgm0/tL/fy/UgjeSmpY4UOr1QnpNP3fIzL7hxacS4uO8v4 +wcc5KlG/xjHRcrzJaaWLldCogBMb8vlModcbeKrkvQ4hUF+zf138RtpRfcRf1X5c +EaAtUZk+BxVYS79qL7YyESRD8YYMhIImLuiyPt2V3HQRhrjqa3mzODBLhUbNRWPX +8/CH2UZ6TD9Hy4FVX0VZzLoDZjfi4KCTgXI3WGrDoL4FF26cSiK8HVx0qJzAWw4a +tkkj5jtd -----END CERTIFICATE----- diff --git a/arbnode/dataposter/testdata/client.key b/arbnode/dataposter/testdata/client.key index b14941dd9f..4313d0f124 100644 --- a/arbnode/dataposter/testdata/client.key +++ b/arbnode/dataposter/testdata/client.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC11asfUzv07QTV -wlM4o3g51ilIFEApPkpdQej/GIItLEVRQW+GI9jYuEM07wdwMhSHJPFNbZB3dmBu -qDLx13hY03ufyeY+nab0/sO6x13kXChvIqgPRyJtkEAoYkMM3W0DS6HeL/6DFoTQ -2xAlZb/7i/9deuUwDL3MNVSjPCm9PjFzSOFgAQQud2uUT7aENGuGWhw3oXz9gU/8 -gv3keLzcIa2PHyEW5M7jeGSYMjfW3wr0d+Z5mSNRc/U6kncKi06cQrMKrgFfF7a5 -kHgxUL7bRCGgCMemXe7VfrW6oKT11JcLWDKhe+uo6bNXUptek55HHfQi6x8cbM46 -/h3riZA3AgMBAAECggEADUboCYMCpm+LqIhzNCtqswQD6QsiSwCmqs8nuKZGk9ue -+hmZj5IpgMJZLrgvWY4s+PGfgiRR/28QCBrVXkETiZ5zirQFN4tvLlKcSK4xZf29 -FBRUCiPxck36NhiqrBNOi1Mn8BKedl4cESkvSu1cvcmeOh100HPcHfLDVqHx3qsl -D/5yMkT2+zdhtLa+X3nkAa+3aibOvgtyfkV679e20CG6h89N9GBKkTXO8ioLZZVm -84ksnd4FcpTo7ebJJxElEB+ZA4akPHbF6ArUmcpqtGso5GtwqqO2ZlguSn2XQT0d -jqvOG4DwfSXk6SpE/dpWvU92fmxWAxZvGrZNgDyJ2QKBgQDyQ8NN4b80Yza/YXar -LWx8A6B0eMc1dXgt9m3UUI+titt45jEcaXhCX01FRFTznWGmWFtJmcWBoaQVPVel -IcDYQSxEuBUrCeI75ocv/IQtENaiX3TK7Nlz5RHfpQpfDVJq45lpiD38CGkYkAif -9pSzC8aup4W3WR0JJZ1AOHUZaQKBgQDAJNJnaSNzB+eDWTKCIN5V9X3QMkmjsuir -Nf2lBXHYARnlYWAbtYFG12wLJQMTNX5ewVQQrWtsdPkGPpCnPLelUTxMssrsXjej -JlLzYUfzRBqEXMI3AA9bVdiauxId2RTcp2F81SM1keCMcuHYxrzVkBSOC9u3wCnb -Whb6+feInwKBgQCbzgC5AcoaQwReqKvNAvWV/C8hONvFAbs8tBOGTBlbHsZvRnun -Lh1tciUbuwp3cmvuszxiZUakS/RexIitZrvDWIbD2y+h8kVRCL1Am0HWSdH/syxF -pXVkF5obHuVApCyxGZb8S+axRCdy6I7jcY3IaHZqtMpGVEVcMJilSKnmoQKBgQCC -tEmgaMfhhx34nqOaG4vDA4T7LEolnh1h4g9RwztnCZC5FZ1QHA79xqrLhfjqhzgY -cwChe6aYl5WSptq1uLrgLTuMnQ8m7QyB4h8JSkKse8ZiBctjqJnJssLutpSjUzk6 -xG2vgjk6RqpuP/PcB40K5cDlw7FJ9OFEQqthPMsi1wKBgQC0/vv5bY3DQ+wV6gUy -nFoSa/XNHaa8y7jmmlCnWJqs6DAAQQ3VW0tPX03GYL/NDcI+PwzYDHDkSB6Qa/o8 -VzVGK1/kr/+bveNvqmi0vNb54fMFLveGgsY4Cu1cffiw8m6nYJ/V4eCsHfpF1B5L -5HDnt5rFKt1Mi9WsUSRtxipxBA== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClOauYsGFeP4D3 +lnMsrOetaUxleKZPrEWNqtM57GRTmHMa42lxI49hxjZLx9aysvAr9WaBVvtXo0GX +i7XdKi/b+Xw5HS7JgmWiGCOQb6Ngn2Oii1GeXNatithmYEJPInkm6gV6vhfxMM4g +34Xl5IQ6ABNOYToOvOLYrW9tXidGSu3+yqGpu3Jm0CNJalAq7TC6dHCc1p31tEqQ +s2oZwmeYd9QWXmSESzsbE74OTdcDor63Vi1xlW6azQhhoVYNBGcYcCNNKVPxPyuJ +nCo7T8v+X6nu9Yx+/4i2DZoUaej6wO/BopcU0+/a66PKKGp7QxskG5vkJJNrTBx7 +ptxjnI7hAgMBAAECggEAAagHWVuDTl+SmmjOtMby96ETm/zOpgPTGq14up7tDo17 +sexPtUum91L2XmIde+MhVz95jJhjoqhHUw6afyIaIrlojmYFfw2omSxmxt7no2NV +q158Lfs+R7UZoEUcxRBSaJp1/ZoEQW2800WKYRieXrp7dxCwdU9dctCiSlVkTWcU +w+f9xr084dUIKgtICbLWRdGDvGFmr99MBZXzHg5+x8MiAVtpiNcggRfQKIg2QYQv +xdUtfxrKHuRcbeo4QOgSR6fb772F5eO6hPfwgl0AqmSX9XyRaPox/vKcq531S95S +JvGeAdS47Qo8Elh9rIlC/pxVdJ/Gz4sbmlYCfcXDRQKBgQDbSFUrEaOnH5ITEDy/ +SDTCbdQ3bP1FmHVoLdCRoBohb0xHZrJoOn6cmxyyHWR6dwpv+rvi1bJCScDdphJM +zV8W5sG94PaM/dwCws4CFAwaAlMakrsNUXtMgIeub27mzX5OMTss8vjKdKbVDyAv +XCT4idJY1EOdLA3R+JTLSszxJwKBgQDA5CSKLn4HpZI6qmR5g7HRZ5g249BbX/q8 +oszAUIFfY0ME5aujWYRmTfdWno0Y2yG9x4g9QDVNK9fUH3Ii6pNRFGc/yF3HkbsP +kT6UW6rw9CeyyYPKjrFx7M+2kBWJ16+5noVvzWhLScMCt7IcVCKaqiJFapOkS75t +zBYH1IX0twKBgCtFBqlM/cIIlMZ2OcZ09RQ4n9ugAgotn11DTRivQvi+AYtFVIcE +o987LFppOl6ABus5ysFj8Zzq+MfD8XB+Rfk655gUQBJqNXPGBOicFBc9xjBEK+zg +2zepVRyymGuquPWs+URRXY51nkYEihFOWW1BpOQqXn0xKDj6mEHVLMOZAoGAc7Ol +k1ll8ZJIT3ZLxHPRYqmALVSjc1v0G9iPdsATijMRTUuyk84rU+5qcYOzYPh4mcyp +FQyBrGOjF7MxFG6epSDW+fRnBEGO8jyOTBFcTSI2+dBUhFjpaUvCIGD2+nLtDitf +IPwWFisNlYC4jrOM+jcZTYgrPX7NoDCt+k5pd6sCgYB7NMYEC9XjoXV8S3n2yni5 +y0oGLox39hh31LJ90yQ/l+oFJWn1BPnzTI6xeJTTzJjQf7padyvfw45b+3BywZHM +TI5LWcIaA6L+tiBlgBjYa7gE3CCxh0AdV+pUa8L/R6OWAK6+lg2zNt1/ommZ2sKg +LcbNAqMiMWH1a1el7idoBA== -----END PRIVATE KEY----- diff --git a/arbnode/dataposter/testdata/localhost.crt b/arbnode/dataposter/testdata/localhost.crt index ca33dfc8cc..8b7fb02c9a 100644 --- a/arbnode/dataposter/testdata/localhost.crt +++ b/arbnode/dataposter/testdata/localhost.crt @@ -1,28 +1,24 @@ -----BEGIN CERTIFICATE----- -MIIEwzCCA6ugAwIBAgIUHx3SdpCP5jXZE7USUqX5uRNFKPIwDQYJKoZIhvcNAQEL -BQAwfzELMAkGA1UEBhMCQ0gxCzAJBgNVBAgMAlpIMQ8wDQYDVQQHDAZadXJpY2gx -FjAUBgNVBAoMDU9mZmNoYWluIExhYnMxEjAQBgNVBAMMCWxvY2FsaG9zdDEmMCQG -CSqGSIb3DQEJARYXYmlnZGVhbEBvZmZjaGFpbmxhYnMuY2gwHhcNMjMxMDE2MTQ0 -MDA1WhcNMjQxMDE1MTQ0MDA1WjB/MQswCQYDVQQGEwJDSDELMAkGA1UECAwCWkgx -DzANBgNVBAcMBlp1cmljaDEWMBQGA1UECgwNT2ZmY2hhaW4gTGFiczESMBAGA1UE -AwwJbG9jYWxob3N0MSYwJAYJKoZIhvcNAQkBFhdiaWdkZWFsQG9mZmNoYWlubGFi -cy5jaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALg7XwaIh4l2Fp8a -MfNMdTQSMPMR0zpnicVTn/eiozWsqlAKaxmQM3PxJ0oVWW3iJ89p4rv5m+UjK6Dr -vsUQOzl8isgyGCTMnkLtxFlyallDNRDawRcuTPuNI9NkdJm+Zz7HooLzFeBDeS13 -iRPEXr1T/4af9MjOxqFvbw5xBY9k4tc2hPp6q00948gPWKIB9Mz4thoB2Hl2rQBY -X/WhjSnre9o9qoyBO0XAsG0mssBs1vPa9/aEp7C5cDY0HCuM1RIjhXnRpb8lC9VQ -aC+FozDffmm23EGVpLmyPs590UOtVJdTUd6Q0TAT6d7fjCRUJ12DendQf2uMFV90 -u6Yj0zUCAwEAAaOCATUwggExMB0GA1UdDgQWBBT2B3FTGFQ49JyBgDGLoZREOIGD -DTCBqAYDVR0jBIGgMIGdoYGEpIGBMH8xCzAJBgNVBAYTAkNIMQswCQYDVQQIDAJa -SDEPMA0GA1UEBwwGWnVyaWNoMRYwFAYDVQQKDA1PZmZjaGFpbiBMYWJzMRIwEAYD -VQQDDAlsb2NhbGhvc3QxJjAkBgkqhkiG9w0BCQEWF2JpZ2RlYWxAb2ZmY2hhaW5s -YWJzLmNoghQfHdJ2kI/mNdkTtRJSpfm5E0Uo8jAJBgNVHRMEAjAAMAsGA1UdDwQE -AwIFoDAfBgNVHREEGDAWgglsb2NhbGhvc3SCCTEyNy4wLjAuMTAsBglghkgBhvhC -AQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwDQYJKoZIhvcNAQEL -BQADggEBAIkhBcnLeeNwUwb+sSG4Qm8JdeplHPMeViNfFIflUfIIYS00JA2q9w8W -+6Nh8s6Dn20lQETUnesYj97BdqzLjFuJYAlblhE+zP8g/3Mkpu+wZAGvQjUIRyGT -C17BEtQQgAnv5pD22jr9hpLl2KowN6Oo1gzilCA+AtMkNZFIGDOxzuIv2u8rSD89 -R/V6UEDMCgusFJnZ/GzKkUNbsrAfNUezNUal+KzMhHGHBwg4jfCNhnAAB43eRtJA -0pSRMMLcUEQnVotXDXYC3DhJmkYp1uXOH/tWs6z9xForOkWFxNMVj+zUWBi7n3Jw -N2BXlb64D96uor13U0dmvQJ72ooJc+A= +MIIEFzCCAv+gAwIBAgITSiI3ITH8yVNHC4Bh412fjdSR2TANBgkqhkiG9w0BAQsF +ADB/MQswCQYDVQQGEwJDSDELMAkGA1UECAwCWkgxDzANBgNVBAcMBlp1cmljaDEW +MBQGA1UECgwNT2ZmY2hhaW4gTGFiczESMBAGA1UEAwwJbG9jYWxob3N0MSYwJAYJ +KoZIhvcNAQkBFhdiaWdkZWFsQG9mZmNoYWlubGFicy5jaDAgFw0yNDEwMTYwMzI1 +NDdaGA8yMTI0MDkyMjAzMjU0N1owfzELMAkGA1UEBhMCQ0gxCzAJBgNVBAgMAlpI +MQ8wDQYDVQQHDAZadXJpY2gxFjAUBgNVBAoMDU9mZmNoYWluIExhYnMxEjAQBgNV +BAMMCWxvY2FsaG9zdDEmMCQGCSqGSIb3DQEJARYXYmlnZGVhbEBvZmZjaGFpbmxh +YnMuY2gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK4BRVZ0nU98/f +l+QC4fF60oNtlMPCC/2R6GZoWz7VyhrrXBuol+F9vboAePxLgxeIr/pwHAbFWlW2 +ueAsN9dorC2waf5PDhfOE0gI6w7LysTkO5n7oMFf1KYPSpPJ15WxlobZR8qWeroR +we7z44tQ2F+es+HaqBrrk7jm0GS9AqaledN/ay9SP4CBu029F6nWDnK+VpNWuoN4 +A/pnwGFWrxDf0ftN7BxnxzzdsWs64+kYfz91Mojce2UKGuDTuk/oqOnHhX34bFDc +/e9KGAQqP1I+RuCJmQXW5b55+3WgpvT3u3Mp7478C+AK8GthPjja7go48nHp3uby +drNpTw+bAgMBAAGjgYkwgYYwHQYDVR0OBBYEFG09BO7OJcjB3fRFhPCsjQ6ICb2E +MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMB8GA1UdEQQYMBaCCWxvY2FsaG9zdIIJ +MTI3LjAuMC4xMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0 +aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAQEAJHG/5WOmpO7wg3BtTt4b0DoqDJjh +eQb9Woq5xbvliZ1RCp8E6m6BmOr66i4qu5r+31DEeAeQ2M9pG2nJKoayCVi2ygaQ +RAulxIH7o5+JUcZtX6FbRBsS7Go+SLmtkkJ89YVSIF40+2CAQs7loqQjHNeo9/iO +rVKt1Fa6rQhXmv4ItVOwRaMBvXRVw4gc3ObmH0ZBYZrvsE7uQkKX5f6sVKXOX3mm +ofyB+22QMYmx3XvEEQm8ELnjIr5Q8LxqQxHqjLFjyrcrXYVi4+3/PfjIdRr5+qes +H8JWJlAbF/SNncdXRb1jtkdxit56Qo7/Mz/c4Yuh1WLiYcQGJeBpr53dmg== -----END CERTIFICATE----- diff --git a/arbnode/dataposter/testdata/localhost.key b/arbnode/dataposter/testdata/localhost.key index aad9b40b3d..f56aef1e77 100644 --- a/arbnode/dataposter/testdata/localhost.key +++ b/arbnode/dataposter/testdata/localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4O18GiIeJdhaf -GjHzTHU0EjDzEdM6Z4nFU5/3oqM1rKpQCmsZkDNz8SdKFVlt4ifPaeK7+ZvlIyug -677FEDs5fIrIMhgkzJ5C7cRZcmpZQzUQ2sEXLkz7jSPTZHSZvmc+x6KC8xXgQ3kt -d4kTxF69U/+Gn/TIzsahb28OcQWPZOLXNoT6eqtNPePID1iiAfTM+LYaAdh5dq0A -WF/1oY0p63vaPaqMgTtFwLBtJrLAbNbz2vf2hKewuXA2NBwrjNUSI4V50aW/JQvV -UGgvhaMw335pttxBlaS5sj7OfdFDrVSXU1HekNEwE+ne34wkVCddg3p3UH9rjBVf -dLumI9M1AgMBAAECggEAHuc8oyKrQ5xmooUZHGP2pAeqJNfYXAtqoYpLwtUJ9hKy -1e7NdNIKw3fP/J4UrHk7btAm65us8hSCeMGatEErAhNZT0gR4zhcksMCBPQLkVIT -+HINYjdOzAJqoEbRRUnaVT5VDQy8HmyLCtyqhoGR18XbjshNnhKLYKCJ2z0Lrvf2 -3rU7bbt7/rvLitVhxVL8SIe2jWSfIgcEmEAZMigB9WAnUyQ/tAfbPy1I764LLfzD -nLXn7E2OH7GrxkLjOsH9kfERlur7V7IhC9NE/wI0q+rnILRa7Q3+ifRu8qla3bo1 -iyHl1ZmsYJ8Jnzbu9exzZaQmk42OoFPcMFm0mRe+2QKBgQDvRv0Q5JhBuVurkU98 -lzATwEO0uYmeWDMnHzrFSWAKr/x4LNQ9ytSCfe1aLxgOkZq6dQ3TyZiCYzpmwGz9 -K7/gghxmsVDKeCqiGVZOgFAWy7AhQyF6zM60oqqwSvJHhmGTsA/B5LPUiYe9lITW -ZSLVYkOzha7Coa++U8vPzI5VaQKBgQDFG4reFT79j8RKEm9jie6PdRdYMzOSDWty -Gjj5N9Jnlp1k/6RzCxjmp7w7yIorq/7fWZsQtt0UqgayOn25+I8dZeGC0BradUSB -tZbGElxPsF8Jg00ZvvK3G5mpZYDrJCud8Q05EaUZPXv9GuZhozEsTQgylVecVzsN -wyEK8VuZ7QKBgQChx9adUGIdtgzkILiknbh08j8U94mz1SCo5/WdpLHaKAlE29KZ -AQXUQP51Rng2iX4bab9yndCPADZheON3/debHX3EdUkRzFPPC+CN7TW5Y/jvVGtT -kxyDh6Ru1A2iDJr290iAKXjpUB/GL5/tMa5upiTuQYnasOWZgyC/nCf0WQKBgEwn -pRLDMLA1IMjhsInL3BEvU1KvjahLaQ0P1p1rlO6TAcLpBrewPPG5MwACLmhLLtFK -xJ/Dl02Jl8a61KLKxzi7iVLKZuWq00ouR8/FfkcHxOBfC6X74bkff9I0NogjVHrU -jKBVEe3blJEpGIP20mPka1tn2g68oUNi9dxNfm/NAoGAWj/Q0pgnNq0MQ8Lj6m99 -1baaXSo8biks3E3A3cqhHQm/j3SRnkf0lueQW8+r9yR9IWdYFXz5Waq13qK+lopE -KDmww0xr8dyMUYTP1vde7np2XKa/OX3iejDzbI3RcZN/DEV+dCBY8pqHHfaAaESu -fwBWvfD8wtwCZzB3lOZEi80= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK4BRVZ0nU98/f +l+QC4fF60oNtlMPCC/2R6GZoWz7VyhrrXBuol+F9vboAePxLgxeIr/pwHAbFWlW2 +ueAsN9dorC2waf5PDhfOE0gI6w7LysTkO5n7oMFf1KYPSpPJ15WxlobZR8qWeroR +we7z44tQ2F+es+HaqBrrk7jm0GS9AqaledN/ay9SP4CBu029F6nWDnK+VpNWuoN4 +A/pnwGFWrxDf0ftN7BxnxzzdsWs64+kYfz91Mojce2UKGuDTuk/oqOnHhX34bFDc +/e9KGAQqP1I+RuCJmQXW5b55+3WgpvT3u3Mp7478C+AK8GthPjja7go48nHp3uby +drNpTw+bAgMBAAECggEANE2Q8HOwlTdOYFbIcfXOS9v6BkZUMbLlrLg9rqnXiUaR +qhwVBWIiwEgpq/WFFfK2HodACacwF7EyZ+mD4eKDpni9Tr4E0lzPxlEyQRpYtjGQ +kUbMbBMFx68LIOYZM/Bgp2gnW90mXaVGU02sTTRctnsSK9g0Yir0xcdP5DHVxuRy +cReMIDjHu7/PN5X12oHpBxT1w7TKYZk3M2ZbmRcGfRSv1UIg06hXjxY4Tonqxv5b +Yv/R/r+RYsHE6cO3hLV7FE/ypaRFGt/qcHhqwmHKlpc+8/FNEpb5truXCbf5LhjM +YD2eMOpiBaiyufs+2BpUy/iDKToYz+fdusT5N3FgAQKBgQDuemOauY/ytbETUabB +Vt11fCvXvsx4L+45vqtVcRWl6jcy73rMnyA80D/ndzyxkuw2NRAllIUvsdVG26vM +8nWAxrsY7rrZ4kdRAAKAPYmTtT2O4mvhvJOYHA/ueEUamrKqZMgyLik1Wl1y2mt2 +Iak0zNB0GWoHsgDF20TTbxKTkwKBgQDZyAa/r3qhokKfvg/7LhwXH7vdiJd35zp1 +K1KZVeZf97FeReL4DLfTHZ92yFyVhWepp97Icd2WtQZ8jVVcesTP9C+mVoFEGXZf +nWx5y2WD92dBP/kKYNayXuBFQnRfS4AC7ALK8fKrU/Fzc7OctnkvHHWZfaKQ2sQ5 +xydj6Rso2QKBgBpgvT2zAsIU6MY7RNej1REWr/7IIvO0UYRfm7HytTNJ6dsfdBTI +ERfI7RicLsFxf+ErE2Mkv2qcH/wbdjBQLUEWOkGyvkY1ajACcURgCiSlam6wisBI +TIcJq5V0BijALbz9MsuiIXq+SRHYKQTDCmVFtlTxLrI1NTKtYzqD0akzAoGAB5fW +zGYk42/R3Nn2mq5f4lqD5VR224JfYmhxR9Fb5+qt73iGUlm3KxA0WCLiP4BYPe0R +cnGt5SxInp0a5c+N/yYnZyhK94Hfw7OsbY6u6mv82KSPXVJFChEOxrtrbUsnmnJ6 +InNPH7QcjgbxszwVe5QFcaWUvnIyN0V/VRdyj/kCgYEA0kPCIPR7t7OFEnQsw50W +AclrTlGRB0TzmCYZN4DTKC+1ZAV80XQUR4umJpHeyArdRZpFv+HBLF7fHsBnkmiU +ixBdbWgW4mxjAhyZkLMjcCkoiLmRPDUvELyFs4xpM+IEKoAk60PPbGi4CeIMHb5k +E47NZaw1bdG1tv2ya1FlQ/k= -----END PRIVATE KEY----- diff --git a/arbnode/dataposter/testdata/regenerate-certs.sh b/arbnode/dataposter/testdata/regenerate-certs.sh new file mode 100755 index 0000000000..6bcbd27b8e --- /dev/null +++ b/arbnode/dataposter/testdata/regenerate-certs.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -eu +cd "$(dirname "$0")" +for name in localhost client; do + openssl genrsa -out "$name.key" 2048 + csr="$(openssl req -new -key "$name.key" -config "$name.cnf" -batch)" + openssl x509 -req -signkey "$name.key" -out "$name.crt" -days 36500 -extensions req_ext -extfile "$name.cnf" <<< "$csr" +done diff --git a/arbnode/delay_buffer.go b/arbnode/delay_buffer.go new file mode 100644 index 0000000000..3f0514bbe2 --- /dev/null +++ b/arbnode/delay_buffer.go @@ -0,0 +1,87 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +// This file contains functions related to the delay buffer feature that are used mostly in the +// batch poster. + +package arbnode + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/bold/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/util/headerreader" +) + +// DelayBufferConfig originates from the sequencer inbox contract. +type DelayBufferConfig struct { + Enabled bool + Threshold uint64 +} + +// GetBufferConfig gets the delay buffer config from the sequencer inbox contract. +// If the contract doesn't support the delay buffer, it returns a config with Enabled set to false. +func GetDelayBufferConfig(ctx context.Context, sequencerInbox *bridgegen.SequencerInbox) ( + *DelayBufferConfig, error) { + + callOpts := bind.CallOpts{Context: ctx} + enabled, err := sequencerInbox.IsDelayBufferable(&callOpts) + if err != nil { + if headerreader.ExecutionRevertedRegexp.MatchString(err.Error()) { + return &DelayBufferConfig{Enabled: false}, nil + } + return nil, fmt.Errorf("retrieve SequencerInbox.isDelayBufferable: %w", err) + } + if !enabled { + return &DelayBufferConfig{Enabled: false}, nil + } + bufferData, err := sequencerInbox.Buffer(&callOpts) + if err != nil { + return nil, fmt.Errorf("retrieve SequencerInbox.buffer: %w", err) + } + config := &DelayBufferConfig{ + Enabled: true, + Threshold: bufferData.Threshold, + } + return config, nil +} + +// GenDelayProof generates the delay proof based on batch's first delayed message and the delayed +// accumulater from the inbox. +func GenDelayProof(ctx context.Context, message *arbostypes.MessageWithMetadata, inbox *InboxTracker) ( + *bridgegen.DelayProof, error) { + + if message.DelayedMessagesRead == 0 { + return nil, fmt.Errorf("BUG: trying to generate delay proof without delayed message") + } + seqNum := message.DelayedMessagesRead - 1 + var beforeDelayedAcc common.Hash + if seqNum > 0 { + var err error + beforeDelayedAcc, err = inbox.GetDelayedAcc(seqNum - 1) + if err != nil { + return nil, err + } + } + delayedMessage := bridgegen.MessagesMessage{ + Kind: message.Message.Header.Kind, + Sender: message.Message.Header.Poster, + BlockNumber: message.Message.Header.BlockNumber, + Timestamp: message.Message.Header.Timestamp, + InboxSeqNum: new(big.Int).SetUint64(seqNum), + BaseFeeL1: message.Message.Header.L1BaseFee, + MessageDataHash: crypto.Keccak256Hash(message.Message.L2msg), + } + delayProof := &bridgegen.DelayProof{ + BeforeDelayedAcc: beforeDelayedAcc, + DelayedMessage: delayedMessage, + } + return delayProof, nil +} diff --git a/arbnode/delayed.go b/arbnode/delayed.go index c166aa2b90..f28a9617a3 100644 --- a/arbnode/delayed.go +++ b/arbnode/delayed.go @@ -19,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" @@ -58,11 +59,11 @@ type DelayedBridge struct { con *bridgegen.IBridge address common.Address fromBlock uint64 - client arbutil.L1Interface + client *ethclient.Client messageProviders map[common.Address]*bridgegen.IDelayedMessageProvider } -func NewDelayedBridge(client arbutil.L1Interface, addr common.Address, fromBlock uint64) (*DelayedBridge, error) { +func NewDelayedBridge(client *ethclient.Client, addr common.Address, fromBlock uint64) (*DelayedBridge, error) { con, err := bridgegen.NewIBridge(addr, client) if err != nil { return nil, err @@ -215,7 +216,7 @@ func (b *DelayedBridge) logsToDeliveredMessages(ctx context.Context, logs []type } messages := make([]*DelayedInboxMessage, 0, len(logs)) - var lastParentChainBlockNumber uint64 + var lastParentChainBlockHash common.Hash var lastL1BlockNumber uint64 for _, parsedLog := range parsedLogs { msgKey := common.BigToHash(parsedLog.MessageIndex) @@ -228,17 +229,17 @@ func (b *DelayedBridge) logsToDeliveredMessages(ctx context.Context, logs []type } requestId := common.BigToHash(parsedLog.MessageIndex) - parentChainBlockNumber := parsedLog.Raw.BlockNumber + parentChainBlockHash := parsedLog.Raw.BlockHash var l1BlockNumber uint64 - if lastParentChainBlockNumber == parentChainBlockNumber && lastParentChainBlockNumber > 0 { + if lastParentChainBlockHash == parentChainBlockHash && lastParentChainBlockHash != (common.Hash{}) { l1BlockNumber = lastL1BlockNumber } else { - var err error - l1BlockNumber, err = arbutil.CorrespondingL1BlockNumber(ctx, b.client, parentChainBlockNumber) + parentChainHeader, err := b.client.HeaderByHash(ctx, parentChainBlockHash) if err != nil { return nil, err } - lastParentChainBlockNumber = parentChainBlockNumber + l1BlockNumber = arbutil.ParentHeaderToL1BlockNumber(parentChainHeader) + lastParentChainBlockHash = parentChainBlockHash lastL1BlockNumber = l1BlockNumber } msg := &DelayedInboxMessage{ @@ -333,7 +334,11 @@ func (b *DelayedBridge) parseMessage(ctx context.Context, ethLog types.Log) (*bi if err != nil { return nil, nil, err } - return parsedLog.MessageNum, args["messageData"].([]byte), nil + dataBytes, ok := args["messageData"].([]byte) + if !ok { + return nil, nil, errors.New("messageData not a byte array") + } + return parsedLog.MessageNum, dataBytes, nil default: return nil, nil, errors.New("unexpected log type") } diff --git a/arbnode/delayed_seq_reorg_test.go b/arbnode/delayed_seq_reorg_test.go index 699eb3e8f6..f821d71e63 100644 --- a/arbnode/delayed_seq_reorg_test.go +++ b/arbnode/delayed_seq_reorg_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/solgen/go/bridgegen" ) diff --git a/arbnode/delayed_sequencer.go b/arbnode/delayed_sequencer.go index 4f18531a76..235a747446 100644 --- a/arbnode/delayed_sequencer.go +++ b/arbnode/delayed_sequencer.go @@ -9,11 +9,13 @@ import ( "fmt" "math/big" "sync" + "time" + + flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" - flag "github.com/spf13/pflag" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/execution" @@ -29,16 +31,17 @@ type DelayedSequencer struct { reader *InboxReader exec execution.ExecutionSequencer coordinator *SeqCoordinator - waitingForFinalizedBlock uint64 + waitingForFinalizedBlock *uint64 mutex sync.Mutex config DelayedSequencerConfigFetcher } type DelayedSequencerConfig struct { - Enable bool `koanf:"enable" reload:"hot"` - FinalizeDistance int64 `koanf:"finalize-distance" reload:"hot"` - RequireFullFinality bool `koanf:"require-full-finality" reload:"hot"` - UseMergeFinality bool `koanf:"use-merge-finality" reload:"hot"` + Enable bool `koanf:"enable" reload:"hot"` + FinalizeDistance int64 `koanf:"finalize-distance" reload:"hot"` + RequireFullFinality bool `koanf:"require-full-finality" reload:"hot"` + UseMergeFinality bool `koanf:"use-merge-finality" reload:"hot"` + RescanInterval time.Duration `koanf:"rescan-interval" reload:"hot"` } type DelayedSequencerConfigFetcher func() *DelayedSequencerConfig @@ -48,6 +51,7 @@ func DelayedSequencerConfigAddOptions(prefix string, f *flag.FlagSet) { f.Int64(prefix+".finalize-distance", DefaultDelayedSequencerConfig.FinalizeDistance, "how many blocks in the past L1 block is considered final (ignored when using Merge finality)") f.Bool(prefix+".require-full-finality", DefaultDelayedSequencerConfig.RequireFullFinality, "whether to wait for full finality before sequencing delayed messages") f.Bool(prefix+".use-merge-finality", DefaultDelayedSequencerConfig.UseMergeFinality, "whether to use The Merge's notion of finality before sequencing delayed messages") + f.Duration(prefix+".rescan-interval", DefaultDelayedSequencerConfig.RescanInterval, "frequency to rescan for new delayed messages (the parent chain reader's poll-interval config is more important than this)") } var DefaultDelayedSequencerConfig = DelayedSequencerConfig{ @@ -55,6 +59,7 @@ var DefaultDelayedSequencerConfig = DelayedSequencerConfig{ FinalizeDistance: 20, RequireFullFinality: false, UseMergeFinality: true, + RescanInterval: time.Second, } var TestDelayedSequencerConfig = DelayedSequencerConfig{ @@ -62,6 +67,7 @@ var TestDelayedSequencerConfig = DelayedSequencerConfig{ FinalizeDistance: 20, RequireFullFinality: false, UseMergeFinality: false, + RescanInterval: time.Millisecond * 100, } func NewDelayedSequencer(l1Reader *headerreader.HeaderReader, reader *InboxReader, exec execution.ExecutionSequencer, coordinator *SeqCoordinator, config DelayedSequencerConfigFetcher) (*DelayedSequencer, error) { @@ -121,16 +127,16 @@ func (d *DelayedSequencer) sequenceWithoutLockout(ctx context.Context, lastBlock if currentNum < config.FinalizeDistance { return nil } + // #nosec G115 finalized = uint64(currentNum - config.FinalizeDistance) } - if d.waitingForFinalizedBlock > finalized { + if d.waitingForFinalizedBlock != nil && *d.waitingForFinalizedBlock > finalized { return nil } - // Unless we find an unfinalized message (which sets waitingForBlock), - // we won't find a new finalized message until FinalizeDistance blocks in the future. - d.waitingForFinalizedBlock = lastBlockHeader.Number.Uint64() + 1 + // Reset what block we're waiting for if we've caught up + d.waitingForFinalizedBlock = nil dbDelayedCount, err := d.inbox.GetDelayedCount() if err != nil { @@ -151,8 +157,8 @@ func (d *DelayedSequencer) sequenceWithoutLockout(ctx context.Context, lastBlock return err } if parentChainBlockNumber > finalized { - // Message isn't finalized yet; stop here - d.waitingForFinalizedBlock = parentChainBlockNumber + // Message isn't finalized yet; wait for it to be + d.waitingForFinalizedBlock = &parentChainBlockNumber break } if lastDelayedAcc != (common.Hash{}) { @@ -189,6 +195,7 @@ func (d *DelayedSequencer) sequenceWithoutLockout(ctx context.Context, lastBlock return fmt.Errorf("inbox reader at delayed message %v db accumulator %v doesn't match delayed bridge accumulator %v at L1 block %v", pos-1, lastDelayedAcc, delayedBridgeAcc, finalized) } for i, msg := range messages { + // #nosec G115 err = d.exec.SequenceDelayedMessage(msg, startPos+uint64(i)) if err != nil { return err @@ -213,20 +220,40 @@ func (d *DelayedSequencer) run(ctx context.Context) { headerChan, cancel := d.l1Reader.Subscribe(false) defer cancel() + latestHeader, err := d.l1Reader.LastHeader(ctx) + if err != nil { + log.Warn("delayed sequencer: failed to get latest header", "err", err) + latestHeader = nil + } + rescanTimer := time.NewTimer(d.config().RescanInterval) for { + if !rescanTimer.Stop() { + select { + case <-rescanTimer.C: + default: + } + } + if latestHeader != nil { + rescanTimer.Reset(d.config().RescanInterval) + } + var ok bool select { - case nextHeader, ok := <-headerChan: + case latestHeader, ok = <-headerChan: if !ok { - log.Info("delayed sequencer: header channel close") + log.Debug("delayed sequencer: header channel close") return } - if err := d.trySequence(ctx, nextHeader); err != nil { - log.Error("Delayed sequencer error", "err", err) + case <-rescanTimer.C: + if latestHeader == nil { + continue } case <-ctx.Done(): - log.Info("delayed sequencer: context done", "err", ctx.Err()) + log.Debug("delayed sequencer: context done", "err", ctx.Err()) return } + if err := d.trySequence(ctx, latestHeader); err != nil { + log.Error("Delayed sequencer error", "err", err) + } } } diff --git a/arbnode/inbox_reader.go b/arbnode/inbox_reader.go index 77a0b6e7a2..50893ca392 100644 --- a/arbnode/inbox_reader.go +++ b/arbnode/inbox_reader.go @@ -13,9 +13,11 @@ import ( "sync/atomic" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" - flag "github.com/spf13/pflag" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/arbmath" @@ -93,7 +95,7 @@ type InboxReader struct { delayedBridge *DelayedBridge sequencerInbox *SequencerInbox caughtUpChan chan struct{} - client arbutil.L1Interface + client *ethclient.Client l1Reader *headerreader.HeaderReader // Atomic @@ -101,7 +103,7 @@ type InboxReader struct { lastReadBatchCount atomic.Uint64 } -func NewInboxReader(tracker *InboxTracker, client arbutil.L1Interface, l1Reader *headerreader.HeaderReader, firstMessageBlock *big.Int, delayedBridge *DelayedBridge, sequencerInbox *SequencerInbox, config InboxReaderConfigFetcher) (*InboxReader, error) { +func NewInboxReader(tracker *InboxTracker, client *ethclient.Client, l1Reader *headerreader.HeaderReader, firstMessageBlock *big.Int, delayedBridge *DelayedBridge, sequencerInbox *SequencerInbox, config InboxReaderConfigFetcher) (*InboxReader, error) { err := config().Validate() if err != nil { return nil, err @@ -228,6 +230,26 @@ func (r *InboxReader) CaughtUp() chan struct{} { return r.caughtUpChan } +type lazyHashLogging struct { + f func() common.Hash +} + +func (l lazyHashLogging) String() string { + return l.f().String() +} + +func (l lazyHashLogging) TerminalString() string { + return l.f().TerminalString() +} + +func (l lazyHashLogging) MarshalText() ([]byte, error) { + return l.f().MarshalText() +} + +func (l lazyHashLogging) Format(s fmt.State, c rune) { + l.f().Format(s, c) +} + func (r *InboxReader) run(ctx context.Context, hadError bool) error { readMode := r.config().ReadMode from, err := r.getNextBlockToRead(ctx) @@ -333,6 +355,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { return err } if ourLatestDelayedCount < checkingDelayedCount { + log.Debug("Expecting to find delayed messages", "checkingDelayedCount", checkingDelayedCount, "ourLatestDelayedCount", ourLatestDelayedCount, "currentHeight", currentHeight) checkingDelayedCount = ourLatestDelayedCount missingDelayed = true } else if ourLatestDelayedCount > checkingDelayedCount { @@ -353,6 +376,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { return err } if dbDelayedAcc != l1DelayedAcc { + log.Debug("Latest delayed accumulator mismatch", "delayedSeqNum", checkingDelayedSeqNum, "dbDelayedAcc", dbDelayedAcc, "l1DelayedAcc", l1DelayedAcc) reorgingDelayed = true } } @@ -370,6 +394,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { return err } if ourLatestBatchCount < checkingBatchCount { + log.Debug("Expecting to find sequencer batches", "checkingBatchCount", checkingBatchCount, "ourLatestBatchCount", ourLatestBatchCount, "currentHeight", currentHeight) checkingBatchCount = ourLatestBatchCount missingSequencer = true } else if ourLatestBatchCount > checkingBatchCount && config.HardReorg { @@ -389,6 +414,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { return err } if dbBatchAcc != l1BatchAcc { + log.Debug("Latest sequencer batch accumulator mismatch", "batchSeqNum", checkingBatchSeqNum, "dbBatchAcc", dbBatchAcc, "l1BatchAcc", l1BatchAcc) reorgingSequencer = true } } @@ -431,14 +457,23 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { if to.Cmp(currentHeight) > 0 { to.Set(currentHeight) } + log.Debug( + "Looking up messages", + "from", from.String(), + "to", to.String(), + "missingDelayed", missingDelayed, + "missingSequencer", missingSequencer, + "reorgingDelayed", reorgingDelayed, + "reorgingSequencer", reorgingSequencer, + ) sequencerBatches, err := r.sequencerInbox.LookupBatchesInRange(ctx, from, to) if err != nil { return err } delayedMessages, err := r.delayedBridge.LookupMessagesInRange(ctx, from, to, func(batchNum uint64) ([]byte, error) { if len(sequencerBatches) > 0 && batchNum >= sequencerBatches[0].SequenceNumber { - idx := int(batchNum - sequencerBatches[0].SequenceNumber) - if idx < len(sequencerBatches) { + idx := batchNum - sequencerBatches[0].SequenceNumber + if idx < uint64(len(sequencerBatches)) { return sequencerBatches[idx].Serialize(ctx, r.l1Reader.Client()) } log.Warn("missing mentioned batch in L1 message lookup", "batch", batchNum) @@ -456,6 +491,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { if len(sequencerBatches) > 0 { missingSequencer = false reorgingSequencer = false + var havePrevAcc common.Hash firstBatch := sequencerBatches[0] if firstBatch.SequenceNumber > 0 { haveAcc, err := r.tracker.GetBatchAcc(firstBatch.SequenceNumber - 1) @@ -466,7 +502,10 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { } else if haveAcc != firstBatch.BeforeInboxAcc { reorgingSequencer = true } + havePrevAcc = haveAcc } + readLastAcc := sequencerBatches[len(sequencerBatches)-1].AfterInboxAcc + var duplicateBatches int if !reorgingSequencer { // Skip any batches we already have in the database for len(sequencerBatches) > 0 { @@ -481,6 +520,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { } else if haveAcc == batch.AfterInboxAcc { // Skip this batch, as we already have it in the database sequencerBatches = sequencerBatches[1:] + duplicateBatches++ } else { // The first batch AfterInboxAcc matches, but this batch doesn't, // so we'll successfully reorg it when we hit the addMessages @@ -488,7 +528,18 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { } } } + log.Debug( + "Found sequencer batches", + "firstSequenceNumber", firstBatch.SequenceNumber, + "newBatchesCount", len(sequencerBatches), + "duplicateBatches", duplicateBatches, + "reorgingSequencer", reorgingSequencer, + "readBeforeAcc", firstBatch.BeforeInboxAcc, + "haveBeforeAcc", havePrevAcc, + "readLastAcc", readLastAcc, + ) } else if missingSequencer && to.Cmp(currentHeight) >= 0 { + log.Debug("Didn't find expected sequencer batches", "from", from, "to", to, "currentHeight", currentHeight) // We were missing sequencer batches but didn't find any. // This must mean that the sequencer batches are in the past. reorgingSequencer = true @@ -503,6 +554,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { if err != nil { return err } + var havePrevAcc common.Hash if beforeCount > 0 { haveAcc, err := r.tracker.GetDelayedAcc(beforeCount - 1) if errors.Is(err, AccumulatorNotFoundErr) { @@ -512,14 +564,27 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { } else if haveAcc != beforeAcc { reorgingDelayed = true } + havePrevAcc = haveAcc } + log.Debug( + "Found delayed messages", + "firstSequenceNumber", beforeCount, + "count", len(delayedMessages), + "reorgingDelayed", reorgingDelayed, + "readBeforeAcc", beforeAcc, + "haveBeforeAcc", havePrevAcc, + "readLastAcc", lazyHashLogging{func() common.Hash { + // Only compute this if we need to log it, as it's somewhat expensive + return delayedMessages[len(delayedMessages)-1].AfterInboxAcc() + }}, + ) } else if missingDelayed && to.Cmp(currentHeight) >= 0 { + log.Debug("Didn't find expected delayed messages", "from", from, "to", to, "currentHeight", currentHeight) // We were missing delayed messages but didn't find any. // This must mean that the delayed messages are in the past. reorgingDelayed = true } - log.Trace("looking up messages", "from", from.String(), "to", to.String(), "missingDelayed", missingDelayed, "missingSequencer", missingSequencer, "reorgingDelayed", reorgingDelayed, "reorgingSequencer", reorgingSequencer) if !reorgingDelayed && !reorgingSequencer && (len(delayedMessages) != 0 || len(sequencerBatches) != 0) { delayedMismatch, err := r.addMessages(ctx, sequencerBatches, delayedMessages) if err != nil { @@ -534,14 +599,7 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { storeSeenBatchCount() } } - if reorgingDelayed || reorgingSequencer { - from, err = r.getPrevBlockForReorg(from) - if err != nil { - return err - } - } else { - from = arbmath.BigAddByUint(to, 1) - } + // #nosec G115 haveMessages := uint64(len(delayedMessages) + len(sequencerBatches)) if haveMessages <= (config.TargetMessagesRead / 2) { blocksToFetch += (blocksToFetch + 4) / 5 @@ -554,6 +612,14 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { } else if blocksToFetch > config.MaxBlocksToRead { blocksToFetch = config.MaxBlocksToRead } + if reorgingDelayed || reorgingSequencer { + from, err = r.getPrevBlockForReorg(from, blocksToFetch) + if err != nil { + return err + } + } else { + from = arbmath.BigAddByUint(to, 1) + } } if !readAnyBatches { @@ -577,11 +643,11 @@ func (r *InboxReader) addMessages(ctx context.Context, sequencerBatches []*Seque return false, nil } -func (r *InboxReader) getPrevBlockForReorg(from *big.Int) (*big.Int, error) { +func (r *InboxReader) getPrevBlockForReorg(from *big.Int, maxBlocksBackwards uint64) (*big.Int, error) { if from.Cmp(r.firstMessageBlock) <= 0 { return nil, errors.New("can't get older messages") } - newFrom := arbmath.BigSub(from, big.NewInt(10)) + newFrom := arbmath.BigSub(from, new(big.Int).SetUint64(maxBlocksBackwards)) if newFrom.Cmp(r.firstMessageBlock) < 0 { newFrom = new(big.Int).Set(r.firstMessageBlock) } diff --git a/arbnode/inbox_test.go b/arbnode/inbox_test.go index 1c46c593b9..28a5d587a9 100644 --- a/arbnode/inbox_test.go +++ b/arbnode/inbox_test.go @@ -11,23 +11,23 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/execution/gethexec" "github.com/offchainlabs/nitro/statetransfer" - "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/testhelpers" "github.com/offchainlabs/nitro/util/testhelpers/env" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/params" - "github.com/offchainlabs/nitro/arbos" ) type execClientWrapper struct { @@ -45,7 +45,7 @@ func (w *execClientWrapper) FullSyncProgressMap() map[string]interface{} { } func NewTransactionStreamerForTest(t *testing.T, ownerAddress common.Address) (*gethexec.ExecutionEngine, *TransactionStreamer, ethdb.Database, *core.BlockChain) { - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() initData := statetransfer.ArbosInitializationInfo{ Accounts: []statetransfer.AccountInitializationInfo{ @@ -72,7 +72,9 @@ func NewTransactionStreamerForTest(t *testing.T, ownerAddress common.Address) (* if err != nil { Fail(t, err) } - if err := execEngine.Initialize(gethexec.DefaultCachingConfig.StylusLRUCache, &gethexec.DefaultStylusTargetConfig); err != nil { + stylusTargetConfig := &gethexec.DefaultStylusTargetConfig + Require(t, stylusTargetConfig.Validate()) // pre-processes config (i.a. parses wasmTargets) + if err := execEngine.Initialize(gethexec.DefaultCachingConfig.StylusLRUCacheCapacity, &gethexec.DefaultStylusTargetConfig); err != nil { Fail(t, err) } execSeq := &execClientWrapper{execEngine, t} @@ -182,7 +184,7 @@ func TestTransactionStreamer(t *testing.T) { state.balances[dest].Add(state.balances[dest], value) } - Require(t, inbox.AddMessages(state.numMessages, false, messages)) + Require(t, inbox.AddMessages(state.numMessages, false, messages, nil)) state.numMessages += arbutil.MessageIndex(len(messages)) prevBlockNumber := state.blockNumber diff --git a/arbnode/inbox_tracker.go b/arbnode/inbox_tracker.go index 52acaea863..a6989abff7 100644 --- a/arbnode/inbox_tracker.go +++ b/arbnode/inbox_tracker.go @@ -13,6 +13,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -604,7 +605,7 @@ type multiplexerBackend struct { positionWithinMessage uint64 ctx context.Context - client arbutil.L1Interface + client *ethclient.Client inbox *InboxTracker } @@ -644,7 +645,7 @@ func (b *multiplexerBackend) ReadDelayedInbox(seqNum uint64) (*arbostypes.L1Inco var delayedMessagesMismatch = errors.New("sequencer batch delayed messages missing or different") -func (t *InboxTracker) AddSequencerBatches(ctx context.Context, client arbutil.L1Interface, batches []*SequencerInboxBatch) error { +func (t *InboxTracker) AddSequencerBatches(ctx context.Context, client *ethclient.Client, batches []*SequencerInboxBatch) error { var nextAcc common.Hash var prevbatchmeta BatchMetadata sequenceNumberToKeep := uint64(0) @@ -701,22 +702,26 @@ func (t *InboxTracker) AddSequencerBatches(ctx context.Context, client arbutil.L for _, batch := range batches { if batch.SequenceNumber != pos { - return errors.New("unexpected batch sequence number") + return fmt.Errorf("unexpected batch sequence number %v expected %v", batch.SequenceNumber, pos) } if nextAcc != batch.BeforeInboxAcc { - return errors.New("previous batch accumulator mismatch") + return fmt.Errorf("previous batch accumulator %v mismatch expected %v", batch.BeforeInboxAcc, nextAcc) } if batch.AfterDelayedCount > 0 { haveDelayedAcc, err := t.GetDelayedAcc(batch.AfterDelayedCount - 1) - if errors.Is(err, AccumulatorNotFoundErr) { - // We somehow missed a referenced delayed message; go back and look for it - return delayedMessagesMismatch - } - if err != nil { + notFound := errors.Is(err, AccumulatorNotFoundErr) + if err != nil && !notFound { return err } - if haveDelayedAcc != batch.AfterDelayedAcc { + if notFound || haveDelayedAcc != batch.AfterDelayedAcc { + log.Debug( + "Delayed message accumulator doesn't match sequencer batch", + "batch", batch.SequenceNumber, + "delayedPosition", batch.AfterDelayedCount-1, + "haveDelayedAcc", haveDelayedAcc, + "batchDelayedAcc", batch.AfterDelayedAcc, + ) // We somehow missed a delayed message reorg; go back and look for it return delayedMessagesMismatch } @@ -809,6 +814,7 @@ func (t *InboxTracker) AddSequencerBatches(ctx context.Context, client arbutil.L if len(messages) > 0 { latestTimestamp = messages[len(messages)-1].Message.Header.Timestamp } + // #nosec G115 log.Info( "InboxTracker", "sequencerBatchCount", pos, @@ -816,7 +822,9 @@ func (t *InboxTracker) AddSequencerBatches(ctx context.Context, client arbutil.L "l1Block", latestL1Block, "l1Timestamp", time.Unix(int64(latestTimestamp), 0), ) + // #nosec G115 inboxLatestBatchGauge.Update(int64(pos)) + // #nosec G115 inboxLatestBatchMessageGauge.Update(int64(newMessageCount)) if t.validator != nil { @@ -824,7 +832,7 @@ func (t *InboxTracker) AddSequencerBatches(ctx context.Context, client arbutil.L } // This also writes the batch - err = t.txStreamer.AddMessagesAndEndBatch(prevbatchmeta.MessageCount, true, messages, dbBatch) + err = t.txStreamer.AddMessagesAndEndBatch(prevbatchmeta.MessageCount, true, messages, nil, dbBatch) if err != nil { return err } diff --git a/arbnode/inbox_tracker_test.go b/arbnode/inbox_tracker_test.go index 582b334aee..82d380b03c 100644 --- a/arbnode/inbox_tracker_test.go +++ b/arbnode/inbox_tracker_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/offchainlabs/nitro/util/containers" ) diff --git a/arbnode/maintenance.go b/arbnode/maintenance.go index 53d038a0f9..5e4e56b577 100644 --- a/arbnode/maintenance.go +++ b/arbnode/maintenance.go @@ -10,12 +10,14 @@ import ( "strings" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbnode/redislock" "github.com/offchainlabs/nitro/execution" "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" ) // Regularly runs db compaction if configured @@ -101,7 +103,7 @@ func NewMaintenanceRunner(config MaintenanceConfigFetcher, seqCoordinator *SeqCo if seqCoordinator != nil { c := func() *redislock.SimpleCfg { return &cfg.Lock } r := func() bool { return true } // always ready to lock - rl, err := redislock.NewSimple(seqCoordinator.Client, c, r) + rl, err := redislock.NewSimple(seqCoordinator.RedisCoordinator().Client, c, r) if err != nil { return nil, fmt.Errorf("creating new simple redis lock: %w", err) } diff --git a/arbnode/message_pruner.go b/arbnode/message_pruner.go index e1bc72632b..08f568796d 100644 --- a/arbnode/message_pruner.go +++ b/arbnode/message_pruner.go @@ -11,14 +11,14 @@ import ( "sync" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" - - flag "github.com/spf13/pflag" ) type MessagePruner struct { @@ -46,7 +46,7 @@ type MessagePrunerConfigFetcher func() *MessagePrunerConfig var DefaultMessagePrunerConfig = MessagePrunerConfig{ Enable: true, PruneInterval: time.Minute, - MinBatchesLeft: 2, + MinBatchesLeft: 1000, } func MessagePrunerConfigAddOptions(prefix string, f *flag.FlagSet) { @@ -112,6 +112,10 @@ func (m *MessagePruner) prune(ctx context.Context, count arbutil.MessageIndex, g } msgCount := endBatchMetadata.MessageCount delayedCount := endBatchMetadata.DelayedMessageCount + if delayedCount > 0 { + // keep an extra delayed message for the inbox reader to use + delayedCount-- + } return m.deleteOldMessagesFromDB(ctx, msgCount, delayedCount) } diff --git a/arbnode/message_pruner_test.go b/arbnode/message_pruner_test.go index e64bb4f838..8e6b744430 100644 --- a/arbnode/message_pruner_test.go +++ b/arbnode/message_pruner_test.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/arbutil" ) diff --git a/arbnode/node.go b/arbnode/node.go index 705a48da08..ec04cadd29 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -18,11 +18,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbnode/dataposter" "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/arbnode/resourcemanager" @@ -40,6 +42,9 @@ import ( "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/staker" + boldstaker "github.com/offchainlabs/nitro/staker/bold" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" + multiprotocolstaker "github.com/offchainlabs/nitro/staker/multi_protocol" "github.com/offchainlabs/nitro/staker/validatorwallet" "github.com/offchainlabs/nitro/util/contracts" "github.com/offchainlabs/nitro/util/headerreader" @@ -77,22 +82,23 @@ func GenerateRollupConfig(prod bool, wasmModuleRoot common.Hash, rollupOwner com } type Config struct { - Sequencer bool `koanf:"sequencer"` - ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` - InboxReader InboxReaderConfig `koanf:"inbox-reader" reload:"hot"` - DelayedSequencer DelayedSequencerConfig `koanf:"delayed-sequencer" reload:"hot"` - BatchPoster BatchPosterConfig `koanf:"batch-poster" reload:"hot"` - MessagePruner MessagePrunerConfig `koanf:"message-pruner" reload:"hot"` - BlockValidator staker.BlockValidatorConfig `koanf:"block-validator" reload:"hot"` - Feed broadcastclient.FeedConfig `koanf:"feed" reload:"hot"` - Staker staker.L1ValidatorConfig `koanf:"staker" reload:"hot"` - SeqCoordinator SeqCoordinatorConfig `koanf:"seq-coordinator"` - DataAvailability das.DataAvailabilityConfig `koanf:"data-availability"` - SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"` - Dangerous DangerousConfig `koanf:"dangerous"` - TransactionStreamer TransactionStreamerConfig `koanf:"transaction-streamer" reload:"hot"` - Maintenance MaintenanceConfig `koanf:"maintenance" reload:"hot"` - ResourceMgmt resourcemanager.Config `koanf:"resource-mgmt" reload:"hot"` + Sequencer bool `koanf:"sequencer"` + ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` + InboxReader InboxReaderConfig `koanf:"inbox-reader" reload:"hot"` + DelayedSequencer DelayedSequencerConfig `koanf:"delayed-sequencer" reload:"hot"` + BatchPoster BatchPosterConfig `koanf:"batch-poster" reload:"hot"` + MessagePruner MessagePrunerConfig `koanf:"message-pruner" reload:"hot"` + BlockValidator staker.BlockValidatorConfig `koanf:"block-validator" reload:"hot"` + Feed broadcastclient.FeedConfig `koanf:"feed" reload:"hot"` + Staker legacystaker.L1ValidatorConfig `koanf:"staker" reload:"hot"` + Bold boldstaker.BoldConfig `koanf:"bold"` + SeqCoordinator SeqCoordinatorConfig `koanf:"seq-coordinator"` + DataAvailability das.DataAvailabilityConfig `koanf:"data-availability"` + SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"` + Dangerous DangerousConfig `koanf:"dangerous"` + TransactionStreamer TransactionStreamerConfig `koanf:"transaction-streamer" reload:"hot"` + Maintenance MaintenanceConfig `koanf:"maintenance" reload:"hot"` + ResourceMgmt resourcemanager.Config `koanf:"resource-mgmt" reload:"hot"` // SnapSyncConfig is only used for testing purposes, these should not be configured in production. SnapSyncTest SnapSyncConfig } @@ -151,7 +157,8 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet, feedInputEnable bool, feed MessagePrunerConfigAddOptions(prefix+".message-pruner", f) staker.BlockValidatorConfigAddOptions(prefix+".block-validator", f) broadcastclient.FeedConfigAddOptions(prefix+".feed", f, feedInputEnable, feedOutputEnable) - staker.L1ValidatorConfigAddOptions(prefix+".staker", f) + legacystaker.L1ValidatorConfigAddOptions(prefix+".staker", f) + boldstaker.BoldConfigAddOptions(prefix+".bold", f) SeqCoordinatorConfigAddOptions(prefix+".seq-coordinator", f) das.DataAvailabilityConfigAddNodeOptions(prefix+".data-availability", f) SyncMonitorConfigAddOptions(prefix+".sync-monitor", f) @@ -169,7 +176,8 @@ var ConfigDefault = Config{ MessagePruner: DefaultMessagePrunerConfig, BlockValidator: staker.DefaultBlockValidatorConfig, Feed: broadcastclient.FeedConfigDefault, - Staker: staker.DefaultL1ValidatorConfig, + Staker: legacystaker.DefaultL1ValidatorConfig, + Bold: boldstaker.DefaultBoldConfig, SeqCoordinator: DefaultSeqCoordinatorConfig, DataAvailability: das.DefaultDataAvailabilityConfig, SyncMonitor: DefaultSyncMonitorConfig, @@ -201,7 +209,7 @@ func ConfigDefaultL1NonSequencerTest() *Config { config.SeqCoordinator.Enable = false config.BlockValidator = staker.TestBlockValidatorConfig config.SyncMonitor = TestSyncMonitorConfig - config.Staker = staker.TestL1ValidatorConfig + config.Staker = legacystaker.TestL1ValidatorConfig config.Staker.Enable = false config.BlockValidator.ValidationServerConfigs = []rpcclient.ClientConfig{{URL: ""}} @@ -217,7 +225,7 @@ func ConfigDefaultL2Test() *Config { config.Feed.Output.Signed = false config.SeqCoordinator.Signer.ECDSA.AcceptSequencer = false config.SeqCoordinator.Signer.ECDSA.Dangerous.AcceptMissing = true - config.Staker = staker.TestL1ValidatorConfig + config.Staker = legacystaker.TestL1ValidatorConfig config.SyncMonitor = TestSyncMonitorConfig config.Staker.Enable = false config.BlockValidator.ValidationServerConfigs = []rpcclient.ClientConfig{{URL: ""}} @@ -265,7 +273,7 @@ type Node struct { MessagePruner *MessagePruner BlockValidator *staker.BlockValidator StatelessBlockValidator *staker.StatelessBlockValidator - Staker *staker.Staker + Staker *multiprotocolstaker.MultiProtocolStaker BroadcastServer *broadcaster.Broadcaster BroadcastClients *broadcastclients.BroadcastClients SeqCoordinator *SeqCoordinator @@ -339,6 +347,29 @@ func checkArbDbSchemaVersion(arbDb ethdb.Database) error { return nil } +func DataposterOnlyUsedToCreateValidatorWalletContract( + ctx context.Context, + l1Reader *headerreader.HeaderReader, + transactOpts *bind.TransactOpts, + cfg *dataposter.DataPosterConfig, + parentChainID *big.Int, +) (*dataposter.DataPoster, error) { + cfg.UseNoOpStorage = true + return dataposter.NewDataPoster(ctx, + &dataposter.DataPosterOpts{ + HeaderReader: l1Reader, + Auth: transactOpts, + Config: func() *dataposter.DataPosterConfig { + return cfg + }, + MetadataRetriever: func(ctx context.Context, blockNum *big.Int) ([]byte, error) { + return nil, nil + }, + ParentChainID: parentChainID, + }, + ) +} + func StakerDataposter( ctx context.Context, db ethdb.Database, l1Reader *headerreader.HeaderReader, transactOpts *bind.TransactOpts, cfgFetcher ConfigFetcher, syncMonitor *SyncMonitor, @@ -384,7 +415,7 @@ func createNodeImpl( arbDb ethdb.Database, configFetcher ConfigFetcher, l2Config *params.ChainConfig, - l1client arbutil.L1Interface, + l1client *ethclient.Client, deployInfo *chaininfo.RollupAddresses, txOptsValidator *bind.TransactOpts, txOptsBatchPoster *bind.TransactOpts, @@ -515,6 +546,7 @@ func createNodeImpl( if err != nil { return nil, err } + // #nosec G115 sequencerInbox, err := NewSequencerInbox(l1client, deployInfo.SequencerInbox, int64(deployInfo.DeployedAt)) if err != nil { return nil, err @@ -607,7 +639,7 @@ func createNodeImpl( } } - var stakerObj *staker.Staker + var stakerObj *multiprotocolstaker.MultiProtocolStaker var messagePruner *MessagePruner var stakerAddr common.Address @@ -627,7 +659,7 @@ func createNodeImpl( getExtraGas := func() uint64 { return configFetcher.Get().Staker.ExtraGas } // TODO: factor this out into separate helper, and split rest of node // creation into multiple helpers. - var wallet staker.ValidatorWalletInterface = validatorwallet.NewNoOp(l1client, deployInfo.Rollup) + var wallet legacystaker.ValidatorWalletInterface = validatorwallet.NewNoOp(l1client, deployInfo.Rollup) if !strings.EqualFold(config.Staker.Strategy, "watchtower") { if config.Staker.UseSmartContractWallet || (txOptsValidator == nil && config.Staker.DataPoster.ExternalSigner.URL == "") { var existingWalletAddress *common.Address @@ -639,6 +671,7 @@ func createNodeImpl( tmpAddress := common.HexToAddress(config.Staker.ContractWalletAddress) existingWalletAddress = &tmpAddress } + // #nosec G115 wallet, err = validatorwallet.NewContract(dp, existingWalletAddress, deployInfo.ValidatorWalletCreator, deployInfo.Rollup, l1Reader, txOptsValidator, int64(deployInfo.DeployedAt), func(common.Address) {}, getExtraGas) if err != nil { return nil, err @@ -654,13 +687,13 @@ func createNodeImpl( } } - var confirmedNotifiers []staker.LatestConfirmedNotifier + var confirmedNotifiers []legacystaker.LatestConfirmedNotifier if config.MessagePruner.Enable { messagePruner = NewMessagePruner(txStreamer, inboxTracker, func() *MessagePrunerConfig { return &configFetcher.Get().MessagePruner }) confirmedNotifiers = append(confirmedNotifiers, messagePruner) } - stakerObj, err = staker.NewStaker(l1Reader, wallet, bind.CallOpts{}, func() *staker.L1ValidatorConfig { return &configFetcher.Get().Staker }, blockValidator, statelessBlockValidator, nil, confirmedNotifiers, deployInfo.ValidatorUtils, fatalErrChan) + stakerObj, err = multiprotocolstaker.NewMultiProtocolStaker(stack, l1Reader, wallet, bind.CallOpts{}, func() *legacystaker.L1ValidatorConfig { return &configFetcher.Get().Staker }, &configFetcher.Get().Bold, blockValidator, statelessBlockValidator, nil, deployInfo.StakeToken, confirmedNotifiers, deployInfo.ValidatorUtils, deployInfo.Bridge, fatalErrChan) if err != nil { return nil, err } @@ -670,11 +703,6 @@ func createNodeImpl( if dp != nil { stakerAddr = dp.Sender() } - whitelisted, err := stakerObj.IsWhitelisted(ctx) - if err != nil { - return nil, err - } - log.Info("running as validator", "txSender", stakerAddr, "actingAsWallet", wallet.Address(), "whitelisted", whitelisted, "strategy", config.Staker.Strategy) } var batchPoster *BatchPoster @@ -756,7 +784,7 @@ func CreateNode( arbDb ethdb.Database, configFetcher ConfigFetcher, l2Config *params.ChainConfig, - l1client arbutil.L1Interface, + l1client *ethclient.Client, deployInfo *chaininfo.RollupAddresses, txOptsValidator *bind.TransactOpts, txOptsBatchPoster *bind.TransactOpts, diff --git a/arbnode/redislock/redis.go b/arbnode/redislock/redis.go index 7e26010cae..075ff60c09 100644 --- a/arbnode/redislock/redis.go +++ b/arbnode/redislock/redis.go @@ -11,10 +11,12 @@ import ( "sync/atomic" "time" + "github.com/redis/go-redis/v9" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" + "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" ) type Simple struct { diff --git a/arbnode/resourcemanager/resource_management.go b/arbnode/resourcemanager/resource_management.go index aba823cc25..462b38c578 100644 --- a/arbnode/resourcemanager/resource_management.go +++ b/arbnode/resourcemanager/resource_management.go @@ -14,10 +14,11 @@ import ( "strings" "time" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/node" - "github.com/spf13/pflag" ) var ( @@ -256,6 +257,7 @@ func readIntFromFile(fileName string) (int, error) { if err != nil { return 0, err } + defer file.Close() var limit int if _, err = fmt.Fscanf(file, "%d", &limit); err != nil { @@ -269,6 +271,7 @@ func readFromMemStats(fileName string, re *regexp.Regexp) (int, error) { if err != nil { return 0, err } + defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { diff --git a/arbnode/seq_coordinator.go b/arbnode/seq_coordinator.go index a582b64ffa..d898ac25dd 100644 --- a/arbnode/seq_coordinator.go +++ b/arbnode/seq_coordinator.go @@ -14,7 +14,7 @@ import ( "sync/atomic" "time" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/log" @@ -37,7 +37,10 @@ var ( type SeqCoordinator struct { stopwaiter.StopWaiter - redisutil.RedisCoordinator + redisCoordinatorMutex sync.RWMutex + redisCoordinator redisutil.RedisCoordinator + prevRedisCoordinator *redisutil.RedisCoordinator + prevRedisMessageCount arbutil.MessageIndex sync *SyncMonitor streamer *TransactionStreamer @@ -61,9 +64,11 @@ type SeqCoordinatorConfig struct { Enable bool `koanf:"enable"` ChosenHealthcheckAddr string `koanf:"chosen-healthcheck-addr"` RedisUrl string `koanf:"redis-url"` + NewRedisUrl string `koanf:"new-redis-url"` LockoutDuration time.Duration `koanf:"lockout-duration"` LockoutSpare time.Duration `koanf:"lockout-spare"` SeqNumDuration time.Duration `koanf:"seq-num-duration"` + BlockMetadataDuration time.Duration `koanf:"block-metadata-duration"` UpdateInterval time.Duration `koanf:"update-interval"` RetryInterval time.Duration `koanf:"retry-interval"` HandoffTimeout time.Duration `koanf:"handoff-timeout"` @@ -86,10 +91,12 @@ func (c *SeqCoordinatorConfig) Url() string { func SeqCoordinatorConfigAddOptions(prefix string, f *flag.FlagSet) { f.Bool(prefix+".enable", DefaultSeqCoordinatorConfig.Enable, "enable sequence coordinator") f.String(prefix+".redis-url", DefaultSeqCoordinatorConfig.RedisUrl, "the Redis URL to coordinate via") + f.String(prefix+".new-redis-url", DefaultSeqCoordinatorConfig.NewRedisUrl, "switch to the new Redis URL to coordinate via") f.String(prefix+".chosen-healthcheck-addr", DefaultSeqCoordinatorConfig.ChosenHealthcheckAddr, "if non-empty, launch an HTTP service binding to this address that returns status code 200 when chosen and 503 otherwise") f.Duration(prefix+".lockout-duration", DefaultSeqCoordinatorConfig.LockoutDuration, "") f.Duration(prefix+".lockout-spare", DefaultSeqCoordinatorConfig.LockoutSpare, "") f.Duration(prefix+".seq-num-duration", DefaultSeqCoordinatorConfig.SeqNumDuration, "") + f.Duration(prefix+".block-metadata-duration", DefaultSeqCoordinatorConfig.BlockMetadataDuration, "") f.Duration(prefix+".update-interval", DefaultSeqCoordinatorConfig.UpdateInterval, "") f.Duration(prefix+".retry-interval", DefaultSeqCoordinatorConfig.RetryInterval, "") f.Duration(prefix+".handoff-timeout", DefaultSeqCoordinatorConfig.HandoffTimeout, "the maximum amount of time to spend waiting for another sequencer to accept the lockout when handing it off on shutdown or db compaction") @@ -105,9 +112,11 @@ var DefaultSeqCoordinatorConfig = SeqCoordinatorConfig{ Enable: false, ChosenHealthcheckAddr: "", RedisUrl: "", + NewRedisUrl: "", LockoutDuration: time.Minute, LockoutSpare: 30 * time.Second, SeqNumDuration: 10 * 24 * time.Hour, + BlockMetadataDuration: 10 * 24 * time.Hour, UpdateInterval: 250 * time.Millisecond, HandoffTimeout: 30 * time.Second, SafeShutdownDelay: 5 * time.Second, @@ -120,20 +129,22 @@ var DefaultSeqCoordinatorConfig = SeqCoordinatorConfig{ } var TestSeqCoordinatorConfig = SeqCoordinatorConfig{ - Enable: false, - RedisUrl: "", - LockoutDuration: time.Second * 2, - LockoutSpare: time.Millisecond * 10, - SeqNumDuration: time.Minute * 10, - UpdateInterval: time.Millisecond * 10, - HandoffTimeout: time.Millisecond * 200, - SafeShutdownDelay: time.Millisecond * 100, - ReleaseRetries: 4, - RetryInterval: time.Millisecond * 3, - MsgPerPoll: 20, - MyUrl: redisutil.INVALID_URL, - DeleteFinalizedMsgs: true, - Signer: signature.DefaultSignVerifyConfig, + Enable: false, + RedisUrl: "", + NewRedisUrl: "", + LockoutDuration: time.Second * 2, + LockoutSpare: time.Millisecond * 10, + SeqNumDuration: time.Minute * 10, + BlockMetadataDuration: time.Minute * 10, + UpdateInterval: time.Millisecond * 10, + HandoffTimeout: time.Millisecond * 200, + SafeShutdownDelay: time.Millisecond * 100, + ReleaseRetries: 4, + RetryInterval: time.Millisecond * 3, + MsgPerPoll: 20, + MyUrl: redisutil.INVALID_URL, + DeleteFinalizedMsgs: true, + Signer: signature.DefaultSignVerifyConfig, } func NewSeqCoordinator( @@ -153,7 +164,7 @@ func NewSeqCoordinator( return nil, err } coordinator := &SeqCoordinator{ - RedisCoordinator: *redisCoordinator, + redisCoordinator: *redisCoordinator, sync: sync, streamer: streamer, sequencer: sequencer, @@ -174,6 +185,19 @@ func (c *SeqCoordinator) SetDelayedSequencer(delayedSequencer *DelayedSequencer) c.delayedSequencer = delayedSequencer } +func (c *SeqCoordinator) RedisCoordinator() *redisutil.RedisCoordinator { + c.redisCoordinatorMutex.RLock() + defer c.redisCoordinatorMutex.RUnlock() + return &c.redisCoordinator +} + +func (c *SeqCoordinator) setRedisCoordinator(redisCoordinator *redisutil.RedisCoordinator) { + c.redisCoordinatorMutex.Lock() + defer c.redisCoordinatorMutex.Unlock() + c.prevRedisCoordinator = &c.redisCoordinator + c.redisCoordinator = *redisCoordinator +} + func StandaloneSeqCoordinatorInvalidateMsgIndex(ctx context.Context, redisClient redis.UniversalClient, keyConfig string, msgIndex arbutil.MessageIndex) error { signerConfig := signature.EmptySimpleHmacConfig if keyConfig == "" { @@ -246,7 +270,7 @@ func (c *SeqCoordinator) signedBytesToMsgCount(ctx context.Context, data []byte) } // Acquires or refreshes the chosen one lockout and optionally writes a message into redis atomically. -func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgCountExpected, msgCountToWrite arbutil.MessageIndex, lastmsg *arbostypes.MessageWithMetadata) error { +func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgCountExpected, msgCountToWrite arbutil.MessageIndex, lastmsg *arbostypes.MessageWithMetadata, blockMetadata arbostypes.BlockMetadata) error { var messageData *string var messageSigData *string if lastmsg != nil { @@ -276,7 +300,7 @@ func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgC defer c.wantsLockoutMutex.Unlock() setWantsLockout := c.avoidLockout <= 0 lockoutUntil := time.Now().Add(c.config.LockoutDuration) - err = c.Client.Watch(ctx, func(tx *redis.Tx) error { + err = c.RedisCoordinator().Client.Watch(ctx, func(tx *redis.Tx) error { current, err := tx.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() var wasEmpty bool if errors.Is(err, redis.Nil) { @@ -317,6 +341,9 @@ func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgC pipe.Set(ctx, redisutil.MessageSigKeyFor(msgCountToWrite-1), *messageSigData, c.config.SeqNumDuration) } } + if blockMetadata != nil { + pipe.Set(ctx, redisutil.BlockMetadataKeyFor(msgCountToWrite-1), string(blockMetadata), c.config.BlockMetadataDuration) + } pipe.PExpireAt(ctx, redisutil.CHOSENSEQ_KEY, lockoutUntil) if setWantsLockout { myWantsLockoutKey := redisutil.WantsLockoutKeyFor(c.config.Url()) @@ -345,7 +372,7 @@ func (c *SeqCoordinator) acquireLockoutAndWriteMessage(ctx context.Context, msgC } func (c *SeqCoordinator) getRemoteFinalizedMsgCount(ctx context.Context) (arbutil.MessageIndex, error) { - resStr, err := c.Client.Get(ctx, redisutil.FINALIZED_MSG_COUNT_KEY).Result() + resStr, err := c.RedisCoordinator().Client.Get(ctx, redisutil.FINALIZED_MSG_COUNT_KEY).Result() if err != nil { return 0, err } @@ -364,23 +391,23 @@ func (c *SeqCoordinator) getRemoteMsgCountImpl(ctx context.Context, r redis.Cmda } func (c *SeqCoordinator) GetRemoteMsgCount() (arbutil.MessageIndex, error) { - return c.getRemoteMsgCountImpl(c.GetContext(), c.Client) + return c.getRemoteMsgCountImpl(c.GetContext(), c.RedisCoordinator().Client) } -func (c *SeqCoordinator) wantsLockoutUpdate(ctx context.Context) error { +func (c *SeqCoordinator) wantsLockoutUpdate(ctx context.Context, client redis.UniversalClient) error { c.wantsLockoutMutex.Lock() defer c.wantsLockoutMutex.Unlock() - return c.wantsLockoutUpdateWithMutex(ctx) + return c.wantsLockoutUpdateWithMutex(ctx, client) } // Requires the caller hold the wantsLockoutMutex -func (c *SeqCoordinator) wantsLockoutUpdateWithMutex(ctx context.Context) error { +func (c *SeqCoordinator) wantsLockoutUpdateWithMutex(ctx context.Context, client redis.UniversalClient) error { if c.avoidLockout > 0 { return nil } myWantsLockoutKey := redisutil.WantsLockoutKeyFor(c.config.Url()) wantsLockoutUntil := time.Now().Add(c.config.LockoutDuration) - pipe := c.Client.TxPipeline() + pipe := client.TxPipeline() initialDuration := c.config.LockoutDuration if initialDuration < 2*time.Second { initialDuration = 2 * time.Second @@ -398,7 +425,7 @@ func (c *SeqCoordinator) wantsLockoutUpdateWithMutex(ctx context.Context) error func (c *SeqCoordinator) chosenOneRelease(ctx context.Context) error { atomicTimeWrite(&c.lockoutUntil, time.Time{}) isActiveSequencer.Update(0) - releaseErr := c.Client.Watch(ctx, func(tx *redis.Tx) error { + releaseErr := c.RedisCoordinator().Client.Watch(ctx, func(tx *redis.Tx) error { current, err := tx.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() if errors.Is(err, redis.Nil) { return nil @@ -421,7 +448,7 @@ func (c *SeqCoordinator) chosenOneRelease(ctx context.Context) error { return nil } // got error - was it still released? - current, readErr := c.Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() + current, readErr := c.RedisCoordinator().Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() if errors.Is(readErr, redis.Nil) { return nil } @@ -438,10 +465,10 @@ func (c *SeqCoordinator) wantsLockoutRelease(ctx context.Context) error { return nil } myWantsLockoutKey := redisutil.WantsLockoutKeyFor(c.config.Url()) - releaseErr := c.Client.Del(ctx, myWantsLockoutKey).Err() + releaseErr := c.RedisCoordinator().Client.Del(ctx, myWantsLockoutKey).Err() if releaseErr != nil { // got error - was it still deleted? - readErr := c.Client.Get(ctx, myWantsLockoutKey).Err() + readErr := c.RedisCoordinator().Client.Get(ctx, myWantsLockoutKey).Err() if !errors.Is(readErr, redis.Nil) { return releaseErr } @@ -491,7 +518,7 @@ func (c *SeqCoordinator) updateWithLockout(ctx context.Context, nextChosen strin // Before proceeding, first try deleting finalized messages from redis and setting the finalizedMsgCount key finalized, err := c.sync.GetFinalizedMsgCount(ctx) if err != nil { - log.Warn("Error getting finalizedMessageCount from syncMonitor: %w", err) + log.Warn("Error getting finalizedMessageCount from syncMonitor", "err", err) } else if finalized == 0 { log.Warn("SyncMonitor returned zero finalizedMessageCount") } else if err := c.deleteFinalizedMsgsFromRedis(ctx, finalized); err != nil { @@ -509,7 +536,7 @@ func (c *SeqCoordinator) updateWithLockout(ctx context.Context, nextChosen strin log.Error("coordinator cannot read message count", "err", err) return c.config.UpdateInterval } - err = c.acquireLockoutAndWriteMessage(ctx, localMsgCount, localMsgCount, nil) + err = c.acquireLockoutAndWriteMessage(ctx, localMsgCount, localMsgCount, nil, nil) if err != nil { log.Warn("coordinator failed chosen-one keepalive", "err", err) return c.retryAfterRedisError() @@ -525,7 +552,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final // In non-init cases it doesn't matter how we delete as we always try to delete from prevFinalized to finalized batchDeleteCount := 1000 for i := len(keys); i > 0; i -= batchDeleteCount { - if err := c.Client.Del(ctx, keys[max(0, i-batchDeleteCount):i]...).Err(); err != nil { + if err := c.RedisCoordinator().Client.Del(ctx, keys[max(0, i-batchDeleteCount):i]...).Err(); err != nil { return fmt.Errorf("error deleting finalized messages and their signatures from redis: %w", err) } } @@ -534,7 +561,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final if err != nil { return err } - if err = c.Client.Set(ctx, redisutil.FINALIZED_MSG_COUNT_KEY, finalizedBytes, c.config.SeqNumDuration).Err(); err != nil { + if err = c.RedisCoordinator().Client.Set(ctx, redisutil.FINALIZED_MSG_COUNT_KEY, finalizedBytes, c.config.SeqNumDuration).Err(); err != nil { return fmt.Errorf("couldn't set %s key to current finalizedMsgCount in redis: %w", redisutil.FINALIZED_MSG_COUNT_KEY, err) } return nil @@ -543,7 +570,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final if errors.Is(err, redis.Nil) { var keys []string for msg := finalized - 1; msg > 0; msg-- { - exists, err := c.Client.Exists(ctx, redisutil.MessageKeyFor(msg), redisutil.MessageSigKeyFor(msg)).Result() + exists, err := c.RedisCoordinator().Client.Exists(ctx, redisutil.MessageKeyFor(msg), redisutil.MessageSigKeyFor(msg)).Result() if err != nil { // If there is an error deleting finalized messages during init, we retry later either from this sequencer or from another return err @@ -558,7 +585,7 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final } else if err != nil { return fmt.Errorf("error getting finalizedMsgCount value from redis: %w", err) } - remoteMsgCount, err := c.getRemoteMsgCountImpl(ctx, c.Client) + remoteMsgCount, err := c.getRemoteMsgCountImpl(ctx, c.RedisCoordinator().Client) if err != nil { return fmt.Errorf("cannot get remote message count: %w", err) } @@ -573,8 +600,19 @@ func (c *SeqCoordinator) deleteFinalizedMsgsFromRedis(ctx context.Context, final return nil } +func (c *SeqCoordinator) blockMetadataAt(ctx context.Context, pos arbutil.MessageIndex) (arbostypes.BlockMetadata, error) { + blockMetadataStr, err := c.RedisCoordinator().Client.Get(ctx, redisutil.BlockMetadataKeyFor(pos)).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, nil + } + return nil, err + } + return arbostypes.BlockMetadata(blockMetadataStr), nil +} + func (c *SeqCoordinator) update(ctx context.Context) time.Duration { - chosenSeq, err := c.RecommendSequencerWantingLockout(ctx) + chosenSeq, err := c.RedisCoordinator().RecommendSequencerWantingLockout(ctx) if err != nil { log.Warn("coordinator failed finding sequencer wanting lockout", "err", err) return c.retryAfterRedisError() @@ -603,6 +641,15 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { log.Error("cannot read message count", "err", err) return c.config.UpdateInterval } + // Cache the previous redis coordinator's message count + if c.prevRedisCoordinator != nil && c.prevRedisMessageCount == 0 { + prevRemoteMsgCount, err := c.getRemoteMsgCountImpl(ctx, c.prevRedisCoordinator.Client) + if err != nil { + log.Warn("cannot get remote message count", "err", err) + return c.retryAfterRedisError() + } + c.prevRedisMessageCount = prevRemoteMsgCount + } remoteFinalizedMsgCount, err := c.getRemoteFinalizedMsgCount(ctx) if err != nil { loglevel := log.Error @@ -617,13 +664,24 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { return c.retryAfterRedisError() } readUntil := min(localMsgCount+c.config.MsgPerPoll, remoteMsgCount) + client := c.RedisCoordinator().Client + // If we have a previous redis coordinator, + // we can read from it until the local message count catches up to the prev coordinator's message count + if c.prevRedisMessageCount > localMsgCount { + readUntil = min(readUntil, c.prevRedisMessageCount) + client = c.prevRedisCoordinator.Client + } + if c.prevRedisMessageCount != 0 && localMsgCount >= c.prevRedisMessageCount { + log.Info("coordinator caught up to prev redis coordinator", "msgcount", localMsgCount, "prevMsgCount", c.prevRedisMessageCount) + } var messages []arbostypes.MessageWithMetadata + var blockMetadataArr []arbostypes.BlockMetadata msgToRead := localMsgCount var msgReadErr error for msgToRead < readUntil && localMsgCount >= remoteFinalizedMsgCount { var resString string - resString, msgReadErr = c.Client.Get(ctx, redisutil.MessageKeyFor(msgToRead)).Result() - if msgReadErr != nil { + resString, msgReadErr = client.Get(ctx, redisutil.MessageKeyFor(msgToRead)).Result() + if msgReadErr != nil && c.sequencer.Synced() { log.Warn("coordinator failed reading message", "pos", msgToRead, "err", msgReadErr) break } @@ -631,7 +689,7 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { var sigString string var sigBytes []byte sigSeparateKey := true - sigString, msgReadErr = c.Client.Get(ctx, redisutil.MessageSigKeyFor(msgToRead)).Result() + sigString, msgReadErr = client.Get(ctx, redisutil.MessageSigKeyFor(msgToRead)).Result() if errors.Is(msgReadErr, redis.Nil) { // no separate signature. Try reading old-style sig if len(rsBytes) < 32 { @@ -677,10 +735,17 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { } } messages = append(messages, message) + blockMetadata, err := c.blockMetadataAt(ctx, msgToRead) + if err != nil { + log.Warn("SeqCoordinator failed reading blockMetadata from redis", "pos", msgToRead, "err", err) + msgReadErr = err + break + } + blockMetadataArr = append(blockMetadataArr, blockMetadata) msgToRead++ } if len(messages) > 0 { - if err := c.streamer.AddMessages(localMsgCount, false, messages); err != nil { + if err := c.streamer.AddMessages(localMsgCount, false, messages, blockMetadataArr); err != nil { log.Warn("coordinator failed to add messages", "err", err, "pos", localMsgCount, "length", len(messages)) } else { localMsgCount = msgToRead @@ -717,12 +782,12 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { // we're here because we don't currently hold the lock // sequencer is already either paused or forwarding c.sequencer.Pause() - err := c.acquireLockoutAndWriteMessage(ctx, localMsgCount, localMsgCount, nil) + err := c.acquireLockoutAndWriteMessage(ctx, localMsgCount, localMsgCount, nil, nil) if err != nil { // this could be just new messages we didn't get yet - even then, we should retry soon log.Info("sequencer failed to become chosen", "err", err, "msgcount", localMsgCount) // make sure we're marked as wanting the lockout - if err := c.wantsLockoutUpdate(ctx); err != nil { + if err := c.wantsLockoutUpdate(ctx, c.RedisCoordinator().Client); err != nil { log.Warn("failed to update wants lockout key", "err", err) } c.prevChosenSequencer = "" @@ -750,7 +815,7 @@ func (c *SeqCoordinator) update(ctx context.Context) time.Duration { // update wanting the lockout var wantsLockoutErr error if synced && !c.AvoidingLockout() { - wantsLockoutErr = c.wantsLockoutUpdate(ctx) + wantsLockoutErr = c.wantsLockoutUpdate(ctx, c.RedisCoordinator().Client) } else { wantsLockoutErr = c.wantsLockoutRelease(ctx) } @@ -817,12 +882,59 @@ func (c *SeqCoordinator) launchHealthcheckServer(ctx context.Context) { func (c *SeqCoordinator) Start(ctxIn context.Context) { c.StopWaiter.Start(ctxIn, c) - c.CallIteratively(c.update) + var newRedisCoordinator *redisutil.RedisCoordinator + if c.config.NewRedisUrl != "" { + var err error + newRedisCoordinator, err = redisutil.NewRedisCoordinator(c.config.NewRedisUrl) + if err != nil { + log.Warn("failed to create new redis coordinator", "err", + err, "newRedisUrl", c.config.NewRedisUrl) + } + } + c.CallIteratively(func(ctx context.Context) time.Duration { return c.chooseRedisAndUpdate(ctx, newRedisCoordinator) }) if c.config.ChosenHealthcheckAddr != "" { c.StopWaiter.LaunchThread(c.launchHealthcheckServer) } } +func (c *SeqCoordinator) chooseRedisAndUpdate(ctx context.Context, newRedisCoordinator *redisutil.RedisCoordinator) time.Duration { + // If we have a new redis coordinator, and we haven't switched to it yet, try to switch. + if c.config.NewRedisUrl != "" && c.prevRedisCoordinator == nil { + // If we fail to try to switch, we'll retry soon. + if err := c.trySwitchingRedis(ctx, newRedisCoordinator); err != nil { + log.Warn("error while trying to switch redis coordinator", "err", err) + return c.retryAfterRedisError() + } + } + return c.update(ctx) +} + +func (c *SeqCoordinator) trySwitchingRedis(ctx context.Context, newRedisCoordinator *redisutil.RedisCoordinator) error { + err := c.wantsLockoutUpdate(ctx, newRedisCoordinator.Client) + if err != nil { + return err + } + current, err := c.RedisCoordinator().Client.Get(ctx, redisutil.CHOSENSEQ_KEY).Result() + var wasEmpty bool + if errors.Is(err, redis.Nil) { + wasEmpty = true + err = nil + } + if err != nil { + log.Warn("failed to get current chosen sequencer", "err", err) + return err + } + // If the chosen key is set to switch, we need to switch to the new redis coordinator. + if !wasEmpty && (current == redisutil.SWITCHED_REDIS) { + err = c.wantsLockoutUpdate(ctx, c.RedisCoordinator().Client) + if err != nil { + return err + } + c.setRedisCoordinator(newRedisCoordinator) + } + return nil +} + // Calls check() every c.config.RetryInterval until it returns true, or the context times out. func (c *SeqCoordinator) waitFor(ctx context.Context, check func() bool) bool { for { @@ -872,18 +984,18 @@ func (c *SeqCoordinator) StopAndWait() { time.Sleep(c.retryAfterRedisError()) } } - _ = c.Client.Close() + _ = c.RedisCoordinator().Client.Close() } func (c *SeqCoordinator) CurrentlyChosen() bool { return time.Now().Before(atomicTimeRead(&c.lockoutUntil)) } -func (c *SeqCoordinator) SequencingMessage(pos arbutil.MessageIndex, msg *arbostypes.MessageWithMetadata) error { +func (c *SeqCoordinator) SequencingMessage(pos arbutil.MessageIndex, msg *arbostypes.MessageWithMetadata, blockMetadata arbostypes.BlockMetadata) error { if !c.CurrentlyChosen() { return fmt.Errorf("%w: not main sequencer", execution.ErrRetrySequencer) } - if err := c.acquireLockoutAndWriteMessage(c.GetContext(), pos, pos+1, msg); err != nil { + if err := c.acquireLockoutAndWriteMessage(c.GetContext(), pos, pos+1, msg, blockMetadata); err != nil { return err } return nil @@ -914,7 +1026,7 @@ func (c *SeqCoordinator) TryToHandoffChosenOne(ctx context.Context) bool { return !c.CurrentlyChosen() }) if success { - wantsLockout, err := c.RecommendSequencerWantingLockout(ctx) + wantsLockout, err := c.RedisCoordinator().RecommendSequencerWantingLockout(ctx) if err == nil { log.Info("released chosen one status; a new sequencer hopefully wants to acquire it", "delay", c.config.SafeShutdownDelay, "wantsLockout", wantsLockout) } else { @@ -936,7 +1048,7 @@ func (c *SeqCoordinator) SeekLockout(ctx context.Context) { log.Info("seeking lockout", "myUrl", c.config.Url()) if c.sequencer.Synced() { // Even if this errors we still internally marked ourselves as wanting the lockout - err := c.wantsLockoutUpdateWithMutex(ctx) + err := c.wantsLockoutUpdateWithMutex(ctx, c.RedisCoordinator().Client) if err != nil { log.Warn("failed to set wants lockout key in redis after seeking lockout again", "err", err) } diff --git a/arbnode/seq_coordinator_test.go b/arbnode/seq_coordinator_test.go index 6498543f3a..d459f59761 100644 --- a/arbnode/seq_coordinator_test.go +++ b/arbnode/seq_coordinator_test.go @@ -4,6 +4,7 @@ package arbnode import ( + "bytes" "context" "fmt" "math/rand" @@ -50,7 +51,7 @@ func coordinatorTestThread(ctx context.Context, coord *SeqCoordinator, data *Coo } asIndex := arbutil.MessageIndex(messageCount) holdingLockout := atomicTimeRead(&coord.lockoutUntil) - err := coord.acquireLockoutAndWriteMessage(ctx, asIndex, asIndex+1, &arbostypes.EmptyTestMessageWithMetadata) + err := coord.acquireLockoutAndWriteMessage(ctx, asIndex, asIndex+1, &arbostypes.EmptyTestMessageWithMetadata, nil) if err == nil { sequenced[messageCount] = true data.messageCount.Store(messageCount + 1) @@ -125,7 +126,7 @@ func TestRedisSeqCoordinatorAtomic(t *testing.T) { redisCoordinator, err := redisutil.NewRedisCoordinator(config.RedisUrl) Require(t, err) coordinator := &SeqCoordinator{ - RedisCoordinator: *redisCoordinator, + redisCoordinator: *redisCoordinator, config: config, signer: nullSigner, } @@ -181,7 +182,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { redisCoordinator, err := redisutil.NewRedisCoordinator(config.RedisUrl) Require(t, err) coordinator := &SeqCoordinator{ - RedisCoordinator: *redisCoordinator, + redisCoordinator: *redisCoordinator, config: config, signer: nullSigner, } @@ -191,18 +192,18 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { msgBytes, err := coordinator.msgCountToSignedBytes(0) Require(t, err) for i := arbutil.MessageIndex(1); i <= 10; i++ { - err = coordinator.Client.Set(ctx, redisutil.MessageKeyFor(i), msgBytes, time.Hour).Err() + err = coordinator.RedisCoordinator().Client.Set(ctx, redisutil.MessageKeyFor(i), msgBytes, time.Hour).Err() Require(t, err) - err = coordinator.Client.Set(ctx, redisutil.MessageSigKeyFor(i), msgBytes, time.Hour).Err() + err = coordinator.RedisCoordinator().Client.Set(ctx, redisutil.MessageSigKeyFor(i), msgBytes, time.Hour).Err() Require(t, err) keys = append(keys, redisutil.MessageKeyFor(i), redisutil.MessageSigKeyFor(i)) } // Set msgCount key msgCountBytes, err := coordinator.msgCountToSignedBytes(11) Require(t, err) - err = coordinator.Client.Set(ctx, redisutil.MSG_COUNT_KEY, msgCountBytes, time.Hour).Err() + err = coordinator.RedisCoordinator().Client.Set(ctx, redisutil.MSG_COUNT_KEY, msgCountBytes, time.Hour).Err() Require(t, err) - exists, err := coordinator.Client.Exists(ctx, keys...).Result() + exists, err := coordinator.RedisCoordinator().Client.Exists(ctx, keys...).Result() Require(t, err) if exists != 20 { t.Fatal("couldn't find all messages and signatures in redis") @@ -213,7 +214,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { Require(t, err) // Check if messages and signatures were deleted successfully - exists, err = coordinator.Client.Exists(ctx, keys[:8]...).Result() + exists, err = coordinator.RedisCoordinator().Client.Exists(ctx, keys[:8]...).Result() Require(t, err) if exists != 0 { t.Fatal("finalized messages and signatures in range 1 to 4 were not deleted") @@ -229,7 +230,7 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { // Try deleting finalized messages when theres already a finalizedMsgCount err = coordinator.deleteFinalizedMsgsFromRedis(ctx, 7) Require(t, err) - exists, err = coordinator.Client.Exists(ctx, keys[8:12]...).Result() + exists, err = coordinator.RedisCoordinator().Client.Exists(ctx, keys[8:12]...).Result() Require(t, err) if exists != 0 { t.Fatal("finalized messages and signatures in range 5 to 6 were not deleted") @@ -241,9 +242,48 @@ func TestSeqCoordinatorDeletesFinalizedMessages(t *testing.T) { } // Check that non-finalized messages are still available in redis - exists, err = coordinator.Client.Exists(ctx, keys[12:]...).Result() + exists, err = coordinator.RedisCoordinator().Client.Exists(ctx, keys[12:]...).Result() Require(t, err) if exists != 8 { t.Fatal("non-finalized messages and signatures in range 7 to 10 are not fully available") } } + +func TestSeqCoordinatorAddsBlockMetadata(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + coordConfig := TestSeqCoordinatorConfig + coordConfig.LockoutDuration = time.Millisecond * 100 + coordConfig.LockoutSpare = time.Millisecond * 10 + coordConfig.Signer.ECDSA.AcceptSequencer = false + coordConfig.Signer.SymmetricFallback = true + coordConfig.Signer.SymmetricSign = true + coordConfig.Signer.Symmetric.Dangerous.DisableSignatureVerification = true + coordConfig.Signer.Symmetric.SigningKey = "" + + nullSigner, err := signature.NewSignVerify(&coordConfig.Signer, nil, nil) + Require(t, err) + + redisUrl := redisutil.CreateTestRedis(ctx, t) + coordConfig.RedisUrl = redisUrl + + config := coordConfig + config.MyUrl = "test" + redisCoordinator, err := redisutil.NewRedisCoordinator(config.RedisUrl) + Require(t, err) + coordinator := &SeqCoordinator{ + redisCoordinator: *redisCoordinator, + config: config, + signer: nullSigner, + } + + pos := arbutil.MessageIndex(1) + blockMetadataWant := arbostypes.BlockMetadata{0, 4} + Require(t, coordinator.acquireLockoutAndWriteMessage(ctx, pos, pos+1, &arbostypes.EmptyTestMessageWithMetadata, blockMetadataWant)) + blockMetadataGot, err := coordinator.blockMetadataAt(ctx, pos) + Require(t, err) + if !bytes.Equal(blockMetadataWant, blockMetadataGot) { + t.Fatal("got incorrect blockMetadata") + } +} diff --git a/arbnode/sequencer_inbox.go b/arbnode/sequencer_inbox.go index 73e52ded53..9dae7cfb8d 100644 --- a/arbnode/sequencer_inbox.go +++ b/arbnode/sequencer_inbox.go @@ -15,9 +15,10 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/arbutil" - "github.com/offchainlabs/nitro/solgen/go/bridgegen" ) @@ -52,10 +53,10 @@ type SequencerInbox struct { con *bridgegen.SequencerInbox address common.Address fromBlock int64 - client arbutil.L1Interface + client *ethclient.Client } -func NewSequencerInbox(client arbutil.L1Interface, addr common.Address, fromBlock int64) (*SequencerInbox, error) { +func NewSequencerInbox(client *ethclient.Client, addr common.Address, fromBlock int64) (*SequencerInbox, error) { con, err := bridgegen.NewSequencerInbox(addr, client) if err != nil { return nil, err @@ -111,7 +112,7 @@ type SequencerInboxBatch struct { serialized []byte // nil if serialization isn't cached yet } -func (m *SequencerInboxBatch) getSequencerData(ctx context.Context, client arbutil.L1Interface) ([]byte, error) { +func (m *SequencerInboxBatch) getSequencerData(ctx context.Context, client *ethclient.Client) ([]byte, error) { switch m.dataLocation { case batchDataTxInput: data, err := arbutil.GetLogEmitterTxData(ctx, client, m.rawLog) @@ -123,7 +124,11 @@ func (m *SequencerInboxBatch) getSequencerData(ctx context.Context, client arbut if err != nil { return nil, err } - return args["data"].([]byte), nil + dataBytes, ok := args["data"].([]byte) + if !ok { + return nil, errors.New("args[\"data\"] not a byte array") + } + return dataBytes, nil case batchDataSeparateEvent: var numberAsHash common.Hash binary.BigEndian.PutUint64(numberAsHash[(32-8):], m.SequenceNumber) @@ -169,7 +174,7 @@ func (m *SequencerInboxBatch) getSequencerData(ctx context.Context, client arbut } } -func (m *SequencerInboxBatch) Serialize(ctx context.Context, client arbutil.L1Interface) ([]byte, error) { +func (m *SequencerInboxBatch) Serialize(ctx context.Context, client *ethclient.Client) ([]byte, error) { if m.serialized != nil { return m.serialized, nil } diff --git a/arbnode/sync_monitor.go b/arbnode/sync_monitor.go index 5ab1ede2d6..f06e9ca49f 100644 --- a/arbnode/sync_monitor.go +++ b/arbnode/sync_monitor.go @@ -5,10 +5,12 @@ import ( "sync" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" ) type SyncMonitor struct { @@ -144,11 +146,13 @@ func (s *SyncMonitor) FullSyncProgressMap() map[string]interface{} { batchProcessed := s.inboxReader.GetLastReadBatchCount() res["batchProcessed"] = batchProcessed - processedBatchMsgs, err := s.inboxReader.Tracker().GetBatchMessageCount(batchProcessed - 1) - if err != nil { - res["batchMetadataError"] = err.Error() - } else { - res["messageOfProcessedBatch"] = processedBatchMsgs + if batchProcessed > 0 { + processedBatchMsgs, err := s.inboxReader.Tracker().GetBatchMessageCount(batchProcessed - 1) + if err != nil { + res["batchMetadataError"] = err.Error() + } else { + res["messageOfProcessedBatch"] = processedBatchMsgs + } } l1reader := s.inboxReader.l1Reader diff --git a/arbnode/transaction_streamer.go b/arbnode/transaction_streamer.go index 1636e06bd3..4b7914ebc5 100644 --- a/arbnode/transaction_streamer.go +++ b/arbnode/transaction_streamer.go @@ -8,6 +8,7 @@ import ( "context" "encoding/binary" "encoding/json" + "errors" "fmt" "math/big" "reflect" @@ -17,8 +18,6 @@ import ( "testing" "time" - "errors" - flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" @@ -279,6 +278,7 @@ func (s *TransactionStreamer) reorg(batch ethdb.Batch, count arbutil.MessageInde return err } config := s.config() + // #nosec G115 maxResequenceMsgCount := count + arbutil.MessageIndex(config.MaxReorgResequenceDepth) if config.MaxReorgResequenceDepth >= 0 && maxResequenceMsgCount < targetMsgCount { log.Error( @@ -392,6 +392,7 @@ func (s *TransactionStreamer) reorg(batch ethdb.Batch, count arbutil.MessageInde } for i := 0; i < len(messagesResults); i++ { + // #nosec G115 pos := count + arbutil.MessageIndex(i) err = s.storeResult(pos, *messagesResults[i], batch) if err != nil { @@ -476,8 +477,11 @@ func (s *TransactionStreamer) getMessageWithMetadataAndBlockInfo(seqNum arbutil. key = dbKey(blockMetadataInputFeedPrefix, uint64(seqNum)) blockMetadata, err := s.db.Get(key) - if err != nil && !dbutil.IsErrNotFound(err) { - return nil, err + if err != nil { + if !dbutil.IsErrNotFound(err) { + return nil, err + } + blockMetadata = nil } msgWithBlockInfo := arbostypes.MessageWithMetadataAndBlockInfo{ @@ -517,8 +521,8 @@ func (s *TransactionStreamer) GetProcessedMessageCount() (arbutil.MessageIndex, return msgCount, nil } -func (s *TransactionStreamer) AddMessages(pos arbutil.MessageIndex, messagesAreConfirmed bool, messages []arbostypes.MessageWithMetadata) error { - return s.AddMessagesAndEndBatch(pos, messagesAreConfirmed, messages, nil) +func (s *TransactionStreamer) AddMessages(pos arbutil.MessageIndex, messagesAreConfirmed bool, messages []arbostypes.MessageWithMetadata, blockMetadataArr []arbostypes.BlockMetadata) error { + return s.AddMessagesAndEndBatch(pos, messagesAreConfirmed, messages, blockMetadataArr, nil) } func (s *TransactionStreamer) FeedPendingMessageCount() arbutil.MessageIndex { @@ -658,7 +662,7 @@ func (s *TransactionStreamer) AddFakeInitMessage() error { L2msg: msg, }, DelayedMessagesRead: 1, - }}) + }}, nil) } // Used in redis tests @@ -675,7 +679,7 @@ func endBatch(batch ethdb.Batch) error { return batch.Write() } -func (s *TransactionStreamer) AddMessagesAndEndBatch(pos arbutil.MessageIndex, messagesAreConfirmed bool, messages []arbostypes.MessageWithMetadata, batch ethdb.Batch) error { +func (s *TransactionStreamer) AddMessagesAndEndBatch(pos arbutil.MessageIndex, messagesAreConfirmed bool, messages []arbostypes.MessageWithMetadata, blockMetadataArr []arbostypes.BlockMetadata, batch ethdb.Batch) error { messagesWithBlockInfo := make([]arbostypes.MessageWithMetadataAndBlockInfo, 0, len(messages)) for _, message := range messages { messagesWithBlockInfo = append(messagesWithBlockInfo, arbostypes.MessageWithMetadataAndBlockInfo{ @@ -683,6 +687,14 @@ func (s *TransactionStreamer) AddMessagesAndEndBatch(pos arbutil.MessageIndex, m }) } + if len(blockMetadataArr) == len(messagesWithBlockInfo) { + for i, blockMetadata := range blockMetadataArr { + messagesWithBlockInfo[i].BlockMetadata = blockMetadata + } + } else if len(blockMetadataArr) > 0 { + return fmt.Errorf("size of blockMetadata array doesn't match the size of messages array. lockMetadataArrSize: %d, messagesSize: %d", len(blockMetadataArr), len(messages)) + } + if messagesAreConfirmed { // Trim confirmed messages from l1pricedataCache s.exec.MarkFeedStart(pos + arbutil.MessageIndex(len(messages))) @@ -692,7 +704,7 @@ func (s *TransactionStreamer) AddMessagesAndEndBatch(pos arbutil.MessageIndex, m if err != nil { return err } - if dups == len(messages) { + if dups == uint64(len(messages)) { return endBatch(batch) } // cant keep reorg lock when catching insertionMutex. @@ -727,10 +739,10 @@ func (s *TransactionStreamer) countDuplicateMessages( pos arbutil.MessageIndex, messages []arbostypes.MessageWithMetadataAndBlockInfo, batch *ethdb.Batch, -) (int, bool, *arbostypes.MessageWithMetadata, error) { - curMsg := 0 +) (uint64, bool, *arbostypes.MessageWithMetadata, error) { + var curMsg uint64 for { - if len(messages) == curMsg { + if uint64(len(messages)) == curMsg { break } key := dbKey(messagePrefix, uint64(pos)) @@ -830,7 +842,7 @@ func (s *TransactionStreamer) addMessagesAndEndBatchImpl(messageStartPos arbutil broadcastStartPos := arbutil.MessageIndex(s.broadcasterQueuedMessagesPos.Load()) if messagesAreConfirmed { - var duplicates int + var duplicates uint64 var err error duplicates, confirmedReorg, oldMsg, err = s.countDuplicateMessages(messageStartPos, messages, &batch) if err != nil { @@ -852,6 +864,7 @@ func (s *TransactionStreamer) addMessagesAndEndBatchImpl(messageStartPos arbutil // Active broadcast reorg and L1 messages at or before start of broadcast messages // Or no active broadcast reorg and broadcast messages start before or immediately after last L1 message if messagesAfterPos >= broadcastStartPos { + // #nosec G115 broadcastSliceIndex := int(messagesAfterPos - broadcastStartPos) messagesOldLen := len(messages) if broadcastSliceIndex < len(s.broadcasterQueuedMessages) { @@ -868,7 +881,7 @@ func (s *TransactionStreamer) addMessagesAndEndBatchImpl(messageStartPos arbutil var feedReorg bool if !hasNewConfirmedMessages { - var duplicates int + var duplicates uint64 var err error duplicates, feedReorg, oldMsg, err = s.countDuplicateMessages(messageStartPos, messages, nil) if err != nil { @@ -900,6 +913,7 @@ func (s *TransactionStreamer) addMessagesAndEndBatchImpl(messageStartPos arbutil // Validate delayed message counts of remaining messages for i, msg := range messages { + // #nosec G115 msgPos := messageStartPos + arbutil.MessageIndex(i) diff := msg.MessageWithMeta.DelayedMessagesRead - lastDelayedRead if diff != 0 && diff != 1 { @@ -935,6 +949,7 @@ func (s *TransactionStreamer) addMessagesAndEndBatchImpl(messageStartPos arbutil // Check if new messages were added at the end of cache, if they were, then dont remove those particular messages if len(s.broadcasterQueuedMessages) > cacheClearLen { s.broadcasterQueuedMessages = s.broadcasterQueuedMessages[cacheClearLen:] + // #nosec G115 s.broadcasterQueuedMessagesPos.Store(uint64(broadcastStartPos) + uint64(cacheClearLen)) } else { s.broadcasterQueuedMessages = s.broadcasterQueuedMessages[:0] @@ -980,7 +995,7 @@ func (s *TransactionStreamer) WriteMessageFromSequencer( } if s.coordinator != nil { - if err := s.coordinator.SequencingMessage(pos, &msgWithMeta); err != nil { + if err := s.coordinator.SequencingMessage(pos, &msgWithMeta, blockMetadata); err != nil { return err } } @@ -1068,6 +1083,7 @@ func (s *TransactionStreamer) writeMessages(pos arbutil.MessageIndex, messages [ batch = s.db.NewBatch() } for i, msg := range messages { + // #nosec G115 err := s.writeMessage(pos+arbutil.MessageIndex(i), msg, batch) if err != nil { return err @@ -1093,13 +1109,16 @@ func (s *TransactionStreamer) writeMessages(pos arbutil.MessageIndex, messages [ func (s *TransactionStreamer) BlockMetadataAtCount(count arbutil.MessageIndex) (arbostypes.BlockMetadata, error) { if count == 0 { - return []byte{}, nil + return nil, nil } pos := count - 1 key := dbKey(blockMetadataInputFeedPrefix, uint64(pos)) blockMetadata, err := s.db.Get(key) - if err != nil && !dbutil.IsErrNotFound(err) { + if err != nil { + if dbutil.IsErrNotFound(err) { + return nil, nil + } return nil, err } return blockMetadata, nil @@ -1173,7 +1192,7 @@ func (s *TransactionStreamer) storeResult( // exposed for testing // return value: true if should be called again immediately -func (s *TransactionStreamer) ExecuteNextMsg(ctx context.Context, exec execution.ExecutionSequencer) bool { +func (s *TransactionStreamer) ExecuteNextMsg(ctx context.Context) bool { if ctx.Err() != nil { return false } @@ -1248,7 +1267,7 @@ func (s *TransactionStreamer) ExecuteNextMsg(ctx context.Context, exec execution } func (s *TransactionStreamer) executeMessages(ctx context.Context, ignored struct{}) time.Duration { - if s.ExecuteNextMsg(ctx, s.exec) { + if s.ExecuteNextMsg(ctx) { return 0 } return s.config().ExecuteMessageLoopDelay diff --git a/arbos/activate_test.go b/arbos/activate_test.go index 55440bb208..b723c37aa6 100644 --- a/arbos/activate_test.go +++ b/arbos/activate_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/util/arbmath" @@ -20,6 +21,7 @@ func TestActivationDataFee(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) state, _ := arbosState.NewArbosMemoryBackedArbOSState() pricer := state.Programs().DataPricer() + // #nosec G115 time := uint64(time.Now().Unix()) assert := func(cond bool) { diff --git a/arbos/addressSet/addressSet.go b/arbos/addressSet/addressSet.go index 1f09ff1440..ccd780aa11 100644 --- a/arbos/addressSet/addressSet.go +++ b/arbos/addressSet/addressSet.go @@ -9,6 +9,8 @@ import ( "errors" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" ) @@ -79,6 +81,7 @@ func (as *AddressSet) AllMembers(maxNumToReturn uint64) ([]common.Address, error } ret := make([]common.Address, size) for i := range ret { + // #nosec G115 sba := as.backingStorage.OpenStorageBackedAddress(uint64(i + 1)) ret[i], err = sba.Get() if err != nil { @@ -183,7 +186,7 @@ func (as *AddressSet) Remove(addr common.Address, arbosVersion uint64) error { if err != nil { return err } - if arbosVersion >= 11 { + if arbosVersion >= params.ArbosVersion_11 { err = as.byAddress.Set(atSize, util.UintToHash(slot)) if err != nil { return err diff --git a/arbos/addressSet/addressSet_test.go b/arbos/addressSet/addressSet_test.go index 7d06c74f0b..b65c278b2b 100644 --- a/arbos/addressSet/addressSet_test.go +++ b/arbos/addressSet/addressSet_test.go @@ -8,16 +8,17 @@ import ( "math/rand" "testing" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/params" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/state" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -26,7 +27,7 @@ func TestEmptyAddressSet(t *testing.T) { sto := storage.NewMemoryBacked(burn.NewSystemBurner(nil, false)) Require(t, Initialize(sto)) aset := OpenAddressSet(sto) - version := params.ArbitrumDevTestParams().InitialArbOSVersion + version := chaininfo.ArbitrumDevTestParams().InitialArbOSVersion if size(t, aset) != 0 { Fail(t) @@ -49,7 +50,7 @@ func TestAddressSet(t *testing.T) { sto := storage.NewGeth(db, burn.NewSystemBurner(nil, false)) Require(t, Initialize(sto)) aset := OpenAddressSet(sto) - version := params.ArbitrumDevTestParams().InitialArbOSVersion + version := chaininfo.ArbitrumDevTestParams().InitialArbOSVersion statedb, _ := (db).(*state.StateDB) stateHashBeforeChanges := statedb.IntermediateRoot(false) @@ -144,7 +145,7 @@ func TestAddressSetAllMembers(t *testing.T) { sto := storage.NewGeth(db, burn.NewSystemBurner(nil, false)) Require(t, Initialize(sto)) aset := OpenAddressSet(sto) - version := params.ArbitrumDevTestParams().InitialArbOSVersion + version := chaininfo.ArbitrumDevTestParams().InitialArbOSVersion addr1 := testhelpers.RandomAddress() addr2 := testhelpers.RandomAddress() @@ -316,6 +317,7 @@ func checkIfRectifyMappingWorks(t *testing.T, aset *AddressSet, owners []common. Fail(t, "RectifyMapping did not fix the mismatch") } + // #nosec G115 if clearList && int(size(t, aset)) != index+1 { Fail(t, "RectifyMapping did not fix the mismatch") } diff --git a/arbos/addressTable/addressTable.go b/arbos/addressTable/addressTable.go index 3fbb7b3782..608883c34d 100644 --- a/arbos/addressTable/addressTable.go +++ b/arbos/addressTable/addressTable.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" + "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" ) @@ -103,6 +104,7 @@ func (atab *AddressTable) Decompress(buf []byte) (common.Address, uint64, error) return common.Address{}, 0, err } if len(input) == 20 { + // #nosec G115 numBytesRead := uint64(rd.Size() - int64(rd.Len())) return common.BytesToAddress(input), numBytesRead, nil } else { @@ -118,6 +120,7 @@ func (atab *AddressTable) Decompress(buf []byte) (common.Address, uint64, error) if !exists { return common.Address{}, 0, errors.New("invalid index in compressed address") } + // #nosec G115 numBytesRead := uint64(rd.Size() - int64(rd.Len())) return addr, numBytesRead, nil } diff --git a/arbos/addressTable/addressTable_test.go b/arbos/addressTable/addressTable_test.go index 6b06ed3406..873d5a4d1c 100644 --- a/arbos/addressTable/addressTable_test.go +++ b/arbos/addressTable/addressTable_test.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/util/testhelpers" diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 91c2207aae..5ee070f942 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -32,6 +32,7 @@ import ( "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/testhelpers/env" ) @@ -41,27 +42,25 @@ import ( // persisted beyond the end of the test.) type ArbosState struct { - arbosVersion uint64 // version of the ArbOS storage format and semantics - maxArbosVersionSupported uint64 // maximum ArbOS version supported by this code - maxDebugArbosVersionSupported uint64 // maximum ArbOS version supported by this code in debug mode - upgradeVersion storage.StorageBackedUint64 // version we're planning to upgrade to, or 0 if not planning to upgrade - upgradeTimestamp storage.StorageBackedUint64 // when to do the planned upgrade - networkFeeAccount storage.StorageBackedAddress - l1PricingState *l1pricing.L1PricingState - l2PricingState *l2pricing.L2PricingState - retryableState *retryables.RetryableState - addressTable *addressTable.AddressTable - chainOwners *addressSet.AddressSet - sendMerkle *merkleAccumulator.MerkleAccumulator - programs *programs.Programs - blockhashes *blockhash.Blockhashes - chainId storage.StorageBackedBigInt - chainConfig storage.StorageBackedBytes - genesisBlockNum storage.StorageBackedUint64 - infraFeeAccount storage.StorageBackedAddress - brotliCompressionLevel storage.StorageBackedUint64 // brotli compression level used for pricing - backingStorage *storage.Storage - Burner burn.Burner + arbosVersion uint64 // version of the ArbOS storage format and semantics + upgradeVersion storage.StorageBackedUint64 // version we're planning to upgrade to, or 0 if not planning to upgrade + upgradeTimestamp storage.StorageBackedUint64 // when to do the planned upgrade + networkFeeAccount storage.StorageBackedAddress + l1PricingState *l1pricing.L1PricingState + l2PricingState *l2pricing.L2PricingState + retryableState *retryables.RetryableState + addressTable *addressTable.AddressTable + chainOwners *addressSet.AddressSet + sendMerkle *merkleAccumulator.MerkleAccumulator + programs *programs.Programs + blockhashes *blockhash.Blockhashes + chainId storage.StorageBackedBigInt + chainConfig storage.StorageBackedBytes + genesisBlockNum storage.StorageBackedUint64 + infraFeeAccount storage.StorageBackedAddress + brotliCompressionLevel storage.StorageBackedUint64 // brotli compression level used for pricing + backingStorage *storage.Storage + Burner burn.Burner } var ErrUninitializedArbOS = errors.New("ArbOS uninitialized") @@ -78,8 +77,6 @@ func OpenArbosState(stateDB vm.StateDB, burner burn.Burner) (*ArbosState, error) } return &ArbosState{ arbosVersion, - 31, - 31, backingStorage.OpenStorageBackedUint64(uint64(upgradeVersionOffset)), backingStorage.OpenStorageBackedUint64(uint64(upgradeTimestampOffset)), backingStorage.OpenStorageBackedAddress(uint64(networkFeeAccountOffset)), @@ -129,7 +126,7 @@ func NewArbosMemoryBackedArbOSState() (*ArbosState, *state.StateDB) { log.Crit("failed to init empty statedb", "error", err) } burner := burn.NewSystemBurner(nil, false) - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() newState, err := InitializeArbosState(statedb, burner, chainConfig, arbostypes.TestInitMessage) if err != nil { log.Crit("failed to open the ArbOS state", "error", err) @@ -205,7 +202,7 @@ func InitializeArbosState(stateDB vm.StateDB, burner burn.Burner, chainConfig *p _ = sto.SetUint64ByUint64(uint64(versionOffset), 1) // initialize to version 1; upgrade at end of this func if needed _ = sto.SetUint64ByUint64(uint64(upgradeVersionOffset), 0) _ = sto.SetUint64ByUint64(uint64(upgradeTimestampOffset), 0) - if desiredArbosVersion >= 2 { + if desiredArbosVersion >= params.ArbosVersion_2 { _ = sto.SetByUint64(uint64(networkFeeAccountOffset), util.AddressToHash(initialChainOwner)) } else { _ = sto.SetByUint64(uint64(networkFeeAccountOffset), common.Hash{}) // the 0 address until an owner sets it @@ -217,7 +214,7 @@ func InitializeArbosState(stateDB vm.StateDB, burner burn.Burner, chainConfig *p _ = sto.SetUint64ByUint64(uint64(brotliCompressionLevelOffset), 0) // default brotliCompressionLevel for fast compression is 0 initialRewardsRecipient := l1pricing.BatchPosterAddress - if desiredArbosVersion >= 2 { + if desiredArbosVersion >= params.ArbosVersion_2 { initialRewardsRecipient = initialChainOwner } _ = l1pricing.InitializeL1PricingState(sto.OpenCachedSubStorage(l1PricingSubspace), initialRewardsRecipient, initMessage.InitialL1BaseFee) @@ -274,29 +271,29 @@ func (state *ArbosState) UpgradeArbosVersion( nextArbosVersion := state.arbosVersion + 1 switch nextArbosVersion { - case 2: + case params.ArbosVersion_2: ensure(state.l1PricingState.SetLastSurplus(common.Big0, 1)) - case 3: + case params.ArbosVersion_3: ensure(state.l1PricingState.SetPerBatchGasCost(0)) ensure(state.l1PricingState.SetAmortizedCostCapBips(math.MaxUint64)) - case 4: + case params.ArbosVersion_4: // no state changes needed - case 5: + case params.ArbosVersion_5: // no state changes needed - case 6: + case params.ArbosVersion_6: // no state changes needed - case 7: + case params.ArbosVersion_7: // no state changes needed - case 8: + case params.ArbosVersion_8: // no state changes needed - case 9: + case params.ArbosVersion_9: // no state changes needed - case 10: + case params.ArbosVersion_10: ensure(state.l1PricingState.SetL1FeesAvailable(stateDB.GetBalance( l1pricing.L1PricerFundsPoolAddress, ).ToBig())) - case 11: + case params.ArbosVersion_11: // Update the PerBatchGasCost to a more accurate value compared to the old v6 default. ensure(state.l1PricingState.SetPerBatchGasCost(l1pricing.InitialPerBatchGasCostV12)) @@ -316,22 +313,25 @@ func (state *ArbosState) UpgradeArbosVersion( case 12, 13, 14, 15, 16, 17, 18, 19: // these versions are left to Orbit chains for custom upgrades. - case 20: + case params.ArbosVersion_20: // Update Brotli compression level for fast compression from 0 to 1 ensure(state.SetBrotliCompressionLevel(1)) case 21, 22, 23, 24, 25, 26, 27, 28, 29: // these versions are left to Orbit chains for custom upgrades. - case 30: + case params.ArbosVersion_30: programs.Initialize(state.backingStorage.OpenSubStorage(programsSubspace)) - case 31: + case params.ArbosVersion_31: params, err := state.Programs().Params() ensure(err) ensure(params.UpgradeToVersion(2)) ensure(params.Save()) + case params.ArbosVersion_32: + // no change state needed + default: return fmt.Errorf( "the chain is upgrading to unsupported ArbOS version %v, %w", @@ -350,8 +350,8 @@ func (state *ArbosState) UpgradeArbosVersion( state.arbosVersion = nextArbosVersion } - if firstTime && upgradeTo >= 6 { - if upgradeTo < 11 { + if firstTime && upgradeTo >= params.ArbosVersion_6 { + if upgradeTo < params.ArbosVersion_11 { state.Restrict(state.l1PricingState.SetPerBatchGasCost(l1pricing.InitialPerBatchGasCostV6)) } state.Restrict(state.l1PricingState.SetEquilibrationUnits(l1pricing.InitialEquilibrationUnitsV6)) @@ -416,14 +416,6 @@ func (state *ArbosState) RetryableState() *retryables.RetryableState { return state.retryableState } -func (state *ArbosState) MaxArbosVersionSupported() uint64 { - return state.maxArbosVersionSupported -} - -func (state *ArbosState) MaxDebugArbosVersionSupported() uint64 { - return state.maxDebugArbosVersionSupported -} - func (state *ArbosState) L1PricingState() *l1pricing.L1PricingState { return state.l1PricingState } diff --git a/arbos/arbosState/arbosstate_test.go b/arbos/arbosState/arbosstate_test.go index ef63c23386..440598991c 100644 --- a/arbos/arbosState/arbosstate_test.go +++ b/arbos/arbosState/arbosstate_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" diff --git a/arbos/arbosState/initialization_test.go b/arbos/arbosState/initialization_test.go index 34802392fe..8ccfbc7cfb 100644 --- a/arbos/arbosState/initialization_test.go +++ b/arbos/arbosState/initialization_test.go @@ -13,9 +13,10 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/statetransfer" "github.com/offchainlabs/nitro/util/testhelpers" "github.com/offchainlabs/nitro/util/testhelpers/env" @@ -61,12 +62,11 @@ func tryMarshalUnmarshal(input *statetransfer.ArbosInitializationInfo, t *testin raw := rawdb.NewMemoryDatabase() initReader := statetransfer.NewMemoryInitDataReader(&initData) - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() cacheConfig := core.DefaultCacheConfigWithScheme(env.GetTestStateScheme()) stateroot, err := InitializeArbosInDatabase(raw, cacheConfig, initReader, chainConfig, arbostypes.TestInitMessage, 0, 0) Require(t, err) - triedbConfig := cacheConfig.TriedbConfig() stateDb, err := state.New(stateroot, state.NewDatabaseWithConfig(raw, triedbConfig), nil) Require(t, err) @@ -109,6 +109,7 @@ func pseudorandomAccountInitInfoForTesting(prand *testhelpers.PseudoRandomDataSo } func pseudorandomHashHashMapForTesting(prand *testhelpers.PseudoRandomDataSource, maxItems uint64) map[common.Hash]common.Hash { + // #nosec G115 size := int(prand.GetUint64() % maxItems) ret := make(map[common.Hash]common.Hash) for i := 0; i < size; i++ { @@ -125,6 +126,7 @@ func checkAddressTable(arbState *ArbosState, addrTable []common.Address, t *test Fail(t) } for i, addr := range addrTable { + // #nosec G115 res, exists, err := atab.LookupIndex(uint64(i)) Require(t, err) if !exists { diff --git a/arbos/arbosState/initialize.go b/arbos/arbosState/initialize.go index 56fa579c15..8fd417c2b2 100644 --- a/arbos/arbosState/initialize.go +++ b/arbos/arbosState/initialize.go @@ -9,16 +9,19 @@ import ( "regexp" "sort" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" - "github.com/holiman/uint256" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/l2pricing" @@ -51,7 +54,7 @@ func MakeGenesisBlock(parentHash common.Hash, blockNumber uint64, timestamp uint } genesisHeaderInfo.UpdateHeaderWithInfo(head) - return types.NewBlock(head, nil, nil, nil, trie.NewStackTrie(nil)) + return types.NewBlock(head, nil, nil, trie.NewStackTrie(nil)) } func InitializeArbosInDatabase(db ethdb.Database, cacheConfig *core.CacheConfig, initData statetransfer.InitDataReader, chainConfig *params.ChainConfig, initMessage *arbostypes.ParsedInitMessage, timestamp uint64, accountsPerSync uint) (root common.Hash, err error) { @@ -96,6 +99,16 @@ func InitializeArbosInDatabase(db ethdb.Database, cacheConfig *core.CacheConfig, log.Crit("failed to open the ArbOS state", "error", err) } + chainOwner, err := initData.GetChainOwner() + if err != nil { + return common.Hash{}, err + } + if chainOwner != (common.Address{}) { + err = arbosState.ChainOwners().Add(chainOwner) + if err != nil { + return common.Hash{}, err + } + } addrTable := arbosState.AddressTable() addrTableSize, err := addrTable.Size() if err != nil { @@ -108,7 +121,7 @@ func InitializeArbosInDatabase(db ethdb.Database, cacheConfig *core.CacheConfig, if err != nil { return common.Hash{}, err } - for i := 0; addressReader.More(); i++ { + for i := uint64(0); addressReader.More(); i++ { addr, err := addressReader.GetNext() if err != nil { return common.Hash{}, err @@ -117,7 +130,7 @@ func InitializeArbosInDatabase(db ethdb.Database, cacheConfig *core.CacheConfig, if err != nil { return common.Hash{}, err } - if uint64(i) != slot { + if i != slot { return common.Hash{}, errors.New("address table slot mismatch") } } @@ -159,7 +172,7 @@ func InitializeArbosInDatabase(db ethdb.Database, cacheConfig *core.CacheConfig, if err != nil { return common.Hash{}, err } - statedb.SetBalance(account.Addr, uint256.MustFromBig(account.EthBalance)) + statedb.SetBalance(account.Addr, uint256.MustFromBig(account.EthBalance), tracing.BalanceChangeUnspecified) statedb.SetNonce(account.Addr, account.Nonce) if account.ContractInfo != nil { statedb.SetCode(account.Addr, account.ContractInfo.Code) @@ -190,7 +203,7 @@ func initializeRetryables(statedb *state.StateDB, rs *retryables.RetryableState, return err } if r.Timeout <= currentTimestamp { - statedb.AddBalance(r.Beneficiary, uint256.MustFromBig(r.Callvalue)) + statedb.AddBalance(r.Beneficiary, uint256.MustFromBig(r.Callvalue), tracing.BalanceChangeUnspecified) continue } retryablesList = append(retryablesList, r) @@ -209,7 +222,7 @@ func initializeRetryables(statedb *state.StateDB, rs *retryables.RetryableState, addr := r.To to = &addr } - statedb.AddBalance(retryables.RetryableEscrowAddress(r.Id), uint256.MustFromBig(r.Callvalue)) + statedb.AddBalance(retryables.RetryableEscrowAddress(r.Id), uint256.MustFromBig(r.Callvalue), tracing.BalanceChangeUnspecified) _, err := rs.CreateRetryable(r.Id, r.Timeout, r.From, to, r.Callvalue, r.Beneficiary, r.Calldata) if err != nil { return err diff --git a/arbos/arbostypes/incomingmessage.go b/arbos/arbostypes/incomingmessage.go index 04ce8ebe2e..b9a366cbd5 100644 --- a/arbos/arbostypes/incomingmessage.go +++ b/arbos/arbostypes/incomingmessage.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -182,6 +183,17 @@ func (msg *L1IncomingMessage) FillInBatchGasCost(batchFetcher FallibleBatchFetch return nil } +func (msg *L1IncomingMessage) PastBatchesRequired() ([]uint64, error) { + if msg.Header.Kind != L1MessageType_BatchPostingReport { + return nil, nil + } + _, _, _, batchNum, _, _, err := ParseBatchPostingReportMessageFields(bytes.NewReader(msg.L2msg)) + if err != nil { + return nil, fmt.Errorf("failed to parse batch posting report: %w", err) + } + return []uint64{batchNum}, nil +} + func ParseIncomingL1Message(rd io.Reader, batchFetcher FallibleBatchFetcher) (*L1IncomingMessage, error) { var kindBuf [1]byte _, err := rd.Read(kindBuf[:]) @@ -254,7 +266,7 @@ type ParsedInitMessage struct { var DefaultInitialL1BaseFee = big.NewInt(50 * params.GWei) var TestInitMessage = &ParsedInitMessage{ - ChainId: params.ArbitrumDevTestChainConfig().ChainID, + ChainId: chaininfo.ArbitrumDevTestChainConfig().ChainID, InitialL1BaseFee: DefaultInitialL1BaseFee, } diff --git a/arbos/arbostypes/messagewithmeta.go b/arbos/arbostypes/messagewithmeta.go index 6701f352de..d42db9a643 100644 --- a/arbos/arbostypes/messagewithmeta.go +++ b/arbos/arbostypes/messagewithmeta.go @@ -3,11 +3,13 @@ package arbostypes import ( "context" "encoding/binary" + "errors" "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" + "github.com/offchainlabs/nitro/arbutil" ) @@ -36,12 +38,18 @@ var TestMessageWithMetadataAndRequestId = MessageWithMetadata{ } // IsTxTimeboosted given a tx's index in the block returns whether the tx was timeboosted or not -func (b BlockMetadata) IsTxTimeboosted(txIndex int) bool { +func (b BlockMetadata) IsTxTimeboosted(txIndex int) (bool, error) { + if len(b) == 0 { + return false, errors.New("blockMetadata is not set") + } + if txIndex < 0 { + return false, fmt.Errorf("invalid transaction index- %d, should be positive", txIndex) + } maxTxCount := (len(b) - 1) * 8 if txIndex >= maxTxCount { - return false + return false, nil } - return b[1+(txIndex/8)]&(1<<(txIndex%8)) != 0 + return b[1+(txIndex/8)]&(1<<(txIndex%8)) != 0, nil } func (m *MessageWithMetadata) Hash(sequenceNumber arbutil.MessageIndex, chainId uint64) (common.Hash, error) { diff --git a/arbos/block_processor.go b/arbos/block_processor.go index b180405c43..77475856ac 100644 --- a/arbos/block_processor.go +++ b/arbos/block_processor.go @@ -10,12 +10,6 @@ import ( "math" "math/big" - "github.com/offchainlabs/nitro/arbos/arbosState" - "github.com/offchainlabs/nitro/arbos/arbostypes" - "github.com/offchainlabs/nitro/arbos/l2pricing" - "github.com/offchainlabs/nitro/arbos/util" - "github.com/offchainlabs/nitro/util/arbmath" - "github.com/ethereum/go-ethereum/arbitrum_types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -25,6 +19,12 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/arbmath" ) // set by the precompile module, to avoid a package dependence cycle @@ -144,6 +144,7 @@ func ProduceBlock( chainContext core.ChainContext, chainConfig *params.ChainConfig, isMsgForPrefetch bool, + runMode core.MessageRunMode, ) (*types.Block, types.Receipts, error) { txes, err := ParseL2Transactions(message, chainConfig.ChainID) if err != nil { @@ -153,7 +154,7 @@ func ProduceBlock( hooks := NoopSequencingHooks() return ProduceBlockAdvanced( - message.Header, txes, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, isMsgForPrefetch, + message.Header, txes, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, isMsgForPrefetch, runMode, ) } @@ -168,6 +169,7 @@ func ProduceBlockAdvanced( chainConfig *params.ChainConfig, sequencingHooks *SequencingHooks, isMsgForPrefetch bool, + runMode core.MessageRunMode, ) (*types.Block, types.Receipts, error) { state, err := arbosState.OpenSystemArbosState(statedb, nil, true) @@ -318,6 +320,7 @@ func ProduceBlockAdvanced( tx, &header.GasUsed, vm.Config{}, + runMode, func(result *core.ExecutionResult) error { return hooks.PostTxFilter(header, state, tx, sender, dataGas, result) }, @@ -337,18 +340,6 @@ func ProduceBlockAdvanced( return receipt, result, nil })() - if tx.Type() == types.ArbitrumInternalTxType { - // ArbOS might have upgraded to a new version, so we need to refresh our state - state, err = arbosState.OpenSystemArbosState(statedb, nil, true) - if err != nil { - return nil, nil, err - } - // Update the ArbOS version in the header (if it changed) - extraInfo := types.DeserializeHeaderExtraInformation(header) - extraInfo.ArbOSFormatVersion = state.ArbOSVersion() - extraInfo.UpdateHeaderWithInfo(header) - } - // append the err, even if it is nil hooks.TxErrors = append(hooks.TxErrors, err) @@ -370,6 +361,18 @@ func ProduceBlockAdvanced( continue } + if tx.Type() == types.ArbitrumInternalTxType { + // ArbOS might have upgraded to a new version, so we need to refresh our state + state, err = arbosState.OpenSystemArbosState(statedb, nil, true) + if err != nil { + return nil, nil, err + } + // Update the ArbOS version in the header (if it changed) + extraInfo := types.DeserializeHeaderExtraInformation(header) + extraInfo.ArbOSFormatVersion = state.ArbOSVersion() + extraInfo.UpdateHeaderWithInfo(header) + } + if tx.Type() == types.ArbitrumInternalTxType && result.Err != nil { return nil, nil, fmt.Errorf("failed to apply internal transaction: %w", result.Err) } @@ -457,7 +460,7 @@ func ProduceBlockAdvanced( FinalizeBlock(header, complete, statedb, chainConfig) // Touch up the block hashes in receipts - tmpBlock := types.NewBlock(header, complete, nil, receipts, trie.NewStackTrie(nil)) + tmpBlock := types.NewBlock(header, &types.Body{Transactions: complete}, receipts, trie.NewStackTrie(nil)) blockHash := tmpBlock.Hash() for _, receipt := range receipts { @@ -467,7 +470,7 @@ func ProduceBlockAdvanced( } } - block := types.NewBlock(header, complete, nil, receipts, trie.NewStackTrie(nil)) + block := types.NewBlock(header, &types.Body{Transactions: complete}, receipts, trie.NewStackTrie(nil)) if len(block.Transactions()) != len(receipts) { return nil, nil, fmt.Errorf("block has %d txes but %d receipts", len(block.Transactions()), len(receipts)) diff --git a/arbos/blockhash/blockhash.go b/arbos/blockhash/blockhash.go index 34c907207c..df5078fd2c 100644 --- a/arbos/blockhash/blockhash.go +++ b/arbos/blockhash/blockhash.go @@ -8,6 +8,8 @@ import ( "errors" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/storage" ) @@ -55,7 +57,7 @@ func (bh *Blockhashes) RecordNewL1Block(number uint64, blockHash common.Hash, ar // fill in hashes for any "skipped over" blocks nextNumber++ var nextNumBuf [8]byte - if arbosVersion >= 8 { + if arbosVersion >= params.ArbosVersion_8 { binary.LittleEndian.PutUint64(nextNumBuf[:], nextNumber) } diff --git a/arbos/blockhash/blockhash_test.go b/arbos/blockhash/blockhash_test.go index bf3ee5ee11..8dec2181a3 100644 --- a/arbos/blockhash/blockhash_test.go +++ b/arbos/blockhash/blockhash_test.go @@ -8,13 +8,15 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/util/testhelpers" ) func TestBlockhash(t *testing.T) { - arbosVersion := uint64(8) + arbosVersion := params.ArbosVersion_8 sto := storage.NewMemoryBacked(burn.NewSystemBurner(nil, false)) InitializeBlockhashes(sto) diff --git a/arbos/burn/burn.go b/arbos/burn/burn.go index 7d30ad12ec..c94f6bec2f 100644 --- a/arbos/burn/burn.go +++ b/arbos/burn/burn.go @@ -7,6 +7,7 @@ import ( "fmt" glog "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos/util" ) diff --git a/arbos/engine.go b/arbos/engine.go index 0014e8ab96..a812e5486b 100644 --- a/arbos/engine.go +++ b/arbos/engine.go @@ -48,16 +48,15 @@ func (e Engine) Prepare(chain consensus.ChainHeaderReader, header *types.Header) return nil } -func (e Engine) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) { - FinalizeBlock(header, txs, state, chain.Config()) +func (e Engine) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) { + FinalizeBlock(header, body.Transactions, state, chain.Config()) } -func (e Engine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, - uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) { +func (e Engine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { - e.Finalize(chain, header, state, txs, uncles, withdrawals) + e.Finalize(chain, header, state, body) - block := types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)) + block := types.NewBlock(header, &types.Body{Transactions: body.Transactions}, receipts, trie.NewStackTrie(nil)) return block, nil } diff --git a/arbos/extra_transaction_checks.go b/arbos/extra_transaction_checks.go index 0f970c9925..480058cb0f 100644 --- a/arbos/extra_transaction_checks.go +++ b/arbos/extra_transaction_checks.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" ) diff --git a/arbos/incomingmessage_test.go b/arbos/incomingmessage_test.go index 2933f6a719..22aba05bce 100644 --- a/arbos/incomingmessage_test.go +++ b/arbos/incomingmessage_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/arbostypes" ) diff --git a/arbos/internal_tx.go b/arbos/internal_tx.go index 9832ac8005..0ecdfe74cf 100644 --- a/arbos/internal_tx.go +++ b/arbos/internal_tx.go @@ -8,15 +8,15 @@ import ( "fmt" "math/big" - "github.com/offchainlabs/nitro/util/arbmath" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/arbmath" ) func InternalTxStartBlock( @@ -57,11 +57,11 @@ func ApplyInternalTxUpdate(tx *types.ArbitrumInternalTx, state *arbosState.Arbos l1BlockNumber := util.SafeMapGet[uint64](inputs, "l1BlockNumber") timePassed := util.SafeMapGet[uint64](inputs, "timePassed") - if state.ArbOSVersion() < 3 { + if state.ArbOSVersion() < params.ArbosVersion_3 { // (incorrectly) use the L2 block number instead timePassed = util.SafeMapGet[uint64](inputs, "l2BlockNumber") } - if state.ArbOSVersion() < 8 { + if state.ArbOSVersion() < params.ArbosVersion_8 { // in old versions we incorrectly used an L1 block number one too high l1BlockNumber++ } diff --git a/arbos/l1pricing/batchPoster.go b/arbos/l1pricing/batchPoster.go index a3428c441c..5975e95d0f 100644 --- a/arbos/l1pricing/batchPoster.go +++ b/arbos/l1pricing/batchPoster.go @@ -9,6 +9,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/addressSet" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/util/arbmath" diff --git a/arbos/l1pricing/batchPoster_test.go b/arbos/l1pricing/batchPoster_test.go index 4e9b8565c0..3263ffca81 100644 --- a/arbos/l1pricing/batchPoster_test.go +++ b/arbos/l1pricing/batchPoster_test.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" ) diff --git a/arbos/l1pricing/l1PricingOldVersions.go b/arbos/l1pricing/l1PricingOldVersions.go index 821d743e7d..e4cbf5e1b3 100644 --- a/arbos/l1pricing/l1PricingOldVersions.go +++ b/arbos/l1pricing/l1PricingOldVersions.go @@ -9,6 +9,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/util" am "github.com/offchainlabs/nitro/util/arbmath" ) @@ -23,7 +25,7 @@ func (ps *L1PricingState) _preversion10_UpdateForBatchPosterSpending( l1Basefee *big.Int, scenario util.TracingScenario, ) error { - if arbosVersion < 2 { + if arbosVersion < params.ArbosVersion_2 { return ps._preVersion2_UpdateForBatchPosterSpending(statedb, evm, updateTime, currentTime, batchPoster, weiSpent, scenario) } @@ -68,7 +70,7 @@ func (ps *L1PricingState) _preversion10_UpdateForBatchPosterSpending( } // impose cap on amortized cost, if there is one - if arbosVersion >= 3 { + if arbosVersion >= params.ArbosVersion_3 { amortizedCostCapBips, err := ps.AmortizedCostCapBips() if err != nil { return err diff --git a/arbos/l1pricing/l1pricing.go b/arbos/l1pricing/l1pricing.go index 9e00eeb581..195df3708c 100644 --- a/arbos/l1pricing/l1pricing.go +++ b/arbos/l1pricing/l1pricing.go @@ -8,22 +8,20 @@ import ( "errors" "fmt" "math/big" - "sync/atomic" - - "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbcompress" - "github.com/offchainlabs/nitro/util/arbmath" - am "github.com/offchainlabs/nitro/util/arbmath" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/util/arbmath" + am "github.com/offchainlabs/nitro/util/arbmath" ) type L1PricingState struct { @@ -217,7 +215,7 @@ func (ps *L1PricingState) LastSurplus() (*big.Int, error) { } func (ps *L1PricingState) SetLastSurplus(val *big.Int, arbosVersion uint64) error { - if arbosVersion < 7 { + if arbosVersion < params.ArbosVersion_7 { return ps.lastSurplus.Set_preVersion7(val) } return ps.lastSurplus.SetSaturatingWithWarning(val, "L1 pricer last surplus") @@ -310,7 +308,7 @@ func (ps *L1PricingState) UpdateForBatchPosterSpending( l1Basefee *big.Int, scenario util.TracingScenario, ) error { - if arbosVersion < 10 { + if arbosVersion < params.ArbosVersion_10 { return ps._preversion10_UpdateForBatchPosterSpending(statedb, evm, arbosVersion, updateTime, currentTime, batchPoster, weiSpent, l1Basefee, scenario) } @@ -360,7 +358,7 @@ func (ps *L1PricingState) UpdateForBatchPosterSpending( } // impose cap on amortized cost, if there is one - if arbosVersion >= 3 { + if arbosVersion >= params.ArbosVersion_3 { amortizedCostCapBips, err := ps.AmortizedCostCapBips() if err != nil { return err @@ -509,7 +507,7 @@ func (ps *L1PricingState) getPosterUnitsWithoutCache(tx *types.Transaction, post return 0 } - l1Bytes, err := byteCountAfterBrotliLevel(txBytes, int(brotliCompressionLevel)) + l1Bytes, err := byteCountAfterBrotliLevel(txBytes, brotliCompressionLevel) if err != nil { panic(fmt.Sprintf("failed to compress tx: %v", err)) } @@ -521,10 +519,13 @@ func (ps *L1PricingState) GetPosterInfo(tx *types.Transaction, poster common.Add if poster != BatchPosterAddress { return common.Big0, 0 } - units := atomic.LoadUint64(&tx.CalldataUnits) - if units == 0 { + var units uint64 + if cachedUnits := tx.GetCachedCalldataUnits(brotliCompressionLevel); cachedUnits != nil { + units = *cachedUnits + } else { + // The cache is empty or invalid, so we need to compute the calldata units units = ps.getPosterUnitsWithoutCache(tx, poster, brotliCompressionLevel) - atomic.StoreUint64(&tx.CalldataUnits, units) + tx.SetCachedCalldataUnits(brotliCompressionLevel, units) } // Approximate the l1 fee charged for posting this tx's calldata @@ -540,7 +541,7 @@ var randomNonce = binary.BigEndian.Uint64(crypto.Keccak256([]byte("Nonce"))[:8]) var randomGasTipCap = new(big.Int).SetBytes(crypto.Keccak256([]byte("GasTipCap"))[:4]) var randomGasFeeCap = new(big.Int).SetBytes(crypto.Keccak256([]byte("GasFeeCap"))[:4]) var RandomGas = uint64(binary.BigEndian.Uint32(crypto.Keccak256([]byte("Gas"))[:4])) -var randV = arbmath.BigMulByUint(params.ArbitrumOneChainConfig().ChainID, 3) +var randV = arbmath.BigMulByUint(chaininfo.ArbitrumOneChainConfig().ChainID, 3) var randR = crypto.Keccak256Hash([]byte("R")).Big() var randS = crypto.Keccak256Hash([]byte("S")).Big() @@ -594,7 +595,7 @@ func (ps *L1PricingState) PosterDataCost(message *core.Message, poster common.Ad return am.BigMulByUint(pricePerUnit, units), units } -func byteCountAfterBrotliLevel(input []byte, level int) (uint64, error) { +func byteCountAfterBrotliLevel(input []byte, level uint64) (uint64, error) { compressed, err := arbcompress.CompressLevel(input, level) if err != nil { return 0, err diff --git a/arbos/l1pricing/l1pricing_test.go b/arbos/l1pricing/l1pricing_test.go index b301c94257..b842c26db7 100644 --- a/arbos/l1pricing/l1pricing_test.go +++ b/arbos/l1pricing/l1pricing_test.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" ) diff --git a/arbos/l1pricing_test.go b/arbos/l1pricing_test.go index 6e2b1b7eec..6ab0135be9 100644 --- a/arbos/l1pricing_test.go +++ b/arbos/l1pricing_test.go @@ -7,18 +7,21 @@ import ( "math/big" "testing" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" - "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/arbmath" - - "github.com/ethereum/go-ethereum/params" - "github.com/offchainlabs/nitro/arbos/burn" ) type l1PricingTest struct { @@ -100,7 +103,7 @@ func expectedResultsForL1Test(input *l1PricingTest) *l1TestExpectedResults { availableFunds = availableFundsCap } } - fundsWantedForRewards := big.NewInt(int64(input.unitReward * input.unitsPerSecond)) + fundsWantedForRewards := new(big.Int).SetUint64(input.unitReward * input.unitsPerSecond) unitsAllocated := arbmath.UintToBig(input.unitsPerSecond) if arbmath.BigLessThan(availableFunds, fundsWantedForRewards) { ret.rewardRecipientBalance = availableFunds @@ -111,7 +114,7 @@ func expectedResultsForL1Test(input *l1PricingTest) *l1TestExpectedResults { uncappedAvailableFunds = arbmath.BigSub(uncappedAvailableFunds, ret.rewardRecipientBalance) ret.unitsRemaining = (3 * input.unitsPerSecond) - unitsAllocated.Uint64() - maxCollectable := big.NewInt(int64(input.fundsSpent)) + maxCollectable := new(big.Int).SetUint64(input.fundsSpent) if arbmath.BigLessThan(availableFunds, maxCollectable) { maxCollectable = availableFunds } @@ -170,9 +173,9 @@ func _testL1PricingFundsDue(t *testing.T, testParams *l1PricingTest, expectedRes Require(t, err) // create some fake collection - balanceAdded := big.NewInt(int64(testParams.fundsCollectedPerSecond * 3)) + balanceAdded := new(big.Int).SetUint64(testParams.fundsCollectedPerSecond * 3) unitsAdded := testParams.unitsPerSecond * 3 - evm.StateDB.AddBalance(l1pricing.L1PricerFundsPoolAddress, uint256.MustFromBig(balanceAdded)) + evm.StateDB.AddBalance(l1pricing.L1PricerFundsPoolAddress, uint256.MustFromBig(balanceAdded), tracing.BalanceChangeUnspecified) err = l1p.SetL1FeesAvailable(balanceAdded) Require(t, err) err = l1p.SetUnitsSinceUpdate(unitsAdded) @@ -279,7 +282,9 @@ func _testL1PriceEquilibration(t *testing.T, initialL1BasefeeEstimate *big.Int, evm.StateDB, evm, 3, + // #nosec G115 uint64(10*(i+1)), + // #nosec G115 uint64(10*(i+1)+5), bpAddr, arbmath.BigMulByUint(equilibriumL1BasefeeEstimate, unitsToAdd), @@ -313,7 +318,7 @@ func _withinOnePercent(v1, v2 *big.Int) bool { } func newMockEVMForTesting() *vm.EVM { - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() _, statedb := arbosState.NewArbosMemoryBackedArbOSState() context := vm.BlockContext{ BlockNumber: big.NewInt(0), diff --git a/arbos/l2pricing/l2pricing_test.go b/arbos/l2pricing/l2pricing_test.go index 57759d7f82..aa1e785f70 100644 --- a/arbos/l2pricing/l2pricing_test.go +++ b/arbos/l2pricing/l2pricing_test.go @@ -40,6 +40,7 @@ func TestPricingModelExp(t *testing.T) { // show that running at the speed limit with a full pool is a steady-state colors.PrintBlue("full pool & speed limit") for seconds := 0; seconds < 4; seconds++ { + // #nosec G115 fakeBlockUpdate(t, pricing, int64(seconds)*int64(limit), uint64(seconds)) if getPrice(t, pricing) != minPrice { Fail(t, "price changed when it shouldn't have") @@ -50,6 +51,7 @@ func TestPricingModelExp(t *testing.T) { // note that for large enough spans of time the price will rise a miniscule amount due to the pool's avg colors.PrintBlue("pool target & speed limit") for seconds := 0; seconds < 4; seconds++ { + // #nosec G115 fakeBlockUpdate(t, pricing, int64(seconds)*int64(limit), uint64(seconds)) if getPrice(t, pricing) != minPrice { Fail(t, "price changed when it shouldn't have") @@ -59,6 +61,7 @@ func TestPricingModelExp(t *testing.T) { // show that running over the speed limit escalates the price before the pool drains colors.PrintBlue("exceeding the speed limit") for { + // #nosec G115 fakeBlockUpdate(t, pricing, 8*int64(limit), 1) newPrice := getPrice(t, pricing) if newPrice < price { diff --git a/arbos/l2pricing/model.go b/arbos/l2pricing/model.go index 131af2c2cf..367e8b6e1a 100644 --- a/arbos/l2pricing/model.go +++ b/arbos/l2pricing/model.go @@ -7,6 +7,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/util/arbmath" ) @@ -30,22 +31,26 @@ func (ps *L2PricingState) AddToGasPool(gas int64) error { return err } // pay off some of the backlog with the added gas, stopping at 0 - backlog = arbmath.SaturatingUCast[uint64](arbmath.SaturatingSub(int64(backlog), gas)) + if gas > 0 { + backlog = arbmath.SaturatingUSub(backlog, uint64(gas)) + } else { + backlog = arbmath.SaturatingUAdd(backlog, uint64(-gas)) + } return ps.SetGasBacklog(backlog) } // UpdatePricingModel updates the pricing model with info from the last block func (ps *L2PricingState) UpdatePricingModel(l2BaseFee *big.Int, timePassed uint64, debug bool) { speedLimit, _ := ps.SpeedLimitPerSecond() - _ = ps.AddToGasPool(int64(timePassed * speedLimit)) + _ = ps.AddToGasPool(arbmath.SaturatingCast[int64](arbmath.SaturatingUMul(timePassed, speedLimit))) inertia, _ := ps.PricingInertia() tolerance, _ := ps.BacklogTolerance() backlog, _ := ps.GasBacklog() minBaseFee, _ := ps.MinBaseFeeWei() baseFee := minBaseFee if backlog > tolerance*speedLimit { - excess := int64(backlog - tolerance*speedLimit) - exponentBips := arbmath.NaturalToBips(excess) / arbmath.Bips(inertia*speedLimit) + excess := arbmath.SaturatingCast[int64](backlog - tolerance*speedLimit) + exponentBips := arbmath.NaturalToBips(excess) / arbmath.SaturatingCast[arbmath.Bips](inertia*speedLimit) baseFee = arbmath.BigMulByBips(minBaseFee, arbmath.ApproxExpBasisPoints(exponentBips, 4)) } _ = ps.SetBaseFeeWei(baseFee) diff --git a/arbos/merkleAccumulator/merkleAccumulator.go b/arbos/merkleAccumulator/merkleAccumulator.go index 2e060c5840..0d51602c02 100644 --- a/arbos/merkleAccumulator/merkleAccumulator.go +++ b/arbos/merkleAccumulator/merkleAccumulator.go @@ -6,6 +6,7 @@ package merkleAccumulator import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -97,6 +98,7 @@ func (acc *MerkleAccumulator) GetPartials() ([]*common.Hash, error) { } partials := make([]*common.Hash, CalcNumPartials(size)) for i := range partials { + // #nosec G115 p, err := acc.getPartial(uint64(i)) if err != nil { return nil, err diff --git a/arbos/parse_l2.go b/arbos/parse_l2.go index 06722e4063..cd926f47bf 100644 --- a/arbos/parse_l2.go +++ b/arbos/parse_l2.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" diff --git a/arbos/programs/api.go b/arbos/programs/api.go index 504289322f..d8f12ffbd3 100644 --- a/arbos/programs/api.go +++ b/arbos/programs/api.go @@ -4,12 +4,14 @@ package programs import ( + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" am "github.com/offchainlabs/nitro/util/arbmath" @@ -254,7 +256,9 @@ func newApiClosures( return memoryModel.GasCost(pages, open, ever) } captureHostio := func(name string, args, outs []byte, startInk, endInk uint64) { - tracingInfo.Tracer.CaptureStylusHostio(name, args, outs, startInk, endInk) + if tracingInfo.Tracer != nil && tracingInfo.Tracer.CaptureStylusHostio != nil { + tracingInfo.Tracer.CaptureStylusHostio(name, args, outs, startInk, endInk) + } tracingInfo.CaptureEVMTraceForHostio(name, args, outs, startInk, endInk) } @@ -400,9 +404,9 @@ func newApiClosures( } startInk := takeU64() endInk := takeU64() - nameLen := takeU16() - argsLen := takeU16() - outsLen := takeU16() + nameLen := takeU32() + argsLen := takeU32() + outsLen := takeU32() name := string(takeFixed(int(nameLen))) args := takeFixed(int(argsLen)) outs := takeFixed(int(outsLen)) diff --git a/arbos/programs/cgo_test.go b/arbos/programs/cgo_test.go index c0e146d98d..e16c362ef8 100644 --- a/arbos/programs/cgo_test.go +++ b/arbos/programs/cgo_test.go @@ -40,5 +40,13 @@ func TestCompileArch(t *testing.T) { if err != nil { t.Fatal(err) } + err = resetNativeTarget() + if err != nil { + t.Fatal(err) + } + err = testCompileLoad() + if err != nil { + t.Fatal(err) + } } } diff --git a/arbos/programs/data_pricer.go b/arbos/programs/data_pricer.go index ed7c98556d..d82aa81f04 100644 --- a/arbos/programs/data_pricer.go +++ b/arbos/programs/data_pricer.go @@ -83,8 +83,8 @@ func (p *DataPricer) UpdateModel(tempBytes uint32, time uint64) (*big.Int, error } exponent := arbmath.OneInBips * arbmath.Bips(demand) / arbmath.Bips(inertia) - multiplier := arbmath.ApproxExpBasisPoints(exponent, 12).Uint64() - costPerByte := arbmath.SaturatingUMul(uint64(minPrice), multiplier) / 10000 + multiplier := arbmath.ApproxExpBasisPoints(exponent, 12) + costPerByte := arbmath.UintSaturatingMulByBips(uint64(minPrice), multiplier) costInWei := arbmath.SaturatingUMul(costPerByte, uint64(tempBytes)) return arbmath.UintToBig(costInWei), nil } diff --git a/arbos/programs/native.go b/arbos/programs/native.go index fd3dec25a0..a996d50d8a 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -7,7 +7,7 @@ package programs /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #cgo LDFLAGS: ${SRCDIR}/../../target/lib/libstylus.a -ldl -lm #include "arbitrator.h" @@ -18,6 +18,7 @@ typedef uint64_t u64; typedef size_t usize; */ import "C" + import ( "errors" "fmt" @@ -27,7 +28,10 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" @@ -44,17 +48,31 @@ type bytes32 = C.Bytes32 type rustBytes = C.RustBytes type rustSlice = C.RustSlice +var ( + stylusLRUCacheSizeBytesGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/lru/size_bytes", nil) + stylusLRUCacheCountGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/lru/count", nil) + stylusLRUCacheHitsCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/hits", nil) + stylusLRUCacheMissesCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/misses", nil) + stylusLRUCacheDoesNotFitCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/lru/does_not_fit", nil) + + stylusLongTermCacheSizeBytesGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/long_term/size_bytes", nil) + stylusLongTermCacheCountGauge = metrics.NewRegisteredGauge("arb/arbos/stylus/cache/long_term/count", nil) + stylusLongTermCacheHitsCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/long_term/hits", nil) + stylusLongTermCacheMissesCounter = metrics.NewRegisteredCounter("arb/arbos/stylus/cache/long_term/misses", nil) +) + func activateProgram( db vm.StateDB, program common.Address, codehash common.Hash, wasm []byte, page_limit uint16, - version uint16, + stylusVersion uint16, + arbosVersionForGas uint64, debug bool, burner burn.Burner, ) (*activationInfo, error) { - info, asmMap, err := activateProgramInternal(db, program, codehash, wasm, page_limit, version, debug, burner.GasLeft()) + info, asmMap, err := activateProgramInternal(db, program, codehash, wasm, page_limit, stylusVersion, arbosVersionForGas, debug, burner.GasLeft()) if err != nil { return nil, err } @@ -68,10 +86,11 @@ func activateProgramInternal( codehash common.Hash, wasm []byte, page_limit uint16, - version uint16, + stylusVersion uint16, + arbosVersionForGas uint64, debug bool, gasLeft *uint64, -) (*activationInfo, map[rawdb.Target][]byte, error) { +) (*activationInfo, map[ethdb.WasmTarget][]byte, error) { output := &rustBytes{} moduleHash := &bytes32{} stylusData := &C.StylusData{} @@ -80,7 +99,8 @@ func activateProgramInternal( status_mod := userStatus(C.stylus_activate( goSlice(wasm), u16(page_limit), - u16(version), + u16(stylusVersion), + u64(arbosVersionForGas), cbool(debug), output, &codeHash, @@ -89,7 +109,7 @@ func activateProgramInternal( (*u64)(gasLeft), )) - module, msg, err := status_mod.toResult(output.intoBytes(), debug) + module, msg, err := status_mod.toResult(rustBytesIntoBytes(output), debug) if err != nil { if debug { log.Warn("activation failed", "err", err, "msg", msg, "program", addressForLogging) @@ -99,24 +119,57 @@ func activateProgramInternal( } return nil, nil, err } - target := rawdb.LocalTarget() - status_asm := C.stylus_compile( - goSlice(wasm), - u16(version), - cbool(debug), - goSlice([]byte(target)), - output, - ) - asm := output.intoBytes() - if status_asm != 0 { - return nil, nil, fmt.Errorf("%w: %s", ErrProgramActivation, string(asm)) + hash := bytes32ToHash(moduleHash) + targets := db.Database().WasmTargets() + type result struct { + target ethdb.WasmTarget + asm []byte + err error } - asmMap := map[rawdb.Target][]byte{ - rawdb.TargetWavm: module, - target: asm, + results := make(chan result, len(targets)) + for _, target := range targets { + target := target + if target == rawdb.TargetWavm { + results <- result{target, module, nil} + } else { + go func() { + output := &rustBytes{} + status_asm := C.stylus_compile( + goSlice(wasm), + u16(stylusVersion), + cbool(debug), + goSlice([]byte(target)), + output, + ) + asm := rustBytesIntoBytes(output) + if status_asm != 0 { + results <- result{target, nil, fmt.Errorf("%w: %s", ErrProgramActivation, string(asm))} + return + } + results <- result{target, asm, nil} + }() + } + } + asmMap := make(map[ethdb.WasmTarget][]byte, len(targets)) + for range targets { + res := <-results + if res.err != nil { + err = errors.Join(res.err, err) + } else { + asmMap[res.target] = res.asm + } + } + if err != nil { + log.Error( + "Compilation failed for one or more targets despite activation succeeding", + "address", addressForLogging, + "codeHash", codeHash, + "moduleHash", hash, + "targets", targets, + "err", err, + ) + panic(fmt.Sprintf("Compilation of %v failed for one or more targets despite activation succeeding: %v", addressForLogging, err)) } - - hash := moduleHash.toHash() info := &activationInfo{ moduleHash: hash, @@ -142,9 +195,12 @@ func getLocalAsm(statedb vm.StateDB, moduleHash common.Hash, addressForLogging c return nil, fmt.Errorf("failed to reactivate program address: %v err: %w", addressForLogging, err) } - unlimitedGas := uint64(0xffffffffffff) + // don't charge gas + zeroArbosVersion := uint64(0) + zeroGas := uint64(0) + // we know program is activated, so it must be in correct version and not use too much memory - info, asmMap, err := activateProgramInternal(statedb, addressForLogging, codeHash, wasm, pagelimit, program.version, debugMode, &unlimitedGas) + info, asmMap, err := activateProgramInternal(statedb, addressForLogging, codeHash, wasm, pagelimit, program.version, zeroArbosVersion, debugMode, &zeroGas) if err != nil { log.Error("failed to reactivate program", "address", addressForLogging, "expected moduleHash", moduleHash, "err", err) return nil, fmt.Errorf("failed to reactivate program address: %v err: %w", addressForLogging, err) @@ -171,7 +227,7 @@ func getLocalAsm(statedb vm.StateDB, moduleHash common.Hash, addressForLogging c } asm, exists := asmMap[localTarget] if !exists { - var availableTargets []rawdb.Target + var availableTargets []ethdb.WasmTarget for target := range asmMap { availableTargets = append(availableTargets, target) } @@ -202,12 +258,8 @@ func callProgram( panic("missing asm") } - if db, ok := db.(*state.StateDB); ok { - targets := []rawdb.Target{ - rawdb.TargetWavm, - rawdb.LocalTarget(), - } - db.RecordProgram(targets, moduleHash) + if stateDb, ok := db.(*state.StateDB); ok { + stateDb.RecordProgram(db.Database().WasmTargets(), moduleHash) } evmApi := newApi(interpreter, tracingInfo, scope, memoryModel) @@ -227,7 +279,7 @@ func callProgram( )) depth := interpreter.Depth() - data, msg, err := status.toResult(output.intoBytes(), debug) + data, msg, err := status.toResult(rustBytesIntoBytes(output), debug) if status == userFailure && debug { log.Warn("program failure", "err", err, "msg", msg, "program", address, "depth", depth) } @@ -240,7 +292,7 @@ func callProgram( //export handleReqImpl func handleReqImpl(apiId usize, req_type u32, data *rustSlice, costPtr *u64, out_response *C.GoSliceData, out_raw_data *C.GoSliceData) { api := getApi(apiId) - reqData := data.read() + reqData := readRustSlice(data) reqType := RequestType(req_type - EvmApiMethodReqOffset) response, raw_data, cost := api.handler(reqType, reqData) *costPtr = u64(cost) @@ -284,14 +336,80 @@ func init() { } } -func ResizeWasmLruCache(size uint32) { - C.stylus_cache_lru_resize(u32(size)) +func SetWasmLruCacheCapacity(capacityBytes uint64) { + C.stylus_set_cache_lru_capacity(u64(capacityBytes)) +} + +func UpdateWasmCacheMetrics() { + metrics := &C.CacheMetrics{} + C.stylus_get_cache_metrics(metrics) + + stylusLRUCacheSizeBytesGauge.Update(int64(metrics.lru.size_bytes)) + stylusLRUCacheCountGauge.Update(int64(metrics.lru.count)) + stylusLRUCacheHitsCounter.Inc(int64(metrics.lru.hits)) + stylusLRUCacheMissesCounter.Inc(int64(metrics.lru.misses)) + stylusLRUCacheDoesNotFitCounter.Inc(int64(metrics.lru.does_not_fit)) + + stylusLongTermCacheSizeBytesGauge.Update(int64(metrics.long_term.size_bytes)) + stylusLongTermCacheCountGauge.Update(int64(metrics.long_term.count)) + stylusLongTermCacheHitsCounter.Inc(int64(metrics.long_term.hits)) + stylusLongTermCacheMissesCounter.Inc(int64(metrics.long_term.misses)) +} + +// Used for testing +type WasmLruCacheMetrics struct { + SizeBytes uint64 + Count uint32 +} + +// Used for testing +type WasmLongTermCacheMetrics struct { + SizeBytes uint64 + Count uint32 +} + +// Used for testing +type WasmCacheMetrics struct { + Lru WasmLruCacheMetrics + LongTerm WasmLongTermCacheMetrics +} + +// Used for testing +func GetWasmCacheMetrics() *WasmCacheMetrics { + metrics := &C.CacheMetrics{} + C.stylus_get_cache_metrics(metrics) + + return &WasmCacheMetrics{ + Lru: WasmLruCacheMetrics{ + SizeBytes: uint64(metrics.lru.size_bytes), + Count: uint32(metrics.lru.count), + }, + LongTerm: WasmLongTermCacheMetrics{ + SizeBytes: uint64(metrics.long_term.size_bytes), + Count: uint32(metrics.long_term.count), + }, + } +} + +// Used for testing +func ClearWasmLruCache() { + C.stylus_clear_lru_cache() +} + +// Used for testing +func ClearWasmLongTermCache() { + C.stylus_clear_long_term_cache() +} + +// Used for testing +func GetEntrySizeEstimateBytes(module []byte, version uint16, debug bool) uint64 { + return uint64(C.stylus_get_entry_size_estimate_bytes(goSlice(module), u16(version), cbool(debug))) } const DefaultTargetDescriptionArm = "arm64-linux-unknown+neon" -const DefaultTargetDescriptionX86 = "x86_64-linux-unknown+sse4.2" +const DefaultTargetDescriptionX86 = "x86_64-linux-unknown+sse4.2+lzcnt+bmi" -func SetTarget(name rawdb.Target, description string, native bool) error { +func SetTarget(name ethdb.WasmTarget, description string, native bool) error { output := &rustBytes{} status := userStatus(C.stylus_target_set( goSlice([]byte(name)), @@ -300,14 +418,14 @@ func SetTarget(name rawdb.Target, description string, native bool) error { cbool(native), )) if status != userSuccess { - msg := arbutil.ToStringOrHex(output.intoBytes()) + msg := arbutil.ToStringOrHex(rustBytesIntoBytes(output)) log.Error("failed to set stylus compilation target", "status", status, "msg", msg) return fmt.Errorf("failed to set stylus compilation target, status %v: %v", status, msg) } return nil } -func (value bytes32) toHash() common.Hash { +func bytes32ToHash(value *bytes32) common.Hash { hash := common.Hash{} for index, b := range value.bytes { hash[index] = byte(b) @@ -331,22 +449,28 @@ func addressToBytes20(addr common.Address) bytes20 { return value } -func (slice *rustSlice) read() []byte { +func readRustSlice(slice *rustSlice) []byte { + if slice.len == 0 { + return nil + } return arbutil.PointerToSlice((*byte)(slice.ptr), int(slice.len)) } -func (vec *rustBytes) read() []byte { +func readRustBytes(vec *rustBytes) []byte { + if vec.len == 0 { + return nil + } return arbutil.PointerToSlice((*byte)(vec.ptr), int(vec.len)) } -func (vec *rustBytes) intoBytes() []byte { - slice := vec.read() - vec.drop() +func rustBytesIntoBytes(vec *rustBytes) []byte { + slice := readRustBytes(vec) + dropRustBytes(vec) return slice } -func (vec *rustBytes) drop() { - C.stylus_drop_vec(*vec) +func dropRustBytes(vec *rustBytes) { + C.free_rust_bytes(*vec) } func goSlice(slice []byte) C.GoSliceData { @@ -369,6 +493,7 @@ func (params *ProgParams) encode() C.StylusConfig { func (data *EvmData) encode() C.EvmData { return C.EvmData{ + arbos_version: u64(data.arbosVersion), block_basefee: hashToBytes32(data.blockBasefee), chainid: u64(data.chainId), block_coinbase: addressToBytes20(data.blockCoinbase), diff --git a/arbos/programs/native_api.go b/arbos/programs/native_api.go index 6fbb630ef3..ab15800ef9 100644 --- a/arbos/programs/native_api.go +++ b/arbos/programs/native_api.go @@ -7,7 +7,7 @@ package programs /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #cgo LDFLAGS: ${SRCDIR}/../../target/lib/libstylus.a -ldl -lm #include "arbitrator.h" @@ -22,6 +22,7 @@ void handleReqWrap(usize api, u32 req_type, RustSlice *data, u64 *out_cost, GoSl } */ import "C" + import ( "runtime" "sync" @@ -29,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" ) diff --git a/arbos/programs/params.go b/arbos/programs/params.go index a0b8acd95c..9b219737d9 100644 --- a/arbos/programs/params.go +++ b/arbos/programs/params.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" am "github.com/offchainlabs/nitro/util/arbmath" diff --git a/arbos/programs/programs.go b/arbos/programs/programs.go index 12102bac84..06ba6ead8c 100644 --- a/arbos/programs/programs.go +++ b/arbos/programs/programs.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" gethParams "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbos/addressSet" "github.com/offchainlabs/nitro/arbos/storage" @@ -82,7 +83,7 @@ func (p Programs) CacheManagers() *addressSet.AddressSet { return p.cacheManagers } -func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runMode core.MessageRunMode, debugMode bool) ( +func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, arbosVersion uint64, runMode core.MessageRunMode, debugMode bool) ( uint16, common.Hash, common.Hash, *big.Int, bool, error, ) { statedb := evm.StateDB @@ -116,7 +117,7 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runMode c // require the program's footprint not exceed the remaining memory budget pageLimit := am.SaturatingUSub(params.PageLimit, statedb.GetStylusPagesOpen()) - info, err := activateProgram(statedb, address, codeHash, wasm, pageLimit, stylusVersion, debugMode, burner) + info, err := activateProgram(statedb, address, codeHash, wasm, pageLimit, stylusVersion, arbosVersion, debugMode, burner) if err != nil { return 0, codeHash, common.Hash{}, nil, true, err } @@ -127,6 +128,7 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runMode c if err != nil { return 0, codeHash, common.Hash{}, nil, true, err } + evictProgram(statedb, oldModuleHash, currentVersion, debugMode, runMode, expired) } if err := p.moduleHashes.Set(codeHash, info.moduleHash); err != nil { @@ -222,6 +224,7 @@ func (p Programs) CallProgram( } evmData := &EvmData{ + arbosVersion: evm.Context.ArbOSVersion, blockBasefee: common.BigToHash(evm.Context.BaseFee), chainId: evm.ChainConfig().ChainID.Uint64(), blockCoinbase: evm.Context.Coinbase, @@ -517,6 +520,7 @@ func (p Programs) progParams(version uint16, debug bool, params *StylusParams) * } type EvmData struct { + arbosVersion uint64 blockBasefee common.Hash chainId uint64 blockCoinbase common.Address diff --git a/arbos/programs/testcompile.go b/arbos/programs/testcompile.go index a16bae52c0..58afa228d5 100644 --- a/arbos/programs/testcompile.go +++ b/arbos/programs/testcompile.go @@ -9,7 +9,7 @@ package programs // This file exists because cgo isn't allowed in tests /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #include "arbitrator.h" typedef uint16_t u16; @@ -20,6 +20,7 @@ typedef size_t usize; void handleReqWrap(usize api, u32 req_type, RustSlice *data, u64 *out_cost, GoSliceData *out_result, GoSliceData *out_raw_data); */ import "C" + import ( "fmt" "os" @@ -34,10 +35,10 @@ func Wat2Wasm(wat []byte) ([]byte, error) { status := C.wat_to_wasm(goSlice(wat), output) if status != 0 { - return nil, fmt.Errorf("failed reading wat file: %v", string(output.intoBytes())) + return nil, fmt.Errorf("failed reading wat file: %v", string(rustBytesIntoBytes(output))) } - return output.intoBytes(), nil + return rustBytesIntoBytes(output), nil } func testCompileArch(store bool) error { @@ -65,7 +66,7 @@ func testCompileArch(store bool) error { cbool(nativeArm64)) if status != 0 { - return fmt.Errorf("failed setting compilation target arm: %v", string(output.intoBytes())) + return fmt.Errorf("failed setting compilation target arm: %v", string(rustBytesIntoBytes(output))) } status = C.stylus_target_set(goSlice(amd64CompileName), @@ -74,7 +75,7 @@ func testCompileArch(store bool) error { cbool(nativeAmd64)) if status != 0 { - return fmt.Errorf("failed setting compilation target amd: %v", string(output.intoBytes())) + return fmt.Errorf("failed setting compilation target amd: %v", string(rustBytesIntoBytes(output))) } source, err := os.ReadFile("../../arbitrator/stylus/tests/add.wat") @@ -106,7 +107,7 @@ func testCompileArch(store bool) error { output, ) if status == 0 { - return fmt.Errorf("succeeded compiling non-existent arch: %v", string(output.intoBytes())) + return fmt.Errorf("succeeded compiling non-existent arch: %v", string(rustBytesIntoBytes(output))) } status = C.stylus_compile( @@ -117,7 +118,7 @@ func testCompileArch(store bool) error { output, ) if status != 0 { - return fmt.Errorf("failed compiling native: %v", string(output.intoBytes())) + return fmt.Errorf("failed compiling native: %v", string(rustBytesIntoBytes(output))) } if store && !nativeAmd64 && !nativeArm64 { _, err := fmt.Printf("writing host file\n") @@ -125,7 +126,7 @@ func testCompileArch(store bool) error { return err } - err = os.WriteFile("../../target/testdata/host.bin", output.intoBytes(), 0644) + err = os.WriteFile("../../target/testdata/host.bin", rustBytesIntoBytes(output), 0644) if err != nil { return err } @@ -139,7 +140,7 @@ func testCompileArch(store bool) error { output, ) if status != 0 { - return fmt.Errorf("failed compiling arm: %v", string(output.intoBytes())) + return fmt.Errorf("failed compiling arm: %v", string(rustBytesIntoBytes(output))) } if store { _, err := fmt.Printf("writing arm file\n") @@ -147,7 +148,7 @@ func testCompileArch(store bool) error { return err } - err = os.WriteFile("../../target/testdata/arm64.bin", output.intoBytes(), 0644) + err = os.WriteFile("../../target/testdata/arm64.bin", rustBytesIntoBytes(output), 0644) if err != nil { return err } @@ -161,7 +162,7 @@ func testCompileArch(store bool) error { output, ) if status != 0 { - return fmt.Errorf("failed compiling amd: %v", string(output.intoBytes())) + return fmt.Errorf("failed compiling amd: %v", string(rustBytesIntoBytes(output))) } if store { _, err := fmt.Printf("writing amd64 file\n") @@ -169,7 +170,7 @@ func testCompileArch(store bool) error { return err } - err = os.WriteFile("../../target/testdata/amd64.bin", output.intoBytes(), 0644) + err = os.WriteFile("../../target/testdata/amd64.bin", rustBytesIntoBytes(output), 0644) if err != nil { return err } @@ -178,6 +179,28 @@ func testCompileArch(store bool) error { return nil } +func resetNativeTarget() error { + output := &rustBytes{} + + _, err := fmt.Print("resetting native target\n") + if err != nil { + return err + } + + localCompileName := []byte("local") + + status := C.stylus_target_set(goSlice(localCompileName), + goSlice([]byte{}), + output, + cbool(true)) + + if status != 0 { + return fmt.Errorf("failed setting compilation target arm: %v", string(rustBytesIntoBytes(output))) + } + + return nil +} + func testCompileLoad() error { filePath := "../../target/testdata/host.bin" localTarget := rawdb.LocalTarget() @@ -237,7 +260,7 @@ func testCompileLoad() error { return err } - _, msg, err := status.toResult(output.intoBytes(), true) + _, msg, err := status.toResult(rustBytesIntoBytes(output), true) if status == userFailure { err = fmt.Errorf("%w: %v", err, msg) } diff --git a/arbos/programs/testconstants.go b/arbos/programs/testconstants.go index 1ab0e6e93b..44f69a52de 100644 --- a/arbos/programs/testconstants.go +++ b/arbos/programs/testconstants.go @@ -9,7 +9,7 @@ package programs // This file exists because cgo isn't allowed in tests /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #include "arbitrator.h" */ import "C" diff --git a/arbos/programs/wasm.go b/arbos/programs/wasm.go index f7191dca8f..12c23a724c 100644 --- a/arbos/programs/wasm.go +++ b/arbos/programs/wasm.go @@ -36,7 +36,7 @@ type rustConfig byte type rustModule byte type rustEvmData byte -//go:wasmimport programs activate +//go:wasmimport programs activate_v2 func programActivate( wasm_ptr unsafe.Pointer, wasm_size uint32, @@ -44,7 +44,8 @@ func programActivate( asm_estimation_ptr unsafe.Pointer, init_gas_ptr unsafe.Pointer, cached_init_gas_ptr unsafe.Pointer, - version uint32, + stylusVersion uint32, + arbosVersion uint64, debug uint32, codehash unsafe.Pointer, module_hash_ptr unsafe.Pointer, @@ -59,7 +60,8 @@ func activateProgram( codehash common.Hash, wasm []byte, pageLimit u16, - version u16, + stylusVersion u16, + arbosVersion uint64, debug bool, burner burn.Burner, ) (*activationInfo, error) { @@ -79,7 +81,8 @@ func activateProgram( unsafe.Pointer(&asmEstimate), unsafe.Pointer(&initGas), unsafe.Pointer(&cachedInitGas), - uint32(version), + uint32(stylusVersion), + arbosVersion, debugMode, arbutil.SliceToUnsafePointer(codehash[:]), arbutil.SliceToUnsafePointer(moduleHash[:]), @@ -151,6 +154,8 @@ func callProgram( return retData, err } +func GetWasmLruCacheMetrics() {} + func CallProgramLoop( moduleHash common.Hash, calldata []byte, diff --git a/arbos/programs/wasm_api.go b/arbos/programs/wasm_api.go index d7bac056c0..a4ebc1f778 100644 --- a/arbos/programs/wasm_api.go +++ b/arbos/programs/wasm_api.go @@ -20,8 +20,9 @@ func createStylusConfig(version uint32, max_depth uint32, ink_price uint32, debu type evmDataHandler uint64 -//go:wasmimport programs create_evm_data +//go:wasmimport programs create_evm_data_v2 func createEvmData( + arbosVersion uint64, blockBaseFee unsafe.Pointer, chainid uint64, blockCoinbase unsafe.Pointer, @@ -45,6 +46,7 @@ func (params *ProgParams) createHandler() stylusConfigHandler { func (data *EvmData) createHandler() evmDataHandler { return createEvmData( + data.arbosVersion, arbutil.SliceToUnsafePointer(data.blockBasefee[:]), data.chainId, arbutil.SliceToUnsafePointer(data.blockCoinbase[:]), diff --git a/arbos/programs/wasmstorehelper.go b/arbos/programs/wasmstorehelper.go index 4f82d80282..c2d1aa65b0 100644 --- a/arbos/programs/wasmstorehelper.go +++ b/arbos/programs/wasmstorehelper.go @@ -17,12 +17,12 @@ import ( // SaveActiveProgramToWasmStore is used to save active stylus programs to wasm store during rebuilding func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash common.Hash, code []byte, time uint64, debugMode bool, rebuildingStartBlockTime uint64) error { - params, err := p.Params() + progParams, err := p.Params() if err != nil { return err } - program, err := p.getActiveProgram(codeHash, time, params) + program, err := p.getActiveProgram(codeHash, time, progParams) if err != nil { // The program is not active so return early log.Info("program is not active, getActiveProgram returned error, hence do not include in rebuilding", "err", err) @@ -43,8 +43,9 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash return err } + targets := statedb.Database().WasmTargets() // If already in wasm store then return early - _, err = statedb.TryGetActivatedAsmMap([]rawdb.Target{rawdb.TargetWavm, rawdb.LocalTarget()}, moduleHash) + _, err = statedb.TryGetActivatedAsmMap(targets, moduleHash) if err == nil { return nil } @@ -55,10 +56,13 @@ func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err) } - unlimitedGas := uint64(0xffffffffffff) + // don't charge gas + zeroArbosVersion := uint64(0) + zeroGas := uint64(0) + // We know program is activated, so it must be in correct version and not use too much memory // Empty program address is supplied because we dont have access to this during rebuilding of wasm store - info, asmMap, err := activateProgramInternal(statedb, common.Address{}, codeHash, wasm, params.PageLimit, program.version, debugMode, &unlimitedGas) + info, asmMap, err := activateProgramInternal(statedb, common.Address{}, codeHash, wasm, progParams.PageLimit, program.version, zeroArbosVersion, debugMode, &zeroGas) if err != nil { log.Error("failed to reactivate program while rebuilding wasm store", "expected moduleHash", moduleHash, "err", err) return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err) diff --git a/arbos/queue_test.go b/arbos/queue_test.go index ff993a233f..75d60b82c3 100644 --- a/arbos/queue_test.go +++ b/arbos/queue_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/offchainlabs/nitro/arbos/arbosState" - "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" ) diff --git a/arbos/retryable_test.go b/arbos/retryable_test.go index ddb88348dd..b2989de331 100644 --- a/arbos/retryable_test.go +++ b/arbos/retryable_test.go @@ -9,17 +9,17 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/util/testhelpers" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/params" ) func TestOpenNonexistentRetryable(t *testing.T) { @@ -38,6 +38,7 @@ func TestRetryableLifecycle(t *testing.T) { retryableState := state.RetryableState() lifetime := uint64(retryables.RetryableLifetimeSeconds) + // #nosec G115 timestampAtCreation := uint64(rand.Int63n(1 << 16)) timeoutAtCreation := timestampAtCreation + lifetime currentTime := timeoutAtCreation @@ -57,6 +58,7 @@ func TestRetryableLifecycle(t *testing.T) { checkQueueSize := func(expected int, message string) { timeoutQueueSize, err := retryableState.TimeoutQueue.Size() Require(t, err) + // #nosec G115 if timeoutQueueSize != uint64(expected) { Fail(t, currentTime, message, timeoutQueueSize) } @@ -167,6 +169,7 @@ func TestRetryableCleanup(t *testing.T) { callvalue := big.NewInt(0) calldata := testhelpers.RandomizeSlice(make([]byte, rand.Intn(1<<12))) + // #nosec G115 timeout := uint64(rand.Int63n(1 << 16)) timestamp := 2 * timeout diff --git a/arbos/retryables/retryable.go b/arbos/retryables/retryable.go index e1cfe48bcf..23ba2458ff 100644 --- a/arbos/retryables/retryable.go +++ b/arbos/retryables/retryable.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" @@ -367,5 +368,7 @@ func RetryableEscrowAddress(ticketId common.Hash) common.Address { } func RetryableSubmissionFee(calldataLengthInBytes int, l1BaseFee *big.Int) *big.Int { - return arbmath.BigMulByUint(l1BaseFee, uint64(1400+6*calldataLengthInBytes)) + // This can't overflow because calldataLengthInBytes would need to be 3 exabytes + // #nosec G115 + return arbmath.BigMulByUint(l1BaseFee, 1400+6*uint64(calldataLengthInBytes)) } diff --git a/arbos/storage/queue.go b/arbos/storage/queue.go index 9c02dc1ee7..3c852a5743 100644 --- a/arbos/storage/queue.go +++ b/arbos/storage/queue.go @@ -4,9 +4,9 @@ package storage import ( - "github.com/offchainlabs/nitro/arbos/util" - "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/arbos/util" ) type Queue struct { diff --git a/arbos/storage/storage.go b/arbos/storage/storage.go index 6e6c976644..63db8ee928 100644 --- a/arbos/storage/storage.go +++ b/arbos/storage/storage.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/hashdb" "github.com/ethereum/go-ethereum/triedb/pathdb" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" @@ -156,11 +157,6 @@ func (s *Storage) GetUint64ByUint64(key uint64) (uint64, error) { return s.GetUint64(util.UintToHash(key)) } -func (s *Storage) GetUint32(key common.Hash) (uint32, error) { - value, err := s.Get(key) - return uint32(value.Big().Uint64()), err -} - func (s *Storage) Set(key common.Hash, value common.Hash) error { if s.burner.ReadOnly() { log.Error("Read-only burner attempted to mutate state", "key", key, "value", value) @@ -327,11 +323,11 @@ func (s *Storage) Burner() burn.Burner { } func (s *Storage) Keccak(data ...[]byte) ([]byte, error) { - byteCount := 0 + var byteCount uint64 for _, part := range data { - byteCount += len(part) + byteCount += uint64(len(part)) } - cost := 30 + 6*arbmath.WordsForBytes(uint64(byteCount)) + cost := 30 + 6*arbmath.WordsForBytes(byteCount) if err := s.burner.Burn(cost); err != nil { return nil, err } @@ -420,10 +416,12 @@ func (sbu *StorageBackedInt64) Get() (int64, error) { if !raw.Big().IsUint64() { panic("invalid value found in StorageBackedInt64 storage") } + // #nosec G115 return int64(raw.Big().Uint64()), err // see implementation note above } func (sbu *StorageBackedInt64) Set(value int64) error { + // #nosec G115 return sbu.StorageSlot.Set(util.UintToHash(uint64(value))) // see implementation note above } @@ -460,7 +458,7 @@ func (sbu *StorageBackedUBips) Get() (arbmath.UBips, error) { } func (sbu *StorageBackedUBips) Set(bips arbmath.UBips) error { - return sbu.backing.Set(bips.Uint64()) + return sbu.backing.Set(uint64(bips)) } type StorageBackedUint16 struct { @@ -477,6 +475,7 @@ func (sbu *StorageBackedUint16) Get() (uint16, error) { if !big.IsUint64() || big.Uint64() > math.MaxUint16 { panic("expected uint16 compatible value in storage") } + // #nosec G115 return uint16(big.Uint64()), err } @@ -517,6 +516,7 @@ func (sbu *StorageBackedUint32) Get() (uint32, error) { if !big.IsUint64() || big.Uint64() > math.MaxUint32 { panic("expected uint32 compatible value in storage") } + // #nosec G115 return uint32(big.Uint64()), err } diff --git a/arbos/storage/storage_test.go b/arbos/storage/storage_test.go index b2e8bdb2ea..dd2c40b8f0 100644 --- a/arbos/storage/storage_test.go +++ b/arbos/storage/storage_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/util/arbmath" ) diff --git a/arbos/tx_processor.go b/arbos/tx_processor.go index b08c7c5d30..7cebd8da37 100644 --- a/arbos/tx_processor.go +++ b/arbos/tx_processor.go @@ -9,22 +9,20 @@ import ( "math/big" "github.com/holiman/uint256" - "github.com/offchainlabs/nitro/arbos/l1pricing" - - "github.com/offchainlabs/nitro/arbos/util" - "github.com/offchainlabs/nitro/util/arbmath" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" + glog "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" - "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/arbosState" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/vm" - glog "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/arbos/retryables" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/arbmath" ) var arbosAddress = types.ArbosAddress @@ -153,13 +151,17 @@ func (p *TxProcessor) StartTxHook() (endTxNow bool, gasUsed uint64, err error, r } evm.IncrementDepth() // fake a call from := p.msg.From - tracer.CaptureStart(evm, from, *p.msg.To, false, p.msg.Data, p.msg.GasLimit, p.msg.Value) + if tracer.OnEnter != nil { + tracer.OnEnter(evm.Depth(), byte(vm.CALL), from, *p.msg.To, p.msg.Data, p.msg.GasLimit, p.msg.Value) + } tracingInfo = util.NewTracingInfo(evm, from, *p.msg.To, util.TracingDuringEVM) p.state = arbosState.OpenSystemArbosStateOrPanic(evm.StateDB, tracingInfo, false) return func() { - tracer.CaptureEnd(nil, p.state.Burner.Burned(), nil) + if tracer.OnExit != nil { + tracer.OnExit(evm.Depth(), nil, p.state.Burner.Burned(), nil, false) + } evm.DecrementDepth() // fake the return to the first faked call tracingInfo = util.NewTracingInfo(evm, from, *p.msg.To, util.TracingAfterEVM) @@ -305,7 +307,7 @@ func (p *TxProcessor) StartTxHook() (endTxNow bool, gasUsed uint64, err error, r // pay for the retryable's gas and update the pools gascost := arbmath.BigMulByUint(effectiveBaseFee, usergas) networkCost := gascost - if p.state.ArbOSVersion() >= 11 { + if p.state.ArbOSVersion() >= params.ArbosVersion_11 { infraFeeAccount, err := p.state.InfraFeeAccount() p.state.Restrict(err) if infraFeeAccount != (common.Address{}) { @@ -532,6 +534,20 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { refund := func(refundFrom common.Address, amount *big.Int) { const errLog = "fee address doesn't have enough funds to give user refund" + logMissingRefund := func(err error) { + if !errors.Is(err, vm.ErrInsufficientBalance) { + log.Error("unexpected error refunding balance", "err", err, "feeAddress", refundFrom) + return + } + logLevel := log.Error + isContract := p.evm.StateDB.GetCodeSize(refundFrom) > 0 + if isContract { + // It's expected that the balance might not still be in this address if it's a contract. + logLevel = log.Debug + } + logLevel(errLog, "err", err, "feeAddress", refundFrom) + } + // Refund funds to the fee refund address without overdrafting the L1 deposit. toRefundAddr := takeFunds(maxRefund, amount) err = util.TransferBalance(&refundFrom, &inner.RefundTo, toRefundAddr, p.evm, scenario, "refund") @@ -539,13 +555,13 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { // Normally the network fee address should be holding any collected fees. // However, in theory, they could've been transferred out during the redeem attempt. // If the network fee address doesn't have the necessary balance, log an error and don't give a refund. - log.Error(errLog, "err", err, "feeAddress", refundFrom) + logMissingRefund(err) } // Any extra refund can't be given to the fee refund address if it didn't come from the L1 deposit. // Instead, give the refund to the retryable from address. err = util.TransferBalance(&refundFrom, &inner.From, arbmath.BigSub(amount, toRefundAddr), p.evm, scenario, "refund") if err != nil { - log.Error(errLog, "err", err, "feeAddress", refundFrom) + logMissingRefund(err) } } @@ -560,7 +576,7 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { takeFunds(maxRefund, arbmath.BigMulByUint(effectiveBaseFee, gasUsed)) // Refund any unused gas, without overdrafting the L1 deposit. networkRefund := gasRefund - if p.state.ArbOSVersion() >= 11 { + if p.state.ArbOSVersion() >= params.ArbosVersion_11 { infraFeeAccount, err := p.state.InfraFeeAccount() p.state.Restrict(err) if infraFeeAccount != (common.Address{}) { @@ -613,7 +629,7 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { } purpose := "feeCollection" - if p.state.ArbOSVersion() > 4 { + if p.state.ArbOSVersion() > params.ArbosVersion_4 { infraFeeAccount, err := p.state.InfraFeeAccount() p.state.Restrict(err) if infraFeeAccount != (common.Address{}) { @@ -630,11 +646,11 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { util.MintBalance(&networkFeeAccount, computeCost, p.evm, scenario, purpose) } posterFeeDestination := l1pricing.L1PricerFundsPoolAddress - if p.state.ArbOSVersion() < 2 { + if p.state.ArbOSVersion() < params.ArbosVersion_2 { posterFeeDestination = p.evm.Context.Coinbase } util.MintBalance(&posterFeeDestination, p.PosterFee, p.evm, scenario, purpose) - if p.state.ArbOSVersion() >= 10 { + if p.state.ArbOSVersion() >= params.ArbosVersion_10 { if _, err := p.state.L1PricingState().AddToL1FeesAvailable(p.PosterFee); err != nil { log.Error("failed to update L1FeesAvailable: ", "err", err) } @@ -732,13 +748,13 @@ func (p *TxProcessor) L1BlockHash(blockCtx vm.BlockContext, l1BlockNumber uint64 func (p *TxProcessor) DropTip() bool { version := p.state.ArbOSVersion() - return version != 9 || p.delayedInbox + return version != params.ArbosVersion_9 || p.delayedInbox } func (p *TxProcessor) GetPaidGasPrice() *big.Int { gasPrice := p.evm.GasPrice version := p.state.ArbOSVersion() - if version != 9 { + if version != params.ArbosVersion_9 { // p.evm.Context.BaseFee is already lowered to 0 when vm runs with NoBaseFee flag and 0 gas price gasPrice = p.evm.Context.BaseFee } @@ -746,7 +762,7 @@ func (p *TxProcessor) GetPaidGasPrice() *big.Int { } func (p *TxProcessor) GasPriceOp(evm *vm.EVM) *big.Int { - if p.state.ArbOSVersion() >= 3 { + if p.state.ArbOSVersion() >= params.ArbosVersion_3 { return p.GetPaidGasPrice() } return evm.GasPrice diff --git a/arbos/util/retryable_encoding_test.go b/arbos/util/retryable_encoding_test.go index d7a5480138..b74983ed0b 100644 --- a/arbos/util/retryable_encoding_test.go +++ b/arbos/util/retryable_encoding_test.go @@ -10,16 +10,15 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/testhelpers" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" ) func TestRetryableEncoding(t *testing.T) { diff --git a/arbos/util/storage_cache.go b/arbos/util/storage_cache.go index bf05a5824d..a9be5fe870 100644 --- a/arbos/util/storage_cache.go +++ b/arbos/util/storage_cache.go @@ -4,6 +4,8 @@ package util import ( + "slices" + "github.com/ethereum/go-ethereum/common" ) @@ -67,6 +69,10 @@ func (s *storageCache) Flush() []storageCacheStores { }) } } + sortFunc := func(a, b storageCacheStores) int { + return a.Key.Cmp(b.Key) + } + slices.SortFunc(stores, sortFunc) return stores } diff --git a/arbos/util/storage_cache_test.go b/arbos/util/storage_cache_test.go index 1cc4ea14ec..0ba2c5285e 100644 --- a/arbos/util/storage_cache_test.go +++ b/arbos/util/storage_cache_test.go @@ -4,12 +4,13 @@ package util import ( - "bytes" "slices" "testing" - "github.com/ethereum/go-ethereum/common" "github.com/google/go-cmp/cmp" + + "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -76,7 +77,7 @@ func TestStorageCache(t *testing.T) { {Key: keys[2], Value: values[2]}, } sortFunc := func(a, b storageCacheStores) int { - return bytes.Compare(a.Key.Bytes(), b.Key.Bytes()) + return a.Key.Cmp(b.Key) } slices.SortFunc(stores, sortFunc) slices.SortFunc(expected, sortFunc) diff --git a/arbos/util/tracing.go b/arbos/util/tracing.go index c4a7168977..f092d32c2d 100644 --- a/arbos/util/tracing.go +++ b/arbos/util/tracing.go @@ -7,10 +7,12 @@ import ( "encoding/binary" "math/big" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" - "github.com/holiman/uint256" ) type TracingScenario uint64 @@ -22,7 +24,7 @@ const ( ) type TracingInfo struct { - Tracer vm.EVMLogger + Tracer *tracing.Hooks Scenario TracingScenario Contract *vm.Contract Depth int @@ -59,8 +61,10 @@ func (info *TracingInfo) RecordStorageGet(key common.Hash) { Stack: TracingStackFromArgs(HashToUint256(key)), Contract: info.Contract, } - tracer.CaptureState(0, vm.SLOAD, 0, 0, scope, []byte{}, info.Depth, nil) - } else { + if tracer.OnOpcode != nil { + tracer.OnOpcode(0, byte(vm.SLOAD), 0, 0, scope, []byte{}, info.Depth, nil) + } + } else if tracer.CaptureArbitrumStorageGet != nil { tracer.CaptureArbitrumStorageGet(key, info.Depth, info.Scenario == TracingBeforeEVM) } } @@ -73,8 +77,10 @@ func (info *TracingInfo) RecordStorageSet(key, value common.Hash) { Stack: TracingStackFromArgs(HashToUint256(key), HashToUint256(value)), Contract: info.Contract, } - tracer.CaptureState(0, vm.SSTORE, 0, 0, scope, []byte{}, info.Depth, nil) - } else { + if tracer.OnOpcode != nil { + tracer.OnOpcode(0, byte(vm.SSTORE), 0, 0, scope, []byte{}, info.Depth, nil) + } + } else if tracer.CaptureArbitrumStorageSet != nil { tracer.CaptureArbitrumStorageSet(key, value, info.Depth, info.Scenario == TracingBeforeEVM) } } @@ -98,8 +104,12 @@ func (info *TracingInfo) MockCall(input []byte, gas uint64, from, to common.Addr ), Contract: contract, } - tracer.CaptureState(0, vm.CALL, 0, 0, scope, []byte{}, depth, nil) - tracer.CaptureEnter(vm.INVALID, from, to, input, 0, amount) + if tracer.OnOpcode != nil { + tracer.OnOpcode(0, byte(vm.CALL), 0, 0, scope, []byte{}, depth, nil) + } + if tracer.OnEnter != nil { + tracer.OnEnter(depth, byte(vm.CALL), from, to, input, gas, amount) + } retScope := &vm.ScopeContext{ Memory: vm.NewMemory(), @@ -109,8 +119,12 @@ func (info *TracingInfo) MockCall(input []byte, gas uint64, from, to common.Addr ), Contract: contract, } - tracer.CaptureState(0, vm.RETURN, 0, 0, retScope, []byte{}, depth+1, nil) - tracer.CaptureExit(nil, 0, nil) + if tracer.OnOpcode != nil { + tracer.OnOpcode(0, byte(vm.RETURN), 0, 0, retScope, []byte{}, depth+1, nil) + } + if tracer.OnExit != nil { + tracer.OnExit(depth, nil, 0, nil, false) + } popScope := &vm.ScopeContext{ Memory: vm.NewMemory(), @@ -119,7 +133,9 @@ func (info *TracingInfo) MockCall(input []byte, gas uint64, from, to common.Addr ), Contract: contract, } - tracer.CaptureState(0, vm.POP, 0, 0, popScope, []byte{}, depth, nil) + if tracer.OnOpcode != nil { + tracer.OnOpcode(0, byte(vm.POP), 0, 0, popScope, []byte{}, depth, nil) + } } func (info *TracingInfo) CaptureEVMTraceForHostio(name string, args, outs []byte, startInk, endInk uint64) { @@ -533,7 +549,9 @@ func (info *TracingInfo) captureState(op vm.OpCode, gas uint64, cost uint64, mem Stack: TracingStackFromArgs(stack...), Contract: info.Contract, } - info.Tracer.CaptureState(0, op, gas, cost, scope, []byte{}, info.Depth, nil) + if info.Tracer.OnOpcode != nil { + info.Tracer.OnOpcode(0, byte(op), gas, cost, scope, []byte{}, info.Depth, nil) + } } func lenToBytes(data []byte) []byte { diff --git a/arbos/util/transfer.go b/arbos/util/transfer.go index e293ef13c3..0b61868abe 100644 --- a/arbos/util/transfer.go +++ b/arbos/util/transfer.go @@ -9,10 +9,14 @@ import ( "fmt" "math/big" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" - "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/util/arbmath" ) @@ -28,20 +32,6 @@ func TransferBalance( if amount.Sign() < 0 { panic(fmt.Sprintf("Tried to transfer negative amount %v from %v to %v", amount, from, to)) } - if from != nil { - balance := evm.StateDB.GetBalance(*from) - if arbmath.BigLessThan(balance.ToBig(), amount) { - return fmt.Errorf("%w: addr %v have %v want %v", vm.ErrInsufficientBalance, *from, balance, amount) - } - evm.StateDB.SubBalance(*from, uint256.MustFromBig(amount)) - if evm.Context.ArbOSVersion >= 30 { - // ensure the from account is "touched" for EIP-161 - evm.StateDB.AddBalance(*from, &uint256.Int{}) - } - } - if to != nil { - evm.StateDB.AddBalance(*to, uint256.MustFromBig(amount)) - } if tracer := evm.Config.Tracer; tracer != nil { if evm.Depth() != 0 && scenario != TracingDuringEVM { // A non-zero depth implies this transfer is occurring inside EVM execution @@ -50,24 +40,40 @@ func TransferBalance( } if scenario != TracingDuringEVM { - tracer.CaptureArbitrumTransfer(evm, from, to, amount, scenario == TracingBeforeEVM, purpose) - return nil - } + if tracer.CaptureArbitrumTransfer != nil { + tracer.CaptureArbitrumTransfer(from, to, amount, scenario == TracingBeforeEVM, purpose) + } + } else { + fromCopy := from + toCopy := to + if fromCopy == nil { + fromCopy = &common.Address{} + } + if toCopy == nil { + toCopy = &common.Address{} + } - if from == nil { - from = &common.Address{} + info := &TracingInfo{ + Tracer: evm.Config.Tracer, + Scenario: scenario, + Contract: vm.NewContract(addressHolder{*toCopy}, addressHolder{*fromCopy}, uint256.NewInt(0), 0), + Depth: evm.Depth(), + } + info.MockCall([]byte{}, 0, *fromCopy, *toCopy, amount) } - if to == nil { - to = &common.Address{} + } + if from != nil { + balance := evm.StateDB.GetBalance(*from) + if arbmath.BigLessThan(balance.ToBig(), amount) { + return fmt.Errorf("%w: addr %v have %v want %v", vm.ErrInsufficientBalance, *from, balance, amount) } - - info := &TracingInfo{ - Tracer: evm.Config.Tracer, - Scenario: scenario, - Contract: vm.NewContract(addressHolder{*to}, addressHolder{*from}, uint256.NewInt(0), 0), - Depth: evm.Depth(), + if evm.Context.ArbOSVersion < params.ArbosVersion_30 && amount.Sign() == 0 { + evm.StateDB.CreateZombieIfDeleted(*from) } - info.MockCall([]byte{}, 0, *from, *to, amount) + evm.StateDB.SubBalance(*from, uint256.MustFromBig(amount), tracing.BalanceChangeTransfer) + } + if to != nil { + evm.StateDB.AddBalance(*to, uint256.MustFromBig(amount), tracing.BalanceChangeTransfer) } return nil } diff --git a/arbos/util/util.go b/arbos/util/util.go index 69d90171a0..abb7135757 100644 --- a/arbos/util/util.go +++ b/arbos/util/util.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" pgen "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" diff --git a/arbstate/daprovider/reader.go b/arbstate/daprovider/reader.go index 488b156454..e2fd884340 100644 --- a/arbstate/daprovider/reader.go +++ b/arbstate/daprovider/reader.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/blobs" ) diff --git a/arbstate/inbox.go b/arbstate/inbox.go index 753ca19cd6..b58a7420b7 100644 --- a/arbstate/inbox.go +++ b/arbstate/inbox.go @@ -246,7 +246,7 @@ func (r *inboxMultiplexer) IsCachedSegementLast() bool { if r.delayedMessagesRead < seqMsg.afterDelayedMessages { return false } - for segmentNum := int(r.cachedSegmentNum) + 1; segmentNum < len(seqMsg.segments); segmentNum++ { + for segmentNum := r.cachedSegmentNum + 1; segmentNum < uint64(len(seqMsg.segments)); segmentNum++ { segment := seqMsg.segments[segmentNum] if len(segment) == 0 { continue @@ -276,7 +276,7 @@ func (r *inboxMultiplexer) getNextMsg() (*arbostypes.MessageWithMetadata, error) if segmentNum >= uint64(len(seqMsg.segments)) { break } - segment = seqMsg.segments[int(segmentNum)] + segment = seqMsg.segments[segmentNum] if len(segment) == 0 { segmentNum++ continue @@ -322,7 +322,7 @@ func (r *inboxMultiplexer) getNextMsg() (*arbostypes.MessageWithMetadata, error) log.Warn("reading virtual delayed message segment", "delayedMessagesRead", r.delayedMessagesRead, "afterDelayedMessages", seqMsg.afterDelayedMessages) segment = []byte{BatchSegmentKindDelayedMessages} } else { - segment = seqMsg.segments[int(segmentNum)] + segment = seqMsg.segments[segmentNum] } if len(segment) == 0 { log.Error("empty sequencer message segment", "sequence", r.cachedSegmentNum, "segmentNum", segmentNum) diff --git a/arbstate/inbox_fuzz_test.go b/arbstate/inbox_fuzz_test.go index 5ede321810..5a77b7e298 100644 --- a/arbstate/inbox_fuzz_test.go +++ b/arbstate/inbox_fuzz_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbstate/daprovider" ) diff --git a/arbutil/block_message_relation.go b/arbutil/block_message_relation.go index a69f9079ee..dcf4c86084 100644 --- a/arbutil/block_message_relation.go +++ b/arbutil/block_message_relation.go @@ -11,9 +11,11 @@ func BlockNumberToMessageCount(blockNumber uint64, genesisBlockNumber uint64) Me // Block number must correspond to a message count, meaning it may not be less than -1 func SignedBlockNumberToMessageCount(blockNumber int64, genesisBlockNumber uint64) MessageIndex { + // #nosec G115 return MessageIndex(uint64(blockNumber+1) - genesisBlockNumber) } func MessageCountToBlockNumber(messageCount MessageIndex, genesisBlockNumber uint64) int64 { + // #nosec G115 return int64(uint64(messageCount)+genesisBlockNumber) - 1 } diff --git a/arbutil/correspondingl1blocknumber.go b/arbutil/correspondingl1blocknumber.go index 05323ed183..c8770e2034 100644 --- a/arbutil/correspondingl1blocknumber.go +++ b/arbutil/correspondingl1blocknumber.go @@ -19,7 +19,12 @@ func ParentHeaderToL1BlockNumber(header *types.Header) uint64 { return header.Number.Uint64() } -func CorrespondingL1BlockNumber(ctx context.Context, client L1Interface, parentBlockNumber uint64) (uint64, error) { +type ParentHeaderFetcher interface { + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) +} + +func CorrespondingL1BlockNumber(ctx context.Context, client ParentHeaderFetcher, parentBlockNumber uint64) (uint64, error) { + // #nosec G115 header, err := client.HeaderByNumber(ctx, big.NewInt(int64(parentBlockNumber))) if err != nil { return 0, fmt.Errorf("error getting L1 block number %d header : %w", parentBlockNumber, err) diff --git a/arbutil/hash_test.go b/arbutil/hash_test.go index 2b93353d08..4b39bf328e 100644 --- a/arbutil/hash_test.go +++ b/arbutil/hash_test.go @@ -4,8 +4,9 @@ import ( "bytes" "testing" - "github.com/ethereum/go-ethereum/common" "github.com/google/go-cmp/cmp" + + "github.com/ethereum/go-ethereum/common" ) func TestSlotAddress(t *testing.T) { diff --git a/arbutil/transaction_data.go b/arbutil/transaction_data.go index 8270a628bd..c5728967c7 100644 --- a/arbutil/transaction_data.go +++ b/arbutil/transaction_data.go @@ -8,9 +8,10 @@ import ( "fmt" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" ) -func GetLogTransaction(ctx context.Context, client L1Interface, log types.Log) (*types.Transaction, error) { +func GetLogTransaction(ctx context.Context, client *ethclient.Client, log types.Log) (*types.Transaction, error) { tx, err := client.TransactionInBlock(ctx, log.BlockHash, log.TxIndex) if err != nil { return nil, err @@ -22,7 +23,7 @@ func GetLogTransaction(ctx context.Context, client L1Interface, log types.Log) ( } // GetLogEmitterTxData requires that the tx's data is at least 4 bytes long -func GetLogEmitterTxData(ctx context.Context, client L1Interface, log types.Log) ([]byte, error) { +func GetLogEmitterTxData(ctx context.Context, client *ethclient.Client, log types.Log) ([]byte, error) { tx, err := GetLogTransaction(ctx, client, log) if err != nil { return nil, err diff --git a/arbutil/wait_for_l1.go b/arbutil/wait_for_l1.go index 4b4819156d..80dd356b24 100644 --- a/arbutil/wait_for_l1.go +++ b/arbutil/wait_for_l1.go @@ -10,27 +10,13 @@ import ( "math/big" "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/ethclient" ) -type L1Interface interface { - bind.ContractBackend - bind.BlockHashContractCaller - ethereum.ChainReader - ethereum.ChainStateReader - ethereum.TransactionReader - TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) - BlockNumber(ctx context.Context) (uint64, error) - PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) - ChainID(ctx context.Context) (*big.Int, error) - Client() rpc.ClientInterface -} - -func SendTxAsCall(ctx context.Context, client L1Interface, tx *types.Transaction, from common.Address, blockNum *big.Int, unlimitedGas bool) ([]byte, error) { +func SendTxAsCall(ctx context.Context, client *ethclient.Client, tx *types.Transaction, from common.Address, blockNum *big.Int, unlimitedGas bool) ([]byte, error) { var gas uint64 if unlimitedGas { gas = 0 @@ -50,7 +36,7 @@ func SendTxAsCall(ctx context.Context, client L1Interface, tx *types.Transaction return client.CallContract(ctx, callMsg, blockNum) } -func GetPendingCallBlockNumber(ctx context.Context, client L1Interface) (*big.Int, error) { +func GetPendingCallBlockNumber(ctx context.Context, client *ethclient.Client) (*big.Int, error) { msg := ethereum.CallMsg{ // Pretend to be a contract deployment to execute EVM code without calling a contract. To: nil, @@ -70,7 +56,7 @@ func GetPendingCallBlockNumber(ctx context.Context, client L1Interface) (*big.In return new(big.Int).SetBytes(callRes), nil } -func DetailTxError(ctx context.Context, client L1Interface, tx *types.Transaction, txRes *types.Receipt) error { +func DetailTxError(ctx context.Context, client *ethclient.Client, tx *types.Transaction, txRes *types.Receipt) error { // Re-execute the transaction as a call to get a better error if ctx.Err() != nil { return ctx.Err() @@ -96,7 +82,7 @@ func DetailTxError(ctx context.Context, client L1Interface, tx *types.Transactio return fmt.Errorf("SendTxAsCall got: %w for tx hash %v", err, tx.Hash()) } -func DetailTxErrorUsingCallMsg(ctx context.Context, client L1Interface, txHash common.Hash, txRes *types.Receipt, callMsg ethereum.CallMsg) error { +func DetailTxErrorUsingCallMsg(ctx context.Context, client *ethclient.Client, txHash common.Hash, txRes *types.Receipt, callMsg ethereum.CallMsg) error { // Re-execute the transaction as a call to get a better error if ctx.Err() != nil { return ctx.Err() diff --git a/blocks_reexecutor/blocks_reexecutor.go b/blocks_reexecutor/blocks_reexecutor.go index 1e4a06fe90..d074457626 100644 --- a/blocks_reexecutor/blocks_reexecutor.go +++ b/blocks_reexecutor/blocks_reexecutor.go @@ -7,24 +7,33 @@ import ( "math/rand" "runtime" "strings" + "sync" + + flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/arbitrum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/triedb" + "github.com/ethereum/go-ethereum/triedb/hashdb" + "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" ) type Config struct { - Enable bool `koanf:"enable"` - Mode string `koanf:"mode"` - StartBlock uint64 `koanf:"start-block"` - EndBlock uint64 `koanf:"end-block"` - Room int `koanf:"room"` - BlocksPerThread uint64 `koanf:"blocks-per-thread"` + Enable bool `koanf:"enable"` + Mode string `koanf:"mode"` + StartBlock uint64 `koanf:"start-block"` + EndBlock uint64 `koanf:"end-block"` + Room int `koanf:"room"` + MinBlocksPerThread uint64 `koanf:"min-blocks-per-thread"` + TrieCleanLimit int `koanf:"trie-clean-limit"` } func (c *Config) Validate() error { @@ -48,10 +57,11 @@ var DefaultConfig = Config{ } var TestConfig = Config{ - Enable: true, - Mode: "full", - Room: runtime.NumCPU(), - BlocksPerThread: 10, + Enable: true, + Mode: "full", + Room: runtime.NumCPU(), + MinBlocksPerThread: 10, + TrieCleanLimit: 600, } func ConfigAddOptions(prefix string, f *flag.FlagSet) { @@ -60,22 +70,28 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) { f.Uint64(prefix+".start-block", DefaultConfig.StartBlock, "first block number of the block range for re-execution") f.Uint64(prefix+".end-block", DefaultConfig.EndBlock, "last block number of the block range for re-execution") f.Int(prefix+".room", DefaultConfig.Room, "number of threads to parallelize blocks re-execution") - f.Uint64(prefix+".blocks-per-thread", DefaultConfig.BlocksPerThread, "minimum number of blocks to execute per thread. When mode is random this acts as the size of random block range sample") + f.Uint64(prefix+".min-blocks-per-thread", DefaultConfig.MinBlocksPerThread, "minimum number of blocks to execute per thread. When mode is random this acts as the size of random block range sample") + f.Int(prefix+".trie-clean-limit", DefaultConfig.TrieCleanLimit, "memory allowance (MB) to use for caching trie nodes in memory") } type BlocksReExecutor struct { stopwaiter.StopWaiter - config *Config - blockchain *core.BlockChain - stateFor arbitrum.StateForHeaderFunction - done chan struct{} - fatalErrChan chan error - startBlock uint64 - currentBlock uint64 - blocksPerThread uint64 + config *Config + db state.Database + blockchain *core.BlockChain + stateFor arbitrum.StateForHeaderFunction + done chan struct{} + fatalErrChan chan error + startBlock uint64 + currentBlock uint64 + minBlocksPerThread uint64 + mutex sync.Mutex } -func New(c *Config, blockchain *core.BlockChain, fatalErrChan chan error) *BlocksReExecutor { +func New(c *Config, blockchain *core.BlockChain, ethDb ethdb.Database, fatalErrChan chan error) (*BlocksReExecutor, error) { + if blockchain.TrieDB().Scheme() == rawdb.PathScheme { + return nil, errors.New("blocksReExecutor not supported on pathdb") + } start := c.StartBlock end := c.EndBlock chainStart := blockchain.Config().ArbitrumChainParams.GenesisBlockNum @@ -92,17 +108,18 @@ func New(c *Config, blockchain *core.BlockChain, fatalErrChan chan error) *Block log.Warn("invalid state reexecutor's end block number, resetting to latest", "end", end, "latest", chainEnd) end = chainEnd } - blocksPerThread := uint64(10000) - if c.BlocksPerThread != 0 { - blocksPerThread = c.BlocksPerThread + minBlocksPerThread := uint64(10000) + if c.MinBlocksPerThread != 0 { + minBlocksPerThread = c.MinBlocksPerThread } if c.Mode == "random" && end != start { - // Reexecute a range of 10000 or (non-zero) c.BlocksPerThread number of blocks between start to end picked randomly - rng := blocksPerThread + // Reexecute a range of 10000 or (non-zero) c.MinBlocksPerThread number of blocks between start to end picked randomly + rng := minBlocksPerThread if rng > end-start { rng = end - start } - start += uint64(rand.Intn(int(end - start - rng + 1))) + // #nosec G115 + start += uint64(rand.Int63n(int64(end - start - rng + 1))) end = start + rng } // Inclusive of block reexecution [start, end] @@ -110,31 +127,46 @@ func New(c *Config, blockchain *core.BlockChain, fatalErrChan chan error) *Block if start > 0 && start != chainStart { start-- } - // Divide work equally among available threads when BlocksPerThread is zero - if c.BlocksPerThread == 0 { - work := (end - start) / uint64(c.Room) + // Divide work equally among available threads when MinBlocksPerThread is zero + if c.MinBlocksPerThread == 0 { + // #nosec G115 + work := (end - start) / uint64(c.Room*2) if work > 0 { - blocksPerThread = work + minBlocksPerThread = work } } - return &BlocksReExecutor{ - config: c, - blockchain: blockchain, - currentBlock: end, - startBlock: start, - blocksPerThread: blocksPerThread, - done: make(chan struct{}, c.Room), - fatalErrChan: fatalErrChan, - stateFor: func(header *types.Header) (*state.StateDB, arbitrum.StateReleaseFunc, error) { - state, err := blockchain.StateAt(header.Root) - return state, arbitrum.NoopStateRelease, err - }, + hashConfig := *hashdb.Defaults + hashConfig.CleanCacheSize = c.TrieCleanLimit * 1024 * 1024 + trieConfig := triedb.Config{ + Preimages: false, + HashDB: &hashConfig, + } + blocksReExecutor := &BlocksReExecutor{ + config: c, + db: state.NewDatabaseWithConfig(ethDb, &trieConfig), + blockchain: blockchain, + currentBlock: end, + startBlock: start, + minBlocksPerThread: minBlocksPerThread, + done: make(chan struct{}, c.Room), + fatalErrChan: fatalErrChan, } + blocksReExecutor.stateFor = func(header *types.Header) (*state.StateDB, arbitrum.StateReleaseFunc, error) { + blocksReExecutor.mutex.Lock() + defer blocksReExecutor.mutex.Unlock() + sdb, err := state.New(header.Root, blocksReExecutor.db, nil) + if err == nil { + _ = blocksReExecutor.db.TrieDB().Reference(header.Root, common.Hash{}) // Will be dereferenced later in advanceStateUpToBlock + return sdb, func() { blocksReExecutor.dereferenceRoot(header.Root) }, nil + } + return sdb, arbitrum.NoopStateRelease, err + } + return blocksReExecutor, nil } -// LaunchBlocksReExecution launches the thread to apply blocks of range [currentBlock-s.config.BlocksPerThread, currentBlock] to the last available valid state +// LaunchBlocksReExecution launches the thread to apply blocks of range [currentBlock-s.config.MinBlocksPerThread, currentBlock] to the last available valid state func (s *BlocksReExecutor) LaunchBlocksReExecution(ctx context.Context, currentBlock uint64) uint64 { - start := arbmath.SaturatingUSub(currentBlock, s.blocksPerThread) + start := arbmath.SaturatingUSub(currentBlock, s.minBlocksPerThread) if start < s.startBlock { start = s.startBlock } @@ -143,12 +175,10 @@ func (s *BlocksReExecutor) LaunchBlocksReExecution(ctx context.Context, currentB s.fatalErrChan <- fmt.Errorf("blocksReExecutor failed to get last available state while searching for state at %d, err: %w", start, err) return s.startBlock } - // NoOp - defer release() start = startHeader.Number.Uint64() s.LaunchThread(func(ctx context.Context) { - _, err := arbitrum.AdvanceStateUpToBlock(ctx, s.blockchain, startState, s.blockchain.GetHeaderByNumber(currentBlock), startHeader, nil) - if err != nil { + log.Info("Starting reexecution of blocks against historic state", "stateAt", start, "startBlock", start+1, "endBlock", currentBlock) + if err := s.advanceStateUpToBlock(ctx, startState, s.blockchain.GetHeaderByNumber(currentBlock), startHeader, release); err != nil { s.fatalErrChan <- fmt.Errorf("blocksReExecutor errored advancing state from block %d to block %d, err: %w", start, currentBlock, err) } else { log.Info("Successfully reexecuted blocks against historic state", "stateAt", start, "startBlock", start+1, "endBlock", currentBlock) @@ -197,3 +227,60 @@ func (s *BlocksReExecutor) Start(ctx context.Context, done chan struct{}) { func (s *BlocksReExecutor) StopAndWait() { s.StopWaiter.StopAndWait() } + +func (s *BlocksReExecutor) dereferenceRoot(root common.Hash) { + s.mutex.Lock() + defer s.mutex.Unlock() + _ = s.db.TrieDB().Dereference(root) +} + +func (s *BlocksReExecutor) commitStateAndVerify(statedb *state.StateDB, expected common.Hash, blockNumber uint64) (*state.StateDB, arbitrum.StateReleaseFunc, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + result, err := statedb.Commit(blockNumber, true) + if err != nil { + return nil, arbitrum.NoopStateRelease, err + } + if result != expected { + return nil, arbitrum.NoopStateRelease, fmt.Errorf("bad root hash expected: %v got: %v", expected, result) + } + sdb, err := state.New(result, s.db, nil) + if err == nil { + _ = s.db.TrieDB().Reference(result, common.Hash{}) + return sdb, func() { s.dereferenceRoot(result) }, nil + } + return sdb, arbitrum.NoopStateRelease, err +} + +func (s *BlocksReExecutor) advanceStateUpToBlock(ctx context.Context, state *state.StateDB, targetHeader *types.Header, lastAvailableHeader *types.Header, lastRelease arbitrum.StateReleaseFunc) error { + targetBlockNumber := targetHeader.Number.Uint64() + blockToRecreate := lastAvailableHeader.Number.Uint64() + 1 + prevHash := lastAvailableHeader.Hash() + var stateRelease arbitrum.StateReleaseFunc + defer func() { + lastRelease() + }() + var block *types.Block + var err error + for ctx.Err() == nil { + state, block, err = arbitrum.AdvanceStateByBlock(ctx, s.blockchain, state, blockToRecreate, prevHash, nil) + if err != nil { + return err + } + prevHash = block.Hash() + state, stateRelease, err = s.commitStateAndVerify(state, block.Root(), block.NumberU64()) + if err != nil { + return fmt.Errorf("failed committing state for block %d : %w", blockToRecreate, err) + } + lastRelease() + lastRelease = stateRelease + if blockToRecreate >= targetBlockNumber { + if block.Hash() != targetHeader.Hash() { + return fmt.Errorf("blockHash doesn't match when recreating number: %d expected: %v got: %v", blockToRecreate, targetHeader.Hash(), block.Hash()) + } + return nil + } + blockToRecreate++ + } + return ctx.Err() +} diff --git a/bold b/bold new file mode 160000 index 0000000000..d0a87de774 --- /dev/null +++ b/bold @@ -0,0 +1 @@ +Subproject commit d0a87de774aecfa97161efd1b0a924d4d5fbcf74 diff --git a/broadcastclient/broadcastclient.go b/broadcastclient/broadcastclient.go index 7d27c57fe9..c4a3743276 100644 --- a/broadcastclient/broadcastclient.go +++ b/broadcastclient/broadcastclient.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/arbutil" m "github.com/offchainlabs/nitro/broadcaster/message" "github.com/offchainlabs/nitro/util/contracts" @@ -129,9 +130,10 @@ type BroadcastClient struct { chainId uint64 - // Protects conn and shuttingDown - connMutex sync.Mutex - conn net.Conn + // Protects conn, shuttingDown and compression + connMutex sync.Mutex + conn net.Conn + compression bool retryCount atomic.Int64 @@ -280,13 +282,25 @@ func (bc *BroadcastClient) connect(ctx context.Context, nextSeqNum arbutil.Messa MinVersion: tls.VersionTLS12, }, Extensions: extensions, + NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) { + var netDialer net.Dialer + // For tcp connections, prefer IPv4 over IPv6 to avoid rate limiting issues + if network == "tcp" { + conn, err := netDialer.DialContext(ctx, "tcp4", addr) + if err == nil { + return conn, nil + } + return netDialer.DialContext(ctx, "tcp6", addr) + } + return netDialer.DialContext(ctx, network, addr) + }, } if bc.isShuttingDown() { return nil, nil } - conn, br, _, err := timeoutDialer.Dial(ctx, bc.websocketUrl) + conn, br, hs, err := timeoutDialer.Dial(ctx, bc.websocketUrl) if errors.Is(err, ErrIncorrectFeedServerVersion) || errors.Is(err, ErrIncorrectChainId) { return nil, err } @@ -312,6 +326,24 @@ func (bc *BroadcastClient) connect(ctx context.Context, nextSeqNum arbutil.Messa return nil, ErrMissingFeedServerVersion } + compressionNegotiated := false + for _, ext := range hs.Extensions { + if ext.Equal(deflateExt) { + compressionNegotiated = true + break + } + } + if !compressionNegotiated && config.EnableCompression { + log.Warn("Compression was not negotiated when connecting to feed server.") + } + if compressionNegotiated && !config.EnableCompression { + err := conn.Close() + if err != nil { + return nil, fmt.Errorf("error closing connection when negotiated disabled extension: %w", err) + } + return nil, errors.New("error dialing feed server: negotiated compression ws extension, but it is disabled") + } + var earlyFrameData io.Reader if br != nil { // Depending on how long the client takes to read the response, there may be @@ -326,6 +358,7 @@ func (bc *BroadcastClient) connect(ctx context.Context, nextSeqNum arbutil.Messa bc.connMutex.Lock() bc.conn = conn + bc.compression = compressionNegotiated bc.connMutex.Unlock() log.Info("Feed connected", "feedServerVersion", feedServerVersion, "chainId", chainId, "requestedSeqNum", nextSeqNum) @@ -349,7 +382,7 @@ func (bc *BroadcastClient) startBackgroundReader(earlyFrameData io.Reader) { var op ws.OpCode var err error config := bc.config() - msg, op, err = wsbroadcastserver.ReadData(ctx, bc.conn, earlyFrameData, config.Timeout, ws.StateClientSide, config.EnableCompression, flateReader) + msg, op, err = wsbroadcastserver.ReadData(ctx, bc.conn, earlyFrameData, config.Timeout, ws.StateClientSide, bc.compression, flateReader) if err != nil { if bc.isShuttingDown() { return diff --git a/broadcastclient/broadcastclient_test.go b/broadcastclient/broadcastclient_test.go index 5ddc59acb6..63ce1dbd84 100644 --- a/broadcastclient/broadcastclient_test.go +++ b/broadcastclient/broadcastclient_test.go @@ -30,43 +30,30 @@ import ( "github.com/offchainlabs/nitro/wsbroadcastserver" ) -func TestReceiveMessagesWithoutCompression(t *testing.T) { +func TestReceiveMessages(t *testing.T) { t.Parallel() - testReceiveMessages(t, false, false, false, false) -} - -func TestReceiveMessagesWithCompression(t *testing.T) { - t.Parallel() - testReceiveMessages(t, true, true, false, false) -} - -func TestReceiveMessagesWithServerOptionalCompression(t *testing.T) { - t.Parallel() - testReceiveMessages(t, true, true, false, false) -} - -func TestReceiveMessagesWithServerOnlyCompression(t *testing.T) { - t.Parallel() - testReceiveMessages(t, false, true, false, false) -} - -func TestReceiveMessagesWithClientOnlyCompression(t *testing.T) { - t.Parallel() - testReceiveMessages(t, true, false, false, false) -} - -func TestReceiveMessagesWithRequiredCompression(t *testing.T) { - t.Parallel() - testReceiveMessages(t, true, true, true, false) -} - -func TestReceiveMessagesWithRequiredCompressionButClientDisabled(t *testing.T) { - t.Parallel() - testReceiveMessages(t, false, true, true, true) + t.Run("withoutCompression", func(t *testing.T) { + testReceiveMessages(t, false, false, false, false) + }) + t.Run("withServerOptionalCompression", func(t *testing.T) { + testReceiveMessages(t, true, true, false, false) + }) + t.Run("withServerOnlyCompression", func(t *testing.T) { + testReceiveMessages(t, false, true, false, false) + }) + t.Run("withClientOnlyCompression", func(t *testing.T) { + testReceiveMessages(t, true, false, false, false) + }) + t.Run("withRequiredCompression", func(t *testing.T) { + testReceiveMessages(t, true, true, true, false) + }) + t.Run("withRequiredCompressionButClientDisabled", func(t *testing.T) { + testReceiveMessages(t, false, true, true, true) + }) } func testReceiveMessages(t *testing.T, clientCompression bool, serverCompression bool, serverRequire bool, expectNoMessagesReceived bool) { - t.Helper() + t.Parallel() ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -105,6 +92,7 @@ func testReceiveMessages(t *testing.T, clientCompression bool, serverCompression go func() { for i := 0; i < messageCount; i++ { + // #nosec G115 Require(t, b.BroadcastSingle(arbostypes.TestMessageWithMetadataAndRequestId, arbutil.MessageIndex(i), nil, nil)) } }() @@ -137,7 +125,11 @@ func TestInvalidSignature(t *testing.T) { badPrivateKey, err := crypto.GenerateKey() Require(t, err) badPublicKey := badPrivateKey.Public() - badSequencerAddr := crypto.PubkeyToAddress(*badPublicKey.(*ecdsa.PublicKey)) + badECDSA, ok := badPublicKey.(*ecdsa.PublicKey) + if !ok { + t.Fatal("badPublicKey is not an ecdsa.PublicKey") + } + badSequencerAddr := crypto.PubkeyToAddress(*badECDSA) config := DefaultTestConfig ts := NewDummyTransactionStreamer(chainId, &badSequencerAddr) @@ -150,12 +142,14 @@ func TestInvalidSignature(t *testing.T) { nil, fatalErrChan, &badSequencerAddr, + t, ) Require(t, err) broadcastClient.Start(ctx) go func() { for i := 0; i < messageCount; i++ { + // #nosec G115 Require(t, b.BroadcastSingle(arbostypes.TestMessageWithMetadataAndRequestId, arbutil.MessageIndex(i), nil, nil)) } }() @@ -199,8 +193,9 @@ func (ts *dummyTransactionStreamer) AddBroadcastMessages(feedMessages []*m.Broad return nil } -func newTestBroadcastClient(config Config, listenerAddress net.Addr, chainId uint64, currentMessageCount arbutil.MessageIndex, txStreamer TransactionStreamerInterface, confirmedSequenceNumberListener chan arbutil.MessageIndex, feedErrChan chan error, validAddr *common.Address) (*BroadcastClient, error) { - port := listenerAddress.(*net.TCPAddr).Port +func newTestBroadcastClient(config Config, listenerAddress net.Addr, chainId uint64, currentMessageCount arbutil.MessageIndex, txStreamer TransactionStreamerInterface, confirmedSequenceNumberListener chan arbutil.MessageIndex, feedErrChan chan error, validAddr *common.Address, t *testing.T) (*BroadcastClient, error) { + t.Helper() + port := testhelpers.AddrTCPPort(listenerAddress, t) var av contracts.AddressVerifierInterface if validAddr != nil { config.Verify.AcceptSequencer = true @@ -223,6 +218,7 @@ func startMakeBroadcastClient(ctx context.Context, t *testing.T, clientConfig Co nil, feedErrChan, sequencerAddr, + t, ) Require(t, err) broadcastClient.Start(ctx) @@ -311,6 +307,7 @@ func TestServerClientDisconnect(t *testing.T) { nil, feedErrChan, &sequencerAddr, + t, ) Require(t, err) broadcastClient.Start(ctx) @@ -382,6 +379,7 @@ func TestBroadcastClientConfirmedMessage(t *testing.T) { confirmedSequenceNumberListener, feedErrChan, &sequencerAddr, + t, ) Require(t, err) broadcastClient.Start(ctx) @@ -454,6 +452,7 @@ func TestServerIncorrectChainId(t *testing.T) { nil, badFeedErrChan, &sequencerAddr, + t, ) Require(t, err) badBroadcastClient.Start(ctx) @@ -513,6 +512,7 @@ func TestServerMissingChainId(t *testing.T) { nil, badFeedErrChan, &sequencerAddr, + t, ) Require(t, err) badBroadcastClient.Start(ctx) @@ -570,6 +570,7 @@ func TestServerIncorrectFeedServerVersion(t *testing.T) { nil, badFeedErrChan, &sequencerAddr, + t, ) Require(t, err) badBroadcastClient.Start(ctx) @@ -629,6 +630,7 @@ func TestServerMissingFeedServerVersion(t *testing.T) { nil, badFeedErrChan, &sequencerAddr, + t, ) Require(t, err) badBroadcastClient.Start(ctx) @@ -680,6 +682,7 @@ func TestBroadcastClientReconnectsOnServerDisconnect(t *testing.T) { nil, feedErrChan, &sequencerAddr, + t, ) Require(t, err) broadcastClient.Start(ctx) @@ -792,6 +795,7 @@ func connectAndGetCachedMessages(ctx context.Context, addr net.Addr, chainId uin nil, feedErrChan, sequencerAddr, + t, ) Require(t, err) broadcastClient.Start(ctx) diff --git a/broadcaster/backlog/backlog.go b/broadcaster/backlog/backlog.go index f6501105c2..0897eedd10 100644 --- a/broadcaster/backlog/backlog.go +++ b/broadcaster/backlog/backlog.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + m "github.com/offchainlabs/nitro/broadcaster/message" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/containers" @@ -97,6 +98,7 @@ func (b *backlog) Append(bm *m.BroadcastMessage) error { if err != nil { log.Warn("error calculating backlogSizeInBytes", "err", err) } else { + // #nosec G115 backlogSizeInBytesGauge.Update(int64(size)) } } @@ -108,6 +110,7 @@ func (b *backlog) Append(bm *m.BroadcastMessage) error { segment = newBacklogSegment() b.head.Store(segment) b.tail.Store(segment) + // #nosec G115 confirmedSequenceNumberGauge.Update(int64(msg.SequenceNumber)) } @@ -143,9 +146,11 @@ func (b *backlog) Append(bm *m.BroadcastMessage) error { } lookupByIndex.Store(uint64(msg.SequenceNumber), segment) b.messageCount.Add(1) + // #nosec G115 backlogSizeInBytesGauge.Inc(int64(msg.Size())) } + // #nosec G115 backlogSizeGauge.Update(int64(b.Count())) return nil } @@ -174,7 +179,7 @@ func (b *backlog) Get(start, end uint64) (*m.BroadcastMessage, error) { } bm := &m.BroadcastMessage{Version: 1} - required := int(end-start) + 1 + required := end - start + 1 for { segMsgs, err := segment.Get(arbmath.MaxInt(start, segment.Start()), arbmath.MinInt(end, segment.End())) if err != nil { @@ -183,7 +188,7 @@ func (b *backlog) Get(start, end uint64) (*m.BroadcastMessage, error) { bm.Messages = append(bm.Messages, segMsgs...) segment = segment.Next() - if len(bm.Messages) == required { + if uint64(len(bm.Messages)) == required { break } else if segment == nil { return nil, errOutOfBounds @@ -213,6 +218,7 @@ func (b *backlog) delete(confirmed uint64) { return } + // #nosec G115 confirmedSequenceNumberGauge.Update(int64(confirmed)) // find the segment containing the confirmed message @@ -323,7 +329,13 @@ func newBacklogSegment() *backlogSegment { func IsBacklogSegmentNil(segment BacklogSegment) bool { if segment == nil { return true - } else if segment.(*backlogSegment) == nil { + } + bs, ok := segment.(*backlogSegment) + if !ok { + log.Error("error in backlogSegment type assertion: clearing backlog") + return false + } + if bs == nil { return true } return false diff --git a/broadcaster/backlog/backlog_test.go b/broadcaster/backlog/backlog_test.go index ee712de9ed..d74389f692 100644 --- a/broadcaster/backlog/backlog_test.go +++ b/broadcaster/backlog/backlog_test.go @@ -33,8 +33,8 @@ func validateBacklog(t *testing.T, b *backlog, count, start, end uint64, lookupK } } - expLen := len(lookupKeys) - actualLen := int(b.Count()) + expLen := uint64(len(lookupKeys)) + actualLen := b.Count() if expLen != actualLen { t.Errorf("expected length of lookupByIndex map (%d) does not equal actual length (%d)", expLen, actualLen) } diff --git a/broadcaster/broadcaster.go b/broadcaster/broadcaster.go index da856f98b4..76e37b879c 100644 --- a/broadcaster/broadcaster.go +++ b/broadcaster/broadcaster.go @@ -107,6 +107,7 @@ func (b *Broadcaster) BroadcastMessages( }() var feedMessages []*m.BroadcastFeedMessage for i, msg := range messagesWithBlockInfo { + // #nosec G115 bfm, err := b.NewBroadcastFeedMessage(msg.MessageWithMeta, seq+arbutil.MessageIndex(i), msg.BlockHash, msg.BlockMetadata) if err != nil { return err @@ -148,6 +149,7 @@ func (b *Broadcaster) ListenerAddr() net.Addr { } func (b *Broadcaster) GetCachedMessageCount() int { + // #nosec G115 return int(b.backlog.Count()) } diff --git a/broadcaster/message/message.go b/broadcaster/message/message.go index b5aae20f2b..b0f439e614 100644 --- a/broadcaster/message/message.go +++ b/broadcaster/message/message.go @@ -2,6 +2,7 @@ package message import ( "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" ) @@ -37,12 +38,13 @@ type BroadcastFeedMessage struct { Message arbostypes.MessageWithMetadata `json:"message"` BlockHash *common.Hash `json:"blockHash,omitempty"` Signature []byte `json:"signature"` - BlockMetadata arbostypes.BlockMetadata `json:"blockMetadata"` + BlockMetadata arbostypes.BlockMetadata `json:"blockMetadata,omitempty"` CumulativeSumMsgSize uint64 `json:"-"` } func (m *BroadcastFeedMessage) Size() uint64 { + // #nosec G115 return uint64(len(m.Signature) + len(m.Message.Message.L2msg) + 160) } diff --git a/broadcaster/message/message_blockmetadata_test.go b/broadcaster/message/message_blockmetadata_test.go index ca51b5bbc0..2f99c2d5b6 100644 --- a/broadcaster/message/message_blockmetadata_test.go +++ b/broadcaster/message/message_blockmetadata_test.go @@ -30,11 +30,13 @@ func TestTimeboostedInDifferentScenarios(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - for txIndex, isTxTimeBoosted := range tc.txs { - if isTxTimeBoosted && !tc.blockMetadata.IsTxTimeboosted(txIndex) { - t.Fatalf("incorrect timeboosted bit for tx of index %d, it should be timeboosted", txIndex) - } else if !isTxTimeBoosted && tc.blockMetadata.IsTxTimeboosted(txIndex) { - t.Fatalf("incorrect timeboosted bit for tx of index %d, it shouldn't be timeboosted", txIndex) + for txIndex, want := range tc.txs { + have, err := tc.blockMetadata.IsTxTimeboosted(txIndex) + if err != nil { + t.Fatalf("error getting timeboosted bit for tx of index %d: %v", txIndex, err) + } + if want != have { + t.Fatalf("incorrect timeboosted bit for tx of index %d, Got: %v, Want: %v", txIndex, have, want) } } }) diff --git a/broadcaster/message/message_serialization_test.go b/broadcaster/message/message_serialization_test.go index 56ffbc191c..bb444cec51 100644 --- a/broadcaster/message/message_serialization_test.go +++ b/broadcaster/message/message_serialization_test.go @@ -10,6 +10,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/arbostypes" ) @@ -77,7 +78,7 @@ func ExampleBroadcastMessage_broadcastfeedmessageWithoutBlockHashAndBlockMetadat encoder := json.NewEncoder(&buf) _ = encoder.Encode(msg) fmt.Println(buf.String()) - // Output: {"version":1,"messages":[{"sequenceNumber":12345,"message":{"message":{"header":{"kind":0,"sender":"0x0000000000000000000000000000000000000000","blockNumber":0,"timestamp":0,"requestId":"0x0000000000000000000000000000000000000000000000000000000000000000","baseFeeL1":0},"l2Msg":"3q2+7w=="},"delayedMessagesRead":3333},"signature":null,"blockMetadata":null}]} + // Output: {"version":1,"messages":[{"sequenceNumber":12345,"message":{"message":{"header":{"kind":0,"sender":"0x0000000000000000000000000000000000000000","blockNumber":0,"timestamp":0,"requestId":"0x0000000000000000000000000000000000000000000000000000000000000000","baseFeeL1":0},"l2Msg":"3q2+7w=="},"delayedMessagesRead":3333},"signature":null}]} } func ExampleBroadcastMessage_emptymessage() { diff --git a/cmd/autonomous-auctioneer/config.go b/cmd/autonomous-auctioneer/config.go index 74ca4340ed..d3f96a8f85 100644 --- a/cmd/autonomous-auctioneer/config.go +++ b/cmd/autonomous-auctioneer/config.go @@ -2,18 +2,19 @@ package main import ( "fmt" - "reflect" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/cmd/conf" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/timeboost" "github.com/offchainlabs/nitro/util/colors" - flag "github.com/spf13/pflag" ) type AutonomousAuctioneerConfig struct { @@ -130,6 +131,9 @@ func (c *AutonomousAuctioneerConfig) GetReloadInterval() time.Duration { } func (c *AutonomousAuctioneerConfig) Validate() error { + if err := c.AuctioneerServer.S3Storage.Validate(); err != nil { + return err + } return nil } diff --git a/cmd/autonomous-auctioneer/main.go b/cmd/autonomous-auctioneer/main.go index e1e540c4a1..eb22d0e177 100644 --- a/cmd/autonomous-auctioneer/main.go +++ b/cmd/autonomous-auctioneer/main.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics/exp" "github.com/ethereum/go-ethereum/node" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util/confighelpers" "github.com/offchainlabs/nitro/timeboost" diff --git a/cmd/bidder-client/main.go b/cmd/bidder-client/main.go index 133b27f498..722717b6bc 100644 --- a/cmd/bidder-client/main.go +++ b/cmd/bidder-client/main.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/cmd/util/confighelpers" "github.com/offchainlabs/nitro/timeboost" ) diff --git a/cmd/chaininfo/arbitrum_chain_info.json b/cmd/chaininfo/arbitrum_chain_info.json index 524433a7b5..d0da391cf8 100644 --- a/cmd/chaininfo/arbitrum_chain_info.json +++ b/cmd/chaininfo/arbitrum_chain_info.json @@ -44,6 +44,7 @@ "sequencer-inbox": "0x1c479675ad559dc151f6ec7ed3fbf8cee79582b6", "validator-utils": "0x9e40625f52829cf04bc4839f186d621ee33b0e67", "validator-wallet-creator": "0x960953f7c69cd2bc2322db9223a815c680ccc7ea", + "stake-token": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "deployed-at": 15411056 } }, @@ -90,6 +91,7 @@ "sequencer-inbox": "0x211e1c4c7f1bf5351ac850ed10fd68cffcf6c21b", "validator-utils": "0x2B081fbaB646D9013f2699BebEf62B7e7d7F0976", "validator-wallet-creator": "0xe05465Aab36ba1277dAE36aa27a7B74830e74DE4", + "stake-token": "0x765277eebeca2e31912c9946eae1021199b39c61", "deployed-at": 15016829 } }, @@ -164,7 +166,7 @@ "EnableArbOS": true, "AllowDebugPrecompiles": true, "DataAvailabilityCommittee": false, - "InitialArbOSVersion": 31, + "InitialArbOSVersion": 32, "InitialChainOwner": "0x0000000000000000000000000000000000000000", "GenesisBlockNum": 0 } @@ -196,7 +198,7 @@ "EnableArbOS": true, "AllowDebugPrecompiles": true, "DataAvailabilityCommittee": true, - "InitialArbOSVersion": 31, + "InitialArbOSVersion": 32, "InitialChainOwner": "0x0000000000000000000000000000000000000000", "GenesisBlockNum": 0 } @@ -245,6 +247,7 @@ "rollup": "0xd80810638dbDF9081b72C1B33c65375e807281C8", "validator-utils": "0x1f6860C3cac255fFFa72B7410b1183c3a0D261e0", "validator-wallet-creator": "0x894fC71fA0A666352824EC954B401573C861D664", + "stake-token": "0xefb383126640fe4a760010c6e59c397d2b6c7141", "deployed-at": 4139226 } }, diff --git a/cmd/chaininfo/chain_defaults.go b/cmd/chaininfo/chain_defaults.go new file mode 100644 index 0000000000..a69472cafc --- /dev/null +++ b/cmd/chaininfo/chain_defaults.go @@ -0,0 +1,141 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package chaininfo + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/params" +) + +var DefaultChainConfigs map[string]*params.ChainConfig + +func init() { + var chainsInfo []ChainInfo + err := json.Unmarshal(DefaultChainsInfoBytes, &chainsInfo) + if err != nil { + panic(fmt.Errorf("error initializing default chainsInfo: %w", err)) + } + if len(chainsInfo) == 0 { + panic("Default chainsInfo is empty") + } + DefaultChainConfigs = make(map[string]*params.ChainConfig) + for _, chainInfo := range chainsInfo { + DefaultChainConfigs[chainInfo.ChainName] = chainInfo.ChainConfig + } +} + +func CopyArbitrumChainParams(arbChainParams params.ArbitrumChainParams) params.ArbitrumChainParams { + return params.ArbitrumChainParams{ + EnableArbOS: arbChainParams.EnableArbOS, + AllowDebugPrecompiles: arbChainParams.AllowDebugPrecompiles, + DataAvailabilityCommittee: arbChainParams.DataAvailabilityCommittee, + InitialArbOSVersion: arbChainParams.InitialArbOSVersion, + InitialChainOwner: arbChainParams.InitialChainOwner, + GenesisBlockNum: arbChainParams.GenesisBlockNum, + MaxCodeSize: arbChainParams.MaxCodeSize, + MaxInitCodeSize: arbChainParams.MaxInitCodeSize, + } +} + +func CopyChainConfig(chainConfig *params.ChainConfig) *params.ChainConfig { + copy := ¶ms.ChainConfig{ + DAOForkSupport: chainConfig.DAOForkSupport, + ArbitrumChainParams: CopyArbitrumChainParams(chainConfig.ArbitrumChainParams), + Clique: ¶ms.CliqueConfig{ + Period: chainConfig.Clique.Period, + Epoch: chainConfig.Clique.Epoch, + }, + } + if chainConfig.ChainID != nil { + copy.ChainID = new(big.Int).Set(chainConfig.ChainID) + } + if chainConfig.HomesteadBlock != nil { + copy.HomesteadBlock = new(big.Int).Set(chainConfig.HomesteadBlock) + } + if chainConfig.DAOForkBlock != nil { + copy.DAOForkBlock = new(big.Int).Set(chainConfig.DAOForkBlock) + } + if chainConfig.EIP150Block != nil { + copy.EIP150Block = new(big.Int).Set(chainConfig.EIP150Block) + } + if chainConfig.EIP155Block != nil { + copy.EIP155Block = new(big.Int).Set(chainConfig.EIP155Block) + } + if chainConfig.EIP158Block != nil { + copy.EIP158Block = new(big.Int).Set(chainConfig.EIP158Block) + } + if chainConfig.ByzantiumBlock != nil { + copy.ByzantiumBlock = new(big.Int).Set(chainConfig.ByzantiumBlock) + } + if chainConfig.ConstantinopleBlock != nil { + copy.ConstantinopleBlock = new(big.Int).Set(chainConfig.ConstantinopleBlock) + } + if chainConfig.PetersburgBlock != nil { + copy.PetersburgBlock = new(big.Int).Set(chainConfig.PetersburgBlock) + } + if chainConfig.IstanbulBlock != nil { + copy.IstanbulBlock = new(big.Int).Set(chainConfig.IstanbulBlock) + } + if chainConfig.MuirGlacierBlock != nil { + copy.MuirGlacierBlock = new(big.Int).Set(chainConfig.MuirGlacierBlock) + } + if chainConfig.BerlinBlock != nil { + copy.BerlinBlock = new(big.Int).Set(chainConfig.BerlinBlock) + } + if chainConfig.LondonBlock != nil { + copy.LondonBlock = new(big.Int).Set(chainConfig.LondonBlock) + } + return copy +} + +func fetchArbitrumChainParams(chainName string) params.ArbitrumChainParams { + originalConfig, ok := DefaultChainConfigs[chainName] + if !ok { + panic(fmt.Sprintf("%s chain config not found in DefaultChainConfigs", chainName)) + } + return CopyArbitrumChainParams(originalConfig.ArbitrumChainParams) +} + +func ArbitrumOneParams() params.ArbitrumChainParams { + return fetchArbitrumChainParams("arb1") +} +func ArbitrumNovaParams() params.ArbitrumChainParams { + return fetchArbitrumChainParams("nova") +} +func ArbitrumRollupGoerliTestnetParams() params.ArbitrumChainParams { + return fetchArbitrumChainParams("goerli-rollup") +} +func ArbitrumDevTestParams() params.ArbitrumChainParams { + return fetchArbitrumChainParams("arb-dev-test") +} +func ArbitrumDevTestDASParams() params.ArbitrumChainParams { + return fetchArbitrumChainParams("anytrust-dev-test") +} + +func fetchChainConfig(chainName string) *params.ChainConfig { + originalConfig, ok := DefaultChainConfigs[chainName] + if !ok { + panic(fmt.Sprintf("%s chain config not found in DefaultChainConfigs", chainName)) + } + return CopyChainConfig(originalConfig) +} + +func ArbitrumOneChainConfig() *params.ChainConfig { + return fetchChainConfig("arb1") +} +func ArbitrumNovaChainConfig() *params.ChainConfig { + return fetchChainConfig("nova") +} +func ArbitrumRollupGoerliTestnetChainConfig() *params.ChainConfig { + return fetchChainConfig("goerli-rollup") +} +func ArbitrumDevTestChainConfig() *params.ChainConfig { + return fetchChainConfig("arb-dev-test") +} +func ArbitrumDevTestDASChainConfig() *params.ChainConfig { + return fetchChainConfig("anytrust-dev-test") +} diff --git a/cmd/chaininfo/chain_defaults_test.go b/cmd/chaininfo/chain_defaults_test.go new file mode 100644 index 0000000000..a19e5849a6 --- /dev/null +++ b/cmd/chaininfo/chain_defaults_test.go @@ -0,0 +1,17 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package chaininfo + +import ( + "reflect" + "testing" +) + +func TestDefaultChainConfigsCopyCorrectly(t *testing.T) { + for _, chainName := range []string{"arb1", "nova", "goerli-rollup", "arb-dev-test", "anytrust-dev-test"} { + if !reflect.DeepEqual(DefaultChainConfigs[chainName], fetchChainConfig(chainName)) { + t.Fatalf("copy of %s default chain config mismatch", chainName) + } + } +} diff --git a/cmd/chaininfo/chain_info.go b/cmd/chaininfo/chain_info.go index 13e586ced2..35f28bebb9 100644 --- a/cmd/chaininfo/chain_info.go +++ b/cmd/chaininfo/chain_info.go @@ -16,7 +16,7 @@ import ( ) //go:embed arbitrum_chain_info.json -var DefaultChainInfo []byte +var DefaultChainsInfoBytes []byte type ChainInfo struct { ChainName string `json:"chain-name"` @@ -80,7 +80,7 @@ func ProcessChainInfo(chainId uint64, chainName string, l2ChainInfoFiles []strin } } - chainInfo, err := findChainInfo(chainId, chainName, DefaultChainInfo) + chainInfo, err := findChainInfo(chainId, chainName, DefaultChainsInfoBytes) if err != nil || chainInfo != nil { return chainInfo, err } @@ -120,5 +120,6 @@ type RollupAddresses struct { UpgradeExecutor common.Address `json:"upgrade-executor"` ValidatorUtils common.Address `json:"validator-utils"` ValidatorWalletCreator common.Address `json:"validator-wallet-creator"` + StakeToken common.Address `json:"stake-token"` DeployedAt uint64 `json:"deployed-at"` } diff --git a/cmd/conf/chain.go b/cmd/conf/chain.go index b85f7727b1..435246e357 100644 --- a/cmd/conf/chain.go +++ b/cmd/conf/chain.go @@ -6,10 +6,11 @@ package conf import ( "time" + flag "github.com/spf13/pflag" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/rpcclient" - flag "github.com/spf13/pflag" ) type ParentChainConfig struct { @@ -52,23 +53,19 @@ func (c *ParentChainConfig) Validate() error { } type L2Config struct { - ID uint64 `koanf:"id"` - Name string `koanf:"name"` - InfoFiles []string `koanf:"info-files"` - InfoJson string `koanf:"info-json"` - DevWallet genericconf.WalletConfig `koanf:"dev-wallet"` - InfoIpfsUrl string `koanf:"info-ipfs-url"` - InfoIpfsDownloadPath string `koanf:"info-ipfs-download-path"` + ID uint64 `koanf:"id"` + Name string `koanf:"name"` + InfoFiles []string `koanf:"info-files"` + InfoJson string `koanf:"info-json"` + DevWallet genericconf.WalletConfig `koanf:"dev-wallet"` } var L2ConfigDefault = L2Config{ - ID: 0, - Name: "", - InfoFiles: []string{}, // Default file used is chaininfo/arbitrum_chain_info.json, stored in DefaultChainInfo in chain_info.go - InfoJson: "", - DevWallet: genericconf.WalletConfigDefault, - InfoIpfsUrl: "", - InfoIpfsDownloadPath: "/tmp/", + ID: 0, + Name: "", + InfoFiles: []string{}, // Default file used is chaininfo/arbitrum_chain_info.json, stored in DefaultChainInfo in chain_info.go + InfoJson: "", + DevWallet: genericconf.WalletConfigDefault, } func L2ConfigAddOptions(prefix string, f *flag.FlagSet) { @@ -79,9 +76,6 @@ func L2ConfigAddOptions(prefix string, f *flag.FlagSet) { // Dev wallet does not exist unless specified genericconf.WalletConfigAddOptions(prefix+".dev-wallet", f, "") - f.String(prefix+".info-ipfs-url", L2ConfigDefault.InfoIpfsUrl, "url to download chain info file") - f.String(prefix+".info-ipfs-download-path", L2ConfigDefault.InfoIpfsDownloadPath, "path to save temp downloaded file") - } func (c *L2Config) ResolveDirectoryNames(chain string) { diff --git a/cmd/conf/database.go b/cmd/conf/database.go index af18bacd57..8d05c44500 100644 --- a/cmd/conf/database.go +++ b/cmd/conf/database.go @@ -12,8 +12,9 @@ import ( "runtime" "time" - "github.com/ethereum/go-ethereum/ethdb/pebble" flag "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/ethdb/pebble" ) type PersistentConfig struct { @@ -111,16 +112,19 @@ func (c *PersistentConfig) Validate() error { } type PebbleConfig struct { + SyncMode bool `koanf:"sync-mode"` MaxConcurrentCompactions int `koanf:"max-concurrent-compactions"` Experimental PebbleExperimentalConfig `koanf:"experimental"` } var PebbleConfigDefault = PebbleConfig{ + SyncMode: false, // use NO-SYNC mode, see: https://github.com/ethereum/go-ethereum/issues/29819 MaxConcurrentCompactions: runtime.NumCPU(), Experimental: PebbleExperimentalConfigDefault, } func PebbleConfigAddOptions(prefix string, f *flag.FlagSet, defaultConfig *PebbleConfig) { + f.Bool(prefix+".sync-mode", defaultConfig.SyncMode, "if true sync mode is used (data needs to be written to WAL before the write is marked as completed)") f.Int(prefix+".max-concurrent-compactions", defaultConfig.MaxConcurrentCompactions, "maximum number of concurrent compactions") PebbleExperimentalConfigAddOptions(prefix+".experimental", f, &defaultConfig.Experimental) } @@ -179,7 +183,7 @@ var PebbleExperimentalConfigDefault = PebbleExperimentalConfig{ BlockSize: 4 << 10, // 4 KB IndexBlockSize: 4 << 10, // 4 KB TargetFileSize: 2 << 20, // 2 MB - TargetFileSizeEqualLevels: true, + TargetFileSizeEqualLevels: false, L0CompactionConcurrency: 10, CompactionDebtConcurrency: 1 << 30, // 1GB @@ -250,6 +254,7 @@ func (c *PebbleConfig) ExtraOptions(namespace string) *pebble.ExtraOptions { walDir = path.Join(walDir, namespace) } return &pebble.ExtraOptions{ + SyncMode: c.SyncMode, BytesPerSync: c.Experimental.BytesPerSync, L0CompactionFileThreshold: c.Experimental.L0CompactionFileThreshold, L0CompactionThreshold: c.Experimental.L0CompactionThreshold, diff --git a/cmd/conf/init.go b/cmd/conf/init.go index d88bcdd241..74bd89fd16 100644 --- a/cmd/conf/init.go +++ b/cmd/conf/init.go @@ -6,8 +6,9 @@ import ( "strings" "time" - "github.com/ethereum/go-ethereum/log" "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/log" ) type InitConfig struct { @@ -20,17 +21,20 @@ type InitConfig struct { DownloadPoll time.Duration `koanf:"download-poll"` DevInit bool `koanf:"dev-init"` DevInitAddress string `koanf:"dev-init-address"` + DevMaxCodeSize uint64 `koanf:"dev-max-code-size"` DevInitBlockNum uint64 `koanf:"dev-init-blocknum"` Empty bool `koanf:"empty"` + ImportWasm bool `koanf:"import-wasm"` AccountsPerSync uint `koanf:"accounts-per-sync"` ImportFile string `koanf:"import-file"` + GenesisJsonFile string `koanf:"genesis-json-file"` ThenQuit bool `koanf:"then-quit"` Prune string `koanf:"prune"` PruneBloomSize uint64 `koanf:"prune-bloom-size"` PruneThreads int `koanf:"prune-threads"` PruneTrieCleanCache int `koanf:"prune-trie-clean-cache"` RecreateMissingStateFrom uint64 `koanf:"recreate-missing-state-from"` - RebuildLocalWasm bool `koanf:"rebuild-local-wasm"` + RebuildLocalWasm string `koanf:"rebuild-local-wasm"` ReorgToBatch int64 `koanf:"reorg-to-batch"` ReorgToMessageBatch int64 `koanf:"reorg-to-message-batch"` ReorgToBlockBatch int64 `koanf:"reorg-to-block-batch"` @@ -46,9 +50,12 @@ var InitConfigDefault = InitConfig{ DownloadPoll: time.Minute, DevInit: false, DevInitAddress: "", + DevMaxCodeSize: 0, DevInitBlockNum: 0, Empty: false, + ImportWasm: false, ImportFile: "", + GenesisJsonFile: "", AccountsPerSync: 100000, ThenQuit: false, Prune: "", @@ -56,7 +63,7 @@ var InitConfigDefault = InitConfig{ PruneThreads: runtime.NumCPU(), PruneTrieCleanCache: 600, RecreateMissingStateFrom: 0, // 0 = disabled - RebuildLocalWasm: true, + RebuildLocalWasm: "auto", ReorgToBatch: -1, ReorgToMessageBatch: -1, ReorgToBlockBatch: -1, @@ -73,19 +80,26 @@ func InitConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Bool(prefix+".dev-init", InitConfigDefault.DevInit, "init with dev data (1 account with balance) instead of file import") f.String(prefix+".dev-init-address", InitConfigDefault.DevInitAddress, "Address of dev-account. Leave empty to use the dev-wallet.") f.Uint64(prefix+".dev-init-blocknum", InitConfigDefault.DevInitBlockNum, "Number of preinit blocks. Must exist in ancient database.") + f.Uint64(prefix+".dev-max-code-size", InitConfigDefault.DevMaxCodeSize, "Max code size for dev accounts") f.Bool(prefix+".empty", InitConfigDefault.Empty, "init with empty state") + f.Bool(prefix+".import-wasm", InitConfigDefault.ImportWasm, "if set, import the wasm directory when downloading a database (contains executable code - only use with highly trusted source)") f.Bool(prefix+".then-quit", InitConfigDefault.ThenQuit, "quit after init is done") f.String(prefix+".import-file", InitConfigDefault.ImportFile, "path for json data to import") + f.String(prefix+".genesis-json-file", InitConfigDefault.GenesisJsonFile, "path for genesis json file") f.Uint(prefix+".accounts-per-sync", InitConfigDefault.AccountsPerSync, "during init - sync database every X accounts. Lower value for low-memory systems. 0 disables.") f.String(prefix+".prune", InitConfigDefault.Prune, "pruning for a given use: \"full\" for full nodes serving RPC requests, or \"validator\" for validators") f.Uint64(prefix+".prune-bloom-size", InitConfigDefault.PruneBloomSize, "the amount of memory in megabytes to use for the pruning bloom filter (higher values prune better)") f.Int(prefix+".prune-threads", InitConfigDefault.PruneThreads, "the number of threads to use when pruning") f.Int(prefix+".prune-trie-clean-cache", InitConfigDefault.PruneTrieCleanCache, "amount of memory in megabytes to cache unchanged state trie nodes with when traversing state database during pruning") f.Uint64(prefix+".recreate-missing-state-from", InitConfigDefault.RecreateMissingStateFrom, "block number to start recreating missing states from (0 = disabled)") - f.Bool(prefix+".rebuild-local-wasm", InitConfigDefault.RebuildLocalWasm, "rebuild local wasm database on boot if needed (otherwise-will be done lazily)") f.Int64(prefix+".reorg-to-batch", InitConfigDefault.ReorgToBatch, "rolls back the blockchain to a specified batch number") f.Int64(prefix+".reorg-to-message-batch", InitConfigDefault.ReorgToMessageBatch, "rolls back the blockchain to the first batch at or before a given message index") f.Int64(prefix+".reorg-to-block-batch", InitConfigDefault.ReorgToBlockBatch, "rolls back the blockchain to the first batch at or before a given block number") + f.String(prefix+".rebuild-local-wasm", InitConfigDefault.RebuildLocalWasm, "rebuild local wasm database on boot if needed (otherwise-will be done lazily). Three modes are supported \n"+ + "\"auto\"- (enabled by default) if any previous rebuilding attempt was successful then rebuilding is disabled else continues to rebuild,\n"+ + "\"force\"- force rebuilding which would commence rebuilding despite the status of previous attempts,\n"+ + "\"false\"- do not rebuild on startup", + ) } func (c *InitConfig) Validate() error { @@ -110,6 +124,10 @@ func (c *InitConfig) Validate() error { } } } + c.RebuildLocalWasm = strings.ToLower(c.RebuildLocalWasm) + if c.RebuildLocalWasm != "auto" && c.RebuildLocalWasm != "force" && c.RebuildLocalWasm != "false" { + return fmt.Errorf("invalid value of rebuild-local-wasm, want: auto or force or false, got: %s", c.RebuildLocalWasm) + } return nil } diff --git a/cmd/dataavailability/data_availability_check.go b/cmd/dataavailability/data_availability_check.go index d80c0475bf..e961215925 100644 --- a/cmd/dataavailability/data_availability_check.go +++ b/cmd/dataavailability/data_availability_check.go @@ -13,6 +13,8 @@ import ( "syscall" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -27,8 +29,6 @@ import ( "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/util/metricsutil" "github.com/offchainlabs/nitro/util/stopwaiter" - - flag "github.com/spf13/pflag" ) // Data availability check is done to as to make sure that the data that is being stored by DAS is available at all time. diff --git a/cmd/datool/datool.go b/cmd/datool/datool.go index ba60cbbd4d..06f94dc952 100644 --- a/cmd/datool/datool.go +++ b/cmd/datool/datool.go @@ -22,10 +22,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util" - "github.com/offchainlabs/nitro/cmd/util/confighelpers" "github.com/offchainlabs/nitro/das" "github.com/offchainlabs/nitro/das/dastree" @@ -166,8 +166,10 @@ func startClientStore(args []string) error { if err != nil { return err } + // #nosec G115 cert, err = client.Store(ctx, message, uint64(time.Now().Add(config.DASRetentionPeriod).Unix())) } else if len(config.Message) > 0 { + // #nosec G115 cert, err = client.Store(ctx, []byte(config.Message), uint64(time.Now().Add(config.DASRetentionPeriod).Unix())) } else { return errors.New("--message or --random-message-size must be specified") @@ -363,6 +365,7 @@ func dumpKeyset(args []string) error { return err } + // #nosec G115 keysetHash, keysetBytes, err := das.KeysetHashFromServices(services, uint64(config.Keyset.AssumedHonest)) if err != nil { return err diff --git a/cmd/dbconv/dbconv/config.go b/cmd/dbconv/dbconv/config.go index 917f34261d..fdebda2d54 100644 --- a/cmd/dbconv/dbconv/config.go +++ b/cmd/dbconv/dbconv/config.go @@ -4,9 +4,10 @@ import ( "errors" "fmt" + flag "github.com/spf13/pflag" + "github.com/offchainlabs/nitro/cmd/conf" "github.com/offchainlabs/nitro/cmd/genericconf" - flag "github.com/spf13/pflag" ) type DBConfig struct { diff --git a/cmd/dbconv/dbconv/dbconv.go b/cmd/dbconv/dbconv/dbconv.go index 6a97df31c0..fdba1907c2 100644 --- a/cmd/dbconv/dbconv/dbconv.go +++ b/cmd/dbconv/dbconv/dbconv.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/dbutil" ) diff --git a/cmd/dbconv/main.go b/cmd/dbconv/main.go index 2d61c96552..f5aaced40b 100644 --- a/cmd/dbconv/main.go +++ b/cmd/dbconv/main.go @@ -6,13 +6,15 @@ import ( "os" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics/exp" + "github.com/offchainlabs/nitro/cmd/dbconv/dbconv" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util/confighelpers" - flag "github.com/spf13/pflag" ) func parseDBConv(args []string) (*dbconv.DBConvConfig, error) { diff --git a/cmd/deploy/deploy.go b/cmd/deploy/deploy.go index d8c0aeeac4..a597799b06 100644 --- a/cmd/deploy/deploy.go +++ b/cmd/deploy/deploy.go @@ -14,20 +14,20 @@ import ( "strings" "time" - "github.com/offchainlabs/nitro/cmd/chaininfo" - "github.com/offchainlabs/nitro/cmd/genericconf" - "github.com/offchainlabs/nitro/solgen/go/precompilesgen" - "github.com/offchainlabs/nitro/util/headerreader" - "github.com/offchainlabs/nitro/validator/server_common" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util" deploycode "github.com/offchainlabs/nitro/deploy" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/headerreader" + "github.com/offchainlabs/nitro/validator/server_common" ) func main() { @@ -61,7 +61,6 @@ func main() { authorizevalidators := flag.Uint64("authorizevalidators", 0, "Number of validators to preemptively authorize") txTimeout := flag.Duration("txtimeout", 10*time.Minute, "Timeout when waiting for a transaction to be included in a block") prod := flag.Bool("prod", false, "Whether to configure the rollup for production or testing") - isUsingFeeToken := flag.Bool("isUsingFeeToken", false, "true if the chain uses custom fee token") flag.Parse() l1ChainId := new(big.Int).SetUint64(*l1ChainIdUint) maxDataSize := new(big.Int).SetUint64(*maxDataSizeUint) @@ -180,7 +179,7 @@ func main() { defer l1Reader.StopAndWait() nativeToken := common.HexToAddress(*nativeTokenAddressString) - deployedAddresses, err := deploycode.DeployOnL1( + deployedAddresses, err := deploycode.DeployOnParentChain( ctx, l1Reader, l1TransactionOpts, @@ -190,7 +189,7 @@ func main() { arbnode.GenerateRollupConfig(*prod, moduleRoot, ownerAddress, &chainConfig, chainConfigJson, loserEscrowAddress), nativeToken, maxDataSize, - *isUsingFeeToken, + true, ) if err != nil { flag.Usage() diff --git a/cmd/genericconf/config.go b/cmd/genericconf/config.go index 06e1fcd12d..408ba9a552 100644 --- a/cmd/genericconf/config.go +++ b/cmd/genericconf/config.go @@ -6,12 +6,13 @@ package genericconf import ( "errors" "io" + "log/slog" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" - flag "github.com/spf13/pflag" - "golang.org/x/exp/slog" ) type ConfConfig struct { diff --git a/cmd/genericconf/filehandler_test.go b/cmd/genericconf/filehandler_test.go index daa9ed397c..7d24fbb69f 100644 --- a/cmd/genericconf/filehandler_test.go +++ b/cmd/genericconf/filehandler_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -55,7 +56,11 @@ func readLogMessagesFromJSONFile(t *testing.T, path string) ([]string, error) { if !ok { testhelpers.FailImpl(t, "Incorrect record, msg key is missing", "record", record) } - messages = append(messages, msg.(string)) + msgString, ok := msg.(string) + if !ok { + testhelpers.FailImpl(t, "Incorrect record, msg is not a string", "record", record) + } + messages = append(messages, msgString) } if errors.Is(err, io.EOF) { return messages, nil diff --git a/cmd/genericconf/liveconfig.go b/cmd/genericconf/liveconfig.go index 1054140e9e..f256fe6612 100644 --- a/cmd/genericconf/liveconfig.go +++ b/cmd/genericconf/liveconfig.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/stopwaiter" ) diff --git a/cmd/genericconf/logging.go b/cmd/genericconf/logging.go index fa45953278..4cdaa5f3e8 100644 --- a/cmd/genericconf/logging.go +++ b/cmd/genericconf/logging.go @@ -8,8 +8,9 @@ import ( "os" "sync" - "github.com/ethereum/go-ethereum/log" "gopkg.in/natefinch/lumberjack.v2" + + "github.com/ethereum/go-ethereum/log" ) var globalFileLoggerFactory = fileLoggerFactory{} diff --git a/cmd/genericconf/loglevel.go b/cmd/genericconf/loglevel.go index f7ad05a2cc..79cba22439 100644 --- a/cmd/genericconf/loglevel.go +++ b/cmd/genericconf/loglevel.go @@ -5,11 +5,11 @@ package genericconf import ( "errors" + "log/slog" "strconv" "strings" "github.com/ethereum/go-ethereum/log" - "golang.org/x/exp/slog" ) func ToSlogLevel(str string) (slog.Level, error) { diff --git a/cmd/genericconf/pprof.go b/cmd/genericconf/pprof.go index 9fd3a6f2a4..0bde03decd 100644 --- a/cmd/genericconf/pprof.go +++ b/cmd/genericconf/pprof.go @@ -3,7 +3,6 @@ package genericconf import ( "fmt" "net/http" - // Blank import pprof registers its HTTP handlers. _ "net/http/pprof" // #nosec G108 diff --git a/cmd/ipfshelper/ipfshelper.bkup_go b/cmd/ipfshelper/ipfshelper.bkup_go deleted file mode 100644 index ccde492ca6..0000000000 --- a/cmd/ipfshelper/ipfshelper.bkup_go +++ /dev/null @@ -1,281 +0,0 @@ -//go:build ipfs -// +build ipfs - -package ipfshelper - -import ( - "context" - "fmt" - "io" - "math/rand" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/ethereum/go-ethereum/log" - "github.com/ipfs/go-libipfs/files" - coreiface "github.com/ipfs/interface-go-ipfs-core" - "github.com/ipfs/interface-go-ipfs-core/options" - "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/config" - "github.com/ipfs/kubo/core" - "github.com/ipfs/kubo/core/coreapi" - "github.com/ipfs/kubo/core/node/libp2p" - "github.com/ipfs/kubo/plugin/loader" - "github.com/ipfs/kubo/repo" - "github.com/ipfs/kubo/repo/fsrepo" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - ma "github.com/multiformats/go-multiaddr" -) - -const DefaultIpfsProfiles = "" - -type IpfsHelper struct { - api coreiface.CoreAPI - node *core.IpfsNode - cfg *config.Config - repoPath string - repo repo.Repo -} - -func (h *IpfsHelper) createRepo(downloadPath string, profiles string) error { - fileInfo, err := os.Stat(downloadPath) - if err != nil { - return fmt.Errorf("failed to stat ipfs repo directory: %w", err) - } - if !fileInfo.IsDir() { - return fmt.Errorf("%s is not a directory", downloadPath) - } - h.repoPath = filepath.Join(downloadPath, "ipfs-repo") - // Create a config with default options and a 2048 bit key - h.cfg, err = config.Init(io.Discard, 2048) - if err != nil { - return err - } - if len(profiles) > 0 { - for _, profile := range strings.Split(profiles, ",") { - transformer, ok := config.Profiles[profile] - if !ok { - return fmt.Errorf("invalid ipfs configuration profile: %s", profile) - } - - if err := transformer.Transform(h.cfg); err != nil { - return err - } - } - } - // Create the repo with the config - // fsrepo.Init initializes new repo only if it's not initialized yet - err = fsrepo.Init(h.repoPath, h.cfg) - if err != nil { - return fmt.Errorf("failed to init ipfs repo: %w", err) - } - h.repo, err = fsrepo.Open(h.repoPath) - if err != nil { - return fmt.Errorf("failed to open ipfs repo: %w", err) - } - return nil -} - -func (h *IpfsHelper) createNode(ctx context.Context, clientOnly bool) error { - var routing libp2p.RoutingOption - if clientOnly { - routing = libp2p.DHTClientOption - } else { - routing = libp2p.DHTOption - } - nodeOptions := &core.BuildCfg{ - Online: true, - Routing: routing, - Repo: h.repo, - } - var err error - h.node, err = core.NewNode(ctx, nodeOptions) - if err != nil { - return err - } - h.api, err = coreapi.NewCoreAPI(h.node) - return err -} - -func (h *IpfsHelper) connectToPeers(ctx context.Context, peers []string) error { - peerInfos := make(map[peer.ID]*peer.AddrInfo, len(peers)) - for _, addressString := range peers { - address, err := ma.NewMultiaddr(addressString) - if err != nil { - return err - } - addressInfo, err := peer.AddrInfoFromP2pAddr(address) - if err != nil { - return err - } - peerInfo, ok := peerInfos[addressInfo.ID] - if !ok { - peerInfo = &peer.AddrInfo{ID: addressInfo.ID} - peerInfos[peerInfo.ID] = peerInfo - } - peerInfo.Addrs = append(peerInfo.Addrs, addressInfo.Addrs...) - } - var wg sync.WaitGroup - wg.Add(len(peerInfos)) - for _, peerInfo := range peerInfos { - go func(peerInfo *peer.AddrInfo) { - defer wg.Done() - err := h.api.Swarm().Connect(ctx, *peerInfo) - if err != nil { - log.Warn("failed to connect to peer", "peerId", peerInfo.ID, "err", err) - return - } - }(peerInfo) - } - wg.Wait() - return nil -} - -func (h *IpfsHelper) GetPeerHostAddresses() ([]string, error) { - addresses, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(h.node.PeerHost)) - if err != nil { - return []string{}, err - } - addressesStrings := make([]string, len(addresses)) - for i, a := range addresses { - addressesStrings[i] = a.String() - } - return addressesStrings, nil -} - -func normalizeCidString(cidString string) string { - if strings.HasPrefix(cidString, "ipfs://") { - return "/ipfs/" + cidString[7:] - } - if strings.HasPrefix(cidString, "ipns://") { - return "/ipns/" + cidString[7:] - } - return cidString -} - -func (h *IpfsHelper) DownloadFile(ctx context.Context, cidString string, destinationDir string) (string, error) { - cidString = normalizeCidString(cidString) - cidPath := path.New(cidString) - resolvedPath, err := h.api.ResolvePath(ctx, cidPath) - if err != nil { - return "", fmt.Errorf("failed to resolve path: %w", err) - } - // first pin the root node, then all its children nodes in random order to improve sharing with peers started at the same time - if err := h.api.Pin().Add(ctx, resolvedPath, options.Pin.Recursive(false)); err != nil { - return "", fmt.Errorf("failed to pin root path: %w", err) - } - links, err := h.api.Object().Links(ctx, resolvedPath) - if err != nil { - return "", fmt.Errorf("failed to get root links: %w", err) - } - log.Info("Pinning ipfs subtrees...") - printProgress := func(done int, all int) { - if all == 0 { - all = 1 // avoid division by 0 - done = 1 - } - fmt.Printf("\033[2K\rPinned %d / %d subtrees (%.2f%%)", done, all, float32(done)/float32(all)*100) - } - permutation := rand.Perm(len(links)) - printProgress(0, len(links)) - for i, j := range permutation { - link := links[j] - if err := h.api.Pin().Add(ctx, path.IpfsPath(link.Cid), options.Pin.Recursive(true)); err != nil { - return "", fmt.Errorf("failed to pin child path: %w", err) - } - printProgress(i+1, len(links)) - } - fmt.Printf("\n") - rootNodeDirectory, err := h.api.Unixfs().Get(ctx, cidPath) - if err != nil { - return "", fmt.Errorf("could not get file with CID: %w", err) - } - log.Info("Writing file...") - outputFilePath := filepath.Join(destinationDir, resolvedPath.Cid().String()) - _ = os.Remove(outputFilePath) - err = files.WriteTo(rootNodeDirectory, outputFilePath) - if err != nil { - return "", fmt.Errorf("could not write out the fetched CID: %w", err) - } - log.Info("Download done.") - return outputFilePath, nil -} - -func (h *IpfsHelper) AddFile(ctx context.Context, filePath string, includeHidden bool) (path.Resolved, error) { - fileInfo, err := os.Stat(filePath) - if err != nil { - return nil, err - } - fileNode, err := files.NewSerialFile(filePath, includeHidden, fileInfo) - if err != nil { - return nil, err - } - return h.api.Unixfs().Add(ctx, fileNode) -} - -func CreateIpfsHelper(ctx context.Context, downloadPath string, clientOnly bool, peerList []string, profiles string) (*IpfsHelper, error) { - return createIpfsHelperImpl(ctx, downloadPath, clientOnly, peerList, profiles) -} - -func (h *IpfsHelper) Close() error { - return h.node.Close() -} - -func setupPlugins() error { - plugins, err := loader.NewPluginLoader("") - if err != nil { - return fmt.Errorf("error loading plugins: %w", err) - } - // Load preloaded and external plugins - if err := plugins.Initialize(); err != nil { - return fmt.Errorf("error initializing plugins: %w", err) - } - if err := plugins.Inject(); err != nil { - return fmt.Errorf("error initializing plugins: %w", err) - } - return nil -} - -var loadPluginsOnce sync.Once - -func createIpfsHelperImpl(ctx context.Context, downloadPath string, clientOnly bool, peerList []string, profiles string) (*IpfsHelper, error) { - var onceErr error - loadPluginsOnce.Do(func() { - onceErr = setupPlugins() - }) - if onceErr != nil { - return nil, onceErr - } - client := IpfsHelper{} - err := client.createRepo(downloadPath, profiles) - if err != nil { - return nil, err - } - err = client.createNode(ctx, clientOnly) - if err != nil { - return nil, err - } - err = client.connectToPeers(ctx, peerList) - if err != nil { - return nil, err - } - return &client, nil -} - -func CanBeIpfsPath(pathString string) bool { - path := path.New(pathString) - return path.IsValid() == nil || - strings.HasPrefix(pathString, "/ipfs/") || - strings.HasPrefix(pathString, "/ipld/") || - strings.HasPrefix(pathString, "/ipns/") || - strings.HasPrefix(pathString, "ipfs://") || - strings.HasPrefix(pathString, "ipns://") -} - -// TODO break abstraction for now til we figure out what fns are needed -func (h *IpfsHelper) GetAPI() coreiface.CoreAPI { - return h.api -} diff --git a/cmd/ipfshelper/ipfshelper_stub.go b/cmd/ipfshelper/ipfshelper_stub.go deleted file mode 100644 index fa6a451927..0000000000 --- a/cmd/ipfshelper/ipfshelper_stub.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build !ipfs -// +build !ipfs - -package ipfshelper - -import ( - "context" - "errors" -) - -type IpfsHelper struct{} - -var ErrIpfsNotSupported = errors.New("ipfs not supported") - -var DefaultIpfsProfiles = "default ipfs profiles stub" - -func CanBeIpfsPath(pathString string) bool { - return false -} - -func CreateIpfsHelper(ctx context.Context, downloadPath string, clientOnly bool, peerList []string, profiles string) (*IpfsHelper, error) { - return nil, ErrIpfsNotSupported -} - -func (h *IpfsHelper) DownloadFile(ctx context.Context, cidString string, destinationDir string) (string, error) { - return "", ErrIpfsNotSupported -} - -func (h *IpfsHelper) Close() error { - return ErrIpfsNotSupported -} diff --git a/cmd/ipfshelper/ipfshelper_test.go b/cmd/ipfshelper/ipfshelper_test.go deleted file mode 100644 index 80f10c21f6..0000000000 --- a/cmd/ipfshelper/ipfshelper_test.go +++ /dev/null @@ -1,123 +0,0 @@ -//go:build ipfs -// +build ipfs - -package ipfshelper - -import ( - "bytes" - "context" - "math/rand" - "os" - "path/filepath" - "testing" - "time" - - "github.com/offchainlabs/nitro/util/testhelpers" -) - -func getTempFileWithData(t *testing.T, data []byte) string { - path := filepath.Join(t.TempDir(), "config.json") - err := os.WriteFile(path, []byte(data), 0600) - testhelpers.RequireImpl(t, err) - return path -} - -func fileDataEqual(t *testing.T, path string, expected []byte) bool { - data, err := os.ReadFile(path) - testhelpers.RequireImpl(t, err) - return bytes.Equal(data, expected) -} - -func TestIpfsHelper(t *testing.T) { - ctx := context.Background() - ipfsA, err := createIpfsHelperImpl(ctx, t.TempDir(), false, []string{}, "test") - testhelpers.RequireImpl(t, err) - // add a test file to node A - testData := make([]byte, 1024*1024) - _, err = rand.Read(testData) - testhelpers.RequireImpl(t, err) - testFile := getTempFileWithData(t, testData) - ipfsTestFilePath, err := ipfsA.AddFile(ctx, testFile, false) - testhelpers.RequireImpl(t, err) - testFileCid := ipfsTestFilePath.Cid().String() - addrsA, err := ipfsA.GetPeerHostAddresses() - testhelpers.RequireImpl(t, err) - // create node B connected to node A - ipfsB, err := createIpfsHelperImpl(ctx, t.TempDir(), false, addrsA, "test") - testhelpers.RequireImpl(t, err) - // download the test file with node B - downloadedFile, err := ipfsB.DownloadFile(ctx, testFileCid, t.TempDir()) - testhelpers.RequireImpl(t, err) - if !fileDataEqual(t, downloadedFile, testData) { - testhelpers.FailImpl(t, "Downloaded file does not contain expected data") - } - // clean up node A and test downloading the file from yet another node C - err = ipfsA.Close() - os.RemoveAll(ipfsA.repoPath) - testhelpers.RequireImpl(t, err) - addrsB, err := ipfsB.GetPeerHostAddresses() - testhelpers.RequireImpl(t, err) - ipfsC, err := createIpfsHelperImpl(ctx, t.TempDir(), false, addrsB, "test") - testhelpers.RequireImpl(t, err) - downloadedFile, err = ipfsC.DownloadFile(ctx, testFileCid, t.TempDir()) - testhelpers.RequireImpl(t, err) - if !fileDataEqual(t, downloadedFile, testData) { - testhelpers.FailImpl(t, "Downloaded file does not contain expected data") - } - // make sure closing B and C nodes (A already closed) will make it impossible to download the test file from new node D - ipfsD, err := createIpfsHelperImpl(ctx, t.TempDir(), false, addrsB, "test") - testhelpers.RequireImpl(t, err) - err = ipfsB.Close() - testhelpers.RequireImpl(t, err) - err = ipfsC.Close() - testhelpers.RequireImpl(t, err) - testTimeout := 300 * time.Millisecond - timeoutCtx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - _, err = ipfsD.DownloadFile(timeoutCtx, testFileCid, t.TempDir()) - if err == nil { - testhelpers.FailImpl(t, "Download attempt did not fail as expected") - } - err = ipfsD.Close() - testhelpers.RequireImpl(t, err) -} - -func TestNormalizeCidString(t *testing.T) { - for _, test := range []struct { - input string - expected string - }{ - {"ipfs://QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"}, - {"ipns://k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8"}, - {"ipns://docs.ipfs.tech/introduction/", "/ipns/docs.ipfs.tech/introduction/"}, - {"/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"}, - {"/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8"}, - {"QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"}, - } { - if res := normalizeCidString(test.input); res != test.expected { - testhelpers.FailImpl(t, "Failed to normalize cid string, input: ", test.input, " got: ", res, " expected: ", test.expected) - } - } -} - -func TestCanBeIpfsPath(t *testing.T) { - correctPaths := []string{ - "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", - "/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", - "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", - "/ipns/docs.ipfs.tech/introduction/", - "ipfs://QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", - "ipns://k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", - } - for _, path := range correctPaths { - if !CanBeIpfsPath(path) { - testhelpers.FailImpl(t, "false negative result for path:", path) - } - } - incorrectPaths := []string{"www.ipfs.tech", "https://www.ipfs.tech", "QmIncorrect"} - for _, path := range incorrectPaths { - if CanBeIpfsPath(path) { - testhelpers.FailImpl(t, "false positive result for path:", path) - } - } -} diff --git a/cmd/nitro-val/config.go b/cmd/nitro-val/config.go index 2adbe5e9aa..bca83277b3 100644 --- a/cmd/nitro-val/config.go +++ b/cmd/nitro-val/config.go @@ -2,19 +2,20 @@ package main import ( "fmt" - "reflect" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/cmd/conf" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/validator/valnode" - flag "github.com/spf13/pflag" ) type ValidationNodeConfig struct { diff --git a/cmd/nitro/config_test.go b/cmd/nitro/config_test.go index 9626893083..ef41d704f1 100644 --- a/cmd/nitro/config_test.go +++ b/cmd/nitro/config_test.go @@ -14,14 +14,14 @@ import ( "testing" "time" + "github.com/r3labs/diff/v3" + flag "github.com/spf13/pflag" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util/confighelpers" "github.com/offchainlabs/nitro/das" "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/util/testhelpers" - - "github.com/r3labs/diff/v3" - flag "github.com/spf13/pflag" ) func TestEmptyCliConfig(t *testing.T) { diff --git a/cmd/nitro/init.go b/cmd/nitro/init.go index d9ae0df3b0..93c51a0040 100644 --- a/cmd/nitro/init.go +++ b/cmd/nitro/init.go @@ -24,11 +24,13 @@ import ( "time" "github.com/cavaliergopher/grab/v3" - extract "github.com/codeclysm/extract/v3" + "github.com/codeclysm/extract/v3" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -37,10 +39,8 @@ import ( "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/conf" - "github.com/offchainlabs/nitro/cmd/ipfshelper" "github.com/offchainlabs/nitro/cmd/pruning" "github.com/offchainlabs/nitro/cmd/staterecovery" "github.com/offchainlabs/nitro/execution/gethexec" @@ -58,25 +58,6 @@ func downloadInit(ctx context.Context, initConfig *conf.InitConfig) (string, err if strings.HasPrefix(initConfig.Url, "file:") { return initConfig.Url[5:], nil } - if ipfshelper.CanBeIpfsPath(initConfig.Url) { - ipfsNode, err := ipfshelper.CreateIpfsHelper(ctx, initConfig.DownloadPath, false, []string{}, ipfshelper.DefaultIpfsProfiles) - if err != nil { - return "", err - } - log.Info("Downloading initial database via IPFS", "url", initConfig.Url) - initFile, downloadErr := ipfsNode.DownloadFile(ctx, initConfig.Url, initConfig.DownloadPath) - closeErr := ipfsNode.Close() - if downloadErr != nil { - if closeErr != nil { - log.Error("Failed to close IPFS node after download error", "err", closeErr) - } - return "", fmt.Errorf("Failed to download file from IPFS: %w", downloadErr) - } - if closeErr != nil { - return "", fmt.Errorf("Failed to close IPFS node: %w", err) - } - return initFile, nil - } log.Info("Downloading initial database", "url", initConfig.Url) if !initConfig.ValidateChecksum { file, err := downloadFile(ctx, initConfig, initConfig.Url, nil) @@ -354,12 +335,12 @@ func validateBlockChain(blockChain *core.BlockChain, chainConfig *params.ChainCo } // Make sure we don't allow accidentally downgrading ArbOS if chainConfig.DebugMode() { - if currentArbosState.ArbOSVersion() > currentArbosState.MaxDebugArbosVersionSupported() { - return fmt.Errorf("attempted to launch node in debug mode with ArbOS version %v on ArbOS state with version %v", currentArbosState.MaxDebugArbosVersionSupported(), currentArbosState.ArbOSVersion()) + if currentArbosState.ArbOSVersion() > params.MaxDebugArbosVersionSupported { + return fmt.Errorf("attempted to launch node in debug mode with ArbOS version %v on ArbOS state with version %v", params.MaxDebugArbosVersionSupported, currentArbosState.ArbOSVersion()) } } else { - if currentArbosState.ArbOSVersion() > currentArbosState.MaxArbosVersionSupported() { - return fmt.Errorf("attempted to launch node with ArbOS version %v on ArbOS state with version %v", currentArbosState.MaxArbosVersionSupported(), currentArbosState.ArbOSVersion()) + if currentArbosState.ArbOSVersion() > params.MaxArbosVersionSupported { + return fmt.Errorf("attempted to launch node with ArbOS version %v on ArbOS state with version %v", params.MaxArbosVersionSupported, currentArbosState.ArbOSVersion()) } } @@ -405,6 +386,46 @@ func databaseIsEmpty(db ethdb.Database) bool { return !it.Next() } +func isWasmDb(path string) bool { + path = strings.ToLower(path) // lowers the path to handle case-insensitive file systems + path = filepath.Clean(path) + parts := strings.Split(path, string(filepath.Separator)) + if len(parts) >= 1 && parts[0] == "wasm" { + return true + } + if len(parts) >= 2 && parts[0] == "" && parts[1] == "wasm" { // Cover "/wasm" case + return true + } + return false +} + +func extractSnapshot(archive string, location string, importWasm bool) error { + reader, err := os.Open(archive) + if err != nil { + return fmt.Errorf("couln't open init '%v' archive: %w", archive, err) + } + defer reader.Close() + stat, err := reader.Stat() + if err != nil { + return err + } + log.Info("extracting downloaded init archive", "size", fmt.Sprintf("%dMB", stat.Size()/1024/1024)) + var rename extract.Renamer + if !importWasm { + rename = func(path string) string { + if isWasmDb(path) { + return "" // do not extract wasm files + } + return path + } + } + err = extract.Archive(context.Background(), reader, location, rename) + if err != nil { + return fmt.Errorf("couln't extract init archive '%v' err: %w", archive, err) + } + return nil +} + // removes all entries with keys prefixed with prefixes and of length used in initial version of wasm store schema func purgeVersion0WasmStoreEntries(db ethdb.Database) error { prefixes, keyLength := rawdb.DeprecatedPrefixesV0() @@ -475,7 +496,52 @@ func validateOrUpgradeWasmStoreSchemaVersion(db ethdb.Database) error { return nil } -func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeConfig, chainId *big.Int, cacheConfig *core.CacheConfig, persistentConfig *conf.PersistentConfig, l1Client arbutil.L1Interface, rollupAddrs chaininfo.RollupAddresses) (ethdb.Database, *core.BlockChain, error) { +func rebuildLocalWasm(ctx context.Context, config *gethexec.Config, l2BlockChain *core.BlockChain, chainDb, wasmDb ethdb.Database, rebuildMode string) (ethdb.Database, *core.BlockChain, error) { + var err error + latestBlock := l2BlockChain.CurrentBlock() + if latestBlock == nil || latestBlock.Number.Uint64() <= l2BlockChain.Config().ArbitrumChainParams.GenesisBlockNum || + types.DeserializeHeaderExtraInformation(latestBlock).ArbOSFormatVersion < params.ArbosVersion_Stylus { + // If there is only genesis block or no blocks in the blockchain, set Rebuilding of wasm store to Done + // If Stylus upgrade hasn't yet happened, skipping rebuilding of wasm store + log.Info("Setting rebuilding of wasm store to done") + if err = gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, gethexec.RebuildingDone); err != nil { + return nil, nil, fmt.Errorf("unable to set rebuilding status of wasm store to done: %w", err) + } + } else if rebuildMode != "false" { + var position common.Hash + if rebuildMode == "force" { + log.Info("Commencing force rebuilding of wasm store by setting codehash position in rebuilding to beginning") + if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, common.Hash{}); err != nil { + return nil, nil, fmt.Errorf("unable to initialize codehash position in rebuilding of wasm store to beginning: %w", err) + } + } else { + position, err = gethexec.ReadFromKeyValueStore[common.Hash](wasmDb, gethexec.RebuildingPositionKey) + if err != nil { + log.Info("Unable to get codehash position in rebuilding of wasm store, its possible it isnt initialized yet, so initializing it and starting rebuilding", "err", err) + if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, common.Hash{}); err != nil { + return nil, nil, fmt.Errorf("unable to initialize codehash position in rebuilding of wasm store to beginning: %w", err) + } + } + } + if position != gethexec.RebuildingDone { + startBlockHash, err := gethexec.ReadFromKeyValueStore[common.Hash](wasmDb, gethexec.RebuildingStartBlockHashKey) + if err != nil { + log.Info("Unable to get start block hash in rebuilding of wasm store, its possible it isnt initialized yet, so initializing it to latest block hash", "err", err) + if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingStartBlockHashKey, latestBlock.Hash()); err != nil { + return nil, nil, fmt.Errorf("unable to initialize start block hash in rebuilding of wasm store to latest block hash: %w", err) + } + startBlockHash = latestBlock.Hash() + } + log.Info("Starting or continuing rebuilding of wasm store", "codeHash", position, "startBlockHash", startBlockHash) + if err := gethexec.RebuildWasmStore(ctx, wasmDb, chainDb, config.RPC.MaxRecreateStateDepth, &config.StylusTarget, l2BlockChain, position, startBlockHash); err != nil { + return nil, nil, fmt.Errorf("error rebuilding of wasm store: %w", err) + } + } + } + return chainDb, l2BlockChain, nil +} + +func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeConfig, chainId *big.Int, cacheConfig *core.CacheConfig, targetConfig *gethexec.StylusTargetConfig, persistentConfig *conf.PersistentConfig, l1Client *ethclient.Client, rollupAddrs chaininfo.RollupAddresses) (ethdb.Database, *core.BlockChain, error) { if !config.Init.Force { if readOnlyDb, err := stack.OpenDatabaseWithFreezerWithExtraOptions("l2chaindata", 0, 0, config.Persistent.Ancient, "l2chaindata/", true, persistentConfig.Pebble.ExtraOptions("l2chaindata")); err == nil { if chainConfig := gethexec.TryReadStoredChainConfig(readOnlyDb); chainConfig != nil { @@ -500,7 +566,7 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err := dbutil.UnfinishedConversionCheck(wasmDb); err != nil { return nil, nil, fmt.Errorf("wasm unfinished database conversion check error: %w", err) } - chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmDb, 1) + chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmDb, 1, targetConfig.WasmTargets()) _, err = rawdb.ParseStateScheme(cacheConfig.StateScheme, chainDb) if err != nil { return nil, nil, err @@ -523,39 +589,7 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo return chainDb, l2BlockChain, fmt.Errorf("failed to recreate missing states: %w", err) } } - latestBlock := l2BlockChain.CurrentBlock() - if latestBlock == nil || latestBlock.Number.Uint64() <= chainConfig.ArbitrumChainParams.GenesisBlockNum || - types.DeserializeHeaderExtraInformation(latestBlock).ArbOSFormatVersion < params.ArbosVersion_Stylus { - // If there is only genesis block or no blocks in the blockchain, set Rebuilding of wasm store to Done - // If Stylus upgrade hasn't yet happened, skipping rebuilding of wasm store - log.Info("Setting rebuilding of wasm store to done") - if err = gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, gethexec.RebuildingDone); err != nil { - return nil, nil, fmt.Errorf("unable to set rebuilding status of wasm store to done: %w", err) - } - } else if config.Init.RebuildLocalWasm { - position, err := gethexec.ReadFromKeyValueStore[common.Hash](wasmDb, gethexec.RebuildingPositionKey) - if err != nil { - log.Info("Unable to get codehash position in rebuilding of wasm store, its possible it isnt initialized yet, so initializing it and starting rebuilding", "err", err) - if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, common.Hash{}); err != nil { - return nil, nil, fmt.Errorf("unable to initialize codehash position in rebuilding of wasm store to beginning: %w", err) - } - } - if position != gethexec.RebuildingDone { - startBlockHash, err := gethexec.ReadFromKeyValueStore[common.Hash](wasmDb, gethexec.RebuildingStartBlockHashKey) - if err != nil { - log.Info("Unable to get start block hash in rebuilding of wasm store, its possible it isnt initialized yet, so initializing it to latest block hash", "err", err) - if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingStartBlockHashKey, latestBlock.Hash()); err != nil { - return nil, nil, fmt.Errorf("unable to initialize start block hash in rebuilding of wasm store to latest block hash: %w", err) - } - startBlockHash = latestBlock.Hash() - } - log.Info("Starting or continuing rebuilding of wasm store", "codeHash", position, "startBlockHash", startBlockHash) - if err := gethexec.RebuildWasmStore(ctx, wasmDb, chainDb, config.Execution.RPC.MaxRecreateStateDepth, l2BlockChain, position, startBlockHash); err != nil { - return nil, nil, fmt.Errorf("error rebuilding of wasm store: %w", err) - } - } - } - return chainDb, l2BlockChain, nil + return rebuildLocalWasm(ctx, &config.Execution, l2BlockChain, chainDb, wasmDb, config.Init.RebuildLocalWasm) } readOnlyDb.Close() } else if !dbutil.IsNotExistError(err) { @@ -589,19 +623,9 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo } if initFile != "" { - reader, err := os.Open(initFile) - if err != nil { - return nil, nil, fmt.Errorf("couln't open init '%v' archive: %w", initFile, err) - } - stat, err := reader.Stat() - if err != nil { + if err := extractSnapshot(initFile, stack.InstanceDir(), config.Init.ImportWasm); err != nil { return nil, nil, err } - log.Info("extracting downloaded init archive", "size", fmt.Sprintf("%dMB", stat.Size()/1024/1024)) - err = extract.Archive(context.Background(), reader, stack.InstanceDir(), nil) - if err != nil { - return nil, nil, fmt.Errorf("couln't extract init archive '%v' err:%w", initFile, err) - } } var initDataReader statetransfer.InitDataReader = nil @@ -617,7 +641,7 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err := validateOrUpgradeWasmStoreSchemaVersion(wasmDb); err != nil { return nil, nil, err } - chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmDb, 1) + chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmDb, 1, targetConfig.WasmTargets()) _, err = rawdb.ParseStateScheme(cacheConfig.StateScheme, chainDb) if err != nil { return nil, nil, err @@ -658,12 +682,43 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo Nonce: 0, }, }, + ChainOwner: common.HexToAddress(config.Init.DevInitAddress), } initDataReader = statetransfer.NewMemoryInitDataReader(&initData) } var chainConfig *params.ChainConfig + if config.Init.GenesisJsonFile != "" { + if initDataReader != nil { + return chainDb, nil, errors.New("multiple init methods supplied") + } + genesisJson, err := os.ReadFile(config.Init.GenesisJsonFile) + if err != nil { + return chainDb, nil, err + } + var gen core.Genesis + if err := json.Unmarshal(genesisJson, &gen); err != nil { + return chainDb, nil, err + } + var accounts []statetransfer.AccountInitializationInfo + for address, account := range gen.Alloc { + accounts = append(accounts, statetransfer.AccountInitializationInfo{ + Addr: address, + EthBalance: account.Balance, + Nonce: account.Nonce, + ContractInfo: &statetransfer.AccountInitContractInfo{ + Code: account.Code, + ContractStorage: account.Storage, + }, + }) + } + initDataReader = statetransfer.NewMemoryInitDataReader(&statetransfer.ArbosInitializationInfo{ + Accounts: accounts, + }) + chainConfig = gen.Config + } + var l2BlockChain *core.BlockChain txIndexWg := sync.WaitGroup{} if initDataReader == nil { @@ -689,10 +744,14 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err != nil { return chainDb, nil, err } - combinedL2ChainInfoFiles := aggregateL2ChainInfoFiles(ctx, config.Chain.InfoFiles, config.Chain.InfoIpfsUrl, config.Chain.InfoIpfsDownloadPath) - chainConfig, err = chaininfo.GetChainConfig(new(big.Int).SetUint64(config.Chain.ID), config.Chain.Name, genesisBlockNr, combinedL2ChainInfoFiles, config.Chain.InfoJson) - if err != nil { - return chainDb, nil, err + if chainConfig == nil { + chainConfig, err = chaininfo.GetChainConfig(new(big.Int).SetUint64(config.Chain.ID), config.Chain.Name, genesisBlockNr, config.Chain.InfoFiles, config.Chain.InfoJson) + if err != nil { + return chainDb, nil, err + } + } + if config.Init.DevInit && config.Init.DevMaxCodeSize != 0 { + chainConfig.ArbitrumChainParams.MaxCodeSize = config.Init.DevMaxCodeSize } testUpdateTxIndex(chainDb, chainConfig, &txIndexWg) ancients, err := chainDb.Ancients() @@ -787,7 +846,7 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo return chainDb, l2BlockChain, err } - return chainDb, l2BlockChain, nil + return rebuildLocalWasm(ctx, &config.Execution, l2BlockChain, chainDb, wasmDb, config.Init.RebuildLocalWasm) } func testTxIndexUpdated(chainDb ethdb.Database, lastBlock uint64) bool { @@ -833,6 +892,7 @@ func testUpdateTxIndex(chainDb ethdb.Database, chainConfig *params.ChainConfig, localWg.Add(1) go func() { batch := chainDb.NewBatch() + // #nosec G115 for blockNum := uint64(thread); blockNum <= lastBlock; blockNum += uint64(threads) { blockHash := rawdb.ReadCanonicalHash(chainDb, blockNum) block := rawdb.ReadBlock(chainDb, blockHash, blockNum) diff --git a/cmd/nitro/init_test.go b/cmd/nitro/init_test.go index b2773ed861..8e7afe369d 100644 --- a/cmd/nitro/init_test.go +++ b/cmd/nitro/init_test.go @@ -4,26 +4,32 @@ package main import ( + "archive/tar" "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "fmt" + "io" "math/big" "net" "net/http" "os" "path" "path/filepath" + "slices" "strings" "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/node" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/conf" @@ -350,6 +356,12 @@ func TestEmptyDatabaseDir(t *testing.T) { } } +func defaultStylusTargetConfigForTest(t *testing.T) *gethexec.StylusTargetConfig { + targetConfig := gethexec.DefaultStylusTargetConfig + Require(t, targetConfig.Validate()) + return &targetConfig +} + func TestOpenInitializeChainDbIncompatibleStateScheme(t *testing.T) { t.Parallel() @@ -377,6 +389,7 @@ func TestOpenInitializeChainDbIncompatibleStateScheme(t *testing.T) { &nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(stack, &nodeConfig.Execution.Caching), + defaultStylusTargetConfigForTest(t), &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, @@ -393,6 +406,7 @@ func TestOpenInitializeChainDbIncompatibleStateScheme(t *testing.T) { &nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(stack, &nodeConfig.Execution.Caching), + defaultStylusTargetConfigForTest(t), &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, @@ -410,6 +424,7 @@ func TestOpenInitializeChainDbIncompatibleStateScheme(t *testing.T) { &nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(stack, &nodeConfig.Execution.Caching), + defaultStylusTargetConfigForTest(t), &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, @@ -545,6 +560,7 @@ func TestOpenInitializeChainDbEmptyInit(t *testing.T) { &nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(stack, &nodeConfig.Execution.Caching), + defaultStylusTargetConfigForTest(t), &nodeConfig.Persistent, l1Client, chaininfo.RollupAddresses{}, @@ -554,3 +570,152 @@ func TestOpenInitializeChainDbEmptyInit(t *testing.T) { err = chainDb.Close() Require(t, err) } + +func TestExtractSnapshot(t *testing.T) { + testCases := []struct { + name string + archiveFiles []string + importWasm bool + wantFiles []string + }{ + { + name: "extractAll", + importWasm: true, + archiveFiles: []string{ + "arbitrumdata/000001.ldb", + "l2chaindata/000001.ldb", + "l2chaindata/ancients/000001.ldb", + "nodes/000001.ldb", + "wasm/000001.ldb", + }, + wantFiles: []string{ + "arbitrumdata/000001.ldb", + "l2chaindata/000001.ldb", + "l2chaindata/ancients/000001.ldb", + "nodes/000001.ldb", + "wasm/000001.ldb", + }, + }, + { + name: "extractAllButWasm", + importWasm: false, + archiveFiles: []string{ + "arbitrumdata/000001.ldb", + "l2chaindata/000001.ldb", + "nodes/000001.ldb", + "wasm/000001.ldb", + }, + wantFiles: []string{ + "arbitrumdata/000001.ldb", + "l2chaindata/000001.ldb", + "nodes/000001.ldb", + }, + }, + { + name: "extractAllButWasmWithPrefixDot", + importWasm: false, + archiveFiles: []string{ + "./arbitrumdata/000001.ldb", + "./l2chaindata/000001.ldb", + "./nodes/000001.ldb", + "./wasm/000001.ldb", + }, + wantFiles: []string{ + "arbitrumdata/000001.ldb", + "l2chaindata/000001.ldb", + "nodes/000001.ldb", + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // Create archive with dummy files + archiveDir := t.TempDir() + archivePath := path.Join(archiveDir, "archive.tar") + { + // Create context to close the file handlers + archiveFile, err := os.Create(archivePath) + Require(t, err) + defer archiveFile.Close() + tarWriter := tar.NewWriter(archiveFile) + defer tarWriter.Close() + for _, relativePath := range testCase.archiveFiles { + filePath := path.Join(archiveDir, relativePath) + dir := filepath.Dir(filePath) + const dirPerm = 0700 + err := os.MkdirAll(dir, dirPerm) + Require(t, err) + const filePerm = 0600 + err = os.WriteFile(filePath, []byte{0xbe, 0xef}, filePerm) + Require(t, err) + file, err := os.Open(filePath) + Require(t, err) + info, err := file.Stat() + Require(t, err) + header, err := tar.FileInfoHeader(info, "") + Require(t, err) + header.Name = relativePath + err = tarWriter.WriteHeader(header) + Require(t, err) + _, err = io.Copy(tarWriter, file) + Require(t, err) + } + } + + // Extract archive and compare contents + targetDir := t.TempDir() + err := extractSnapshot(archivePath, targetDir, testCase.importWasm) + Require(t, err, "failed to extract snapshot") + gotFiles := []string{} + err = filepath.WalkDir(targetDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + gotFiles = append(gotFiles, path) + } + return nil + }) + Require(t, err) + slices.Sort(gotFiles) + for i, f := range testCase.wantFiles { + testCase.wantFiles[i] = path.Join(targetDir, f) + } + if diff := cmp.Diff(gotFiles, testCase.wantFiles); diff != "" { + t.Fatal("extracted files don't match", diff) + } + }) + } +} + +func TestIsWasmDb(t *testing.T) { + testCases := []struct { + path string + want bool + }{ + {"wasm", true}, + {"wasm/", true}, + {"wasm/something", true}, + {"/wasm", true}, + {"./wasm", true}, + {"././wasm", true}, + {"/./wasm", true}, + {"WASM", true}, + {"wAsM", true}, + {"nitro/../wasm", true}, + {"/nitro/../wasm", true}, + {".//nitro/.//../wasm", true}, + {"not-wasm", false}, + {"l2chaindata/example@@", false}, + {"somedir/wasm", false}, + } + for _, testCase := range testCases { + name := fmt.Sprintf("%q", testCase.path) + t.Run(name, func(t *testing.T) { + got := isWasmDb(testCase.path) + if testCase.want != got { + t.Fatalf("want %v, but got %v", testCase.want, got) + } + }) + } +} diff --git a/cmd/nitro/nitro.go b/cmd/nitro/nitro.go index bea754d5ce..e7afa6389e 100644 --- a/cmd/nitro/nitro.go +++ b/cmd/nitro/nitro.go @@ -59,7 +59,7 @@ import ( "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" - "github.com/offchainlabs/nitro/staker" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" "github.com/offchainlabs/nitro/staker/validatorwallet" "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/util/dbutil" @@ -249,7 +249,7 @@ func mainImpl() int { // If sequencer and signing is enabled or batchposter is enabled without // external signing sequencer will need a key. sequencerNeedsKey := (nodeConfig.Node.Sequencer && !nodeConfig.Node.Feed.Output.DisableSigning) || - (nodeConfig.Node.BatchPoster.Enable && nodeConfig.Node.BatchPoster.DataPoster.ExternalSigner.URL == "") + (nodeConfig.Node.BatchPoster.Enable && (nodeConfig.Node.BatchPoster.DataPoster.ExternalSigner.URL == "" || nodeConfig.Node.DataAvailability.Enable)) validatorNeedsKey := nodeConfig.Node.Staker.OnlyCreateWalletContract || (nodeConfig.Node.Staker.Enable && !strings.EqualFold(nodeConfig.Node.Staker.Strategy, "watchtower") && nodeConfig.Node.Staker.DataPoster.ExternalSigner.URL == "") @@ -257,7 +257,7 @@ func mainImpl() int { defaultL1WalletConfig.ResolveDirectoryNames(nodeConfig.Persistent.Chain) nodeConfig.Node.Staker.ParentChainWallet.ResolveDirectoryNames(nodeConfig.Persistent.Chain) - defaultValidatorL1WalletConfig := staker.DefaultValidatorL1WalletConfig + defaultValidatorL1WalletConfig := legacystaker.DefaultValidatorL1WalletConfig defaultValidatorL1WalletConfig.ResolveDirectoryNames(nodeConfig.Persistent.Chain) nodeConfig.Node.BatchPoster.ParentChainWallet.ResolveDirectoryNames(nodeConfig.Persistent.Chain) @@ -285,18 +285,16 @@ func mainImpl() int { } } - combinedL2ChainInfoFile := aggregateL2ChainInfoFiles(ctx, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoIpfsUrl, nodeConfig.Chain.InfoIpfsDownloadPath) - if nodeConfig.Node.Staker.Enable { if !nodeConfig.Node.ParentChainReader.Enable { flag.Usage() log.Crit("validator must have the parent chain reader enabled") } - strategy, err := nodeConfig.Node.Staker.ParseStrategy() + strategy, err := legacystaker.ParseStrategy(nodeConfig.Node.Staker.Strategy) if err != nil { log.Crit("couldn't parse staker strategy", "err", err) } - if strategy != staker.WatchtowerStrategy && !nodeConfig.Node.Staker.Dangerous.WithoutBlockValidator { + if strategy != legacystaker.WatchtowerStrategy && !nodeConfig.Node.Staker.Dangerous.WithoutBlockValidator { nodeConfig.Node.BlockValidator.Enable = true } } @@ -335,7 +333,7 @@ func mainImpl() int { log.Info("connected to l1 chain", "l1url", nodeConfig.ParentChain.Connection.URL, "l1chainid", nodeConfig.ParentChain.ID) - rollupAddrs, err = chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, combinedL2ChainInfoFile, nodeConfig.Chain.InfoJson) + rollupAddrs, err = chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoJson) if err != nil { log.Crit("error getting rollup addresses", "err", err) } @@ -367,11 +365,25 @@ func mainImpl() int { log.Crit("--node.validator.only-create-wallet-contract conflicts with --node.dangerous.no-l1-listener") } // Just create validator smart wallet if needed then exit - deployInfo, err := chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, combinedL2ChainInfoFile, nodeConfig.Chain.InfoJson) + deployInfo, err := chaininfo.GetRollupAddressesConfig(nodeConfig.Chain.ID, nodeConfig.Chain.Name, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoJson) if err != nil { log.Crit("error getting rollup addresses config", "err", err) } - addr, err := validatorwallet.GetValidatorWalletContract(ctx, deployInfo.ValidatorWalletCreator, int64(deployInfo.DeployedAt), l1TransactionOptsValidator, l1Reader, true) + + dataPoster, err := arbnode.DataposterOnlyUsedToCreateValidatorWalletContract( + ctx, + l1Reader, + l1TransactionOptsValidator, + &nodeConfig.Node.Staker.DataPoster, + new(big.Int).SetUint64(nodeConfig.ParentChain.ID), + ) + if err != nil { + log.Crit("error creating data poster to create validator wallet contract", "err", err) + } + getExtraGas := func() uint64 { return nodeConfig.Node.Staker.ExtraGas } + + // #nosec G115 + addr, err := validatorwallet.GetValidatorWalletContract(ctx, deployInfo.ValidatorWalletCreator, int64(deployInfo.DeployedAt), l1Reader, true, dataPoster, getExtraGas) if err != nil { log.Crit("error creating validator wallet contract", "error", err, "address", l1TransactionOptsValidator.From.Hex()) } @@ -423,65 +435,13 @@ func mainImpl() int { // Check that node is compatible with on-chain WASM module root on startup and before any ArbOS upgrades take effect to prevent divergences if nodeConfig.Node.ParentChainReader.Enable && nodeConfig.Validation.Wasm.EnableWasmrootsCheck { - // Fetch current on-chain WASM module root - rollupUserLogic, err := rollupgen.NewRollupUserLogic(rollupAddrs.Rollup, l1Client) - if err != nil { - log.Error("failed to create rollupUserLogic", "err", err) - return 1 - } - moduleRoot, err := rollupUserLogic.WasmModuleRoot(&bind.CallOpts{Context: ctx}) + err := checkWasmModuleRootCompatibility(ctx, nodeConfig.Validation.Wasm, l1Client, rollupAddrs) if err != nil { - log.Error("failed to get on-chain WASM module root", "err", err) - return 1 - } - if (moduleRoot == common.Hash{}) { - log.Error("on-chain WASM module root is zero") - return 1 - } - // Check if the on-chain WASM module root belongs to the set of allowed module roots - allowedWasmModuleRoots := nodeConfig.Validation.Wasm.AllowedWasmModuleRoots - if len(allowedWasmModuleRoots) > 0 { - moduleRootMatched := false - for _, root := range allowedWasmModuleRoots { - bytes, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) - if err == nil { - if common.HexToHash(root) == common.BytesToHash(bytes) { - moduleRootMatched = true - break - } - continue - } - locator, locatorErr := server_common.NewMachineLocator(root) - if locatorErr != nil { - log.Warn("allowed-wasm-module-roots: value not a hex nor valid path:", "value", root, "locatorErr", locatorErr, "decodeErr", err) - continue - } - path := locator.GetMachinePath(moduleRoot) - if _, err := os.Stat(path); err == nil { - moduleRootMatched = true - break - } - } - if !moduleRootMatched { - log.Error("on-chain WASM module root did not match with any of the allowed WASM module roots") - return 1 - } - } else { - // If no allowed module roots were provided in config, check if we have a validator machine directory for the on-chain WASM module root - locator, err := server_common.NewMachineLocator(nodeConfig.Validation.Wasm.RootPath) - if err != nil { - log.Warn("failed to create machine locator. Skipping the check for compatibility with on-chain WASM module root", "err", err) - } else { - path := locator.GetMachinePath(moduleRoot) - if _, err := os.Stat(path); err != nil { - log.Error("unable to find validator machine directory for the on-chain WASM module root", "err", err) - return 1 - } - } + log.Warn("failed to check if node is compatible with on-chain WASM module root", "err", err) } } - chainDb, l2BlockChain, err := openInitializeChainDb(ctx, stack, nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(stack, &nodeConfig.Execution.Caching), &nodeConfig.Persistent, l1Client, rollupAddrs) + chainDb, l2BlockChain, err := openInitializeChainDb(ctx, stack, nodeConfig, new(big.Int).SetUint64(nodeConfig.Chain.ID), gethexec.DefaultCacheConfigFor(stack, &nodeConfig.Execution.Caching), &nodeConfig.Execution.StylusTarget, &nodeConfig.Persistent, l1Client, rollupAddrs) if l2BlockChain != nil { deferFuncs = append(deferFuncs, func() { l2BlockChain.Stop() }) } @@ -506,20 +466,29 @@ func mainImpl() int { fatalErrChan := make(chan error, 10) - var blocksReExecutor *blocksreexecutor.BlocksReExecutor if nodeConfig.BlocksReExecutor.Enable && l2BlockChain != nil { - blocksReExecutor = blocksreexecutor.New(&nodeConfig.BlocksReExecutor, l2BlockChain, fatalErrChan) - if nodeConfig.Init.ThenQuit { - success := make(chan struct{}) - blocksReExecutor.Start(ctx, success) - deferFuncs = append(deferFuncs, func() { blocksReExecutor.StopAndWait() }) - select { - case err := <-fatalErrChan: - log.Error("shutting down due to fatal error", "err", err) - defer log.Error("shut down due to fatal error", "err", err) - return 1 - case <-success: - } + if !nodeConfig.Init.ThenQuit { + log.Error("blocks-reexecutor cannot be enabled without --init.then-quit") + return 1 + } + blocksReExecutor, err := blocksreexecutor.New(&nodeConfig.BlocksReExecutor, l2BlockChain, chainDb, fatalErrChan) + if err != nil { + log.Error("error initializing blocksReExecutor", "err", err) + return 1 + } + if err := gethexec.PopulateStylusTargetCache(&nodeConfig.Execution.StylusTarget); err != nil { + log.Error("error populating stylus target cache", "err", err) + return 1 + } + success := make(chan struct{}) + blocksReExecutor.Start(ctx, success) + deferFuncs = append(deferFuncs, func() { blocksReExecutor.StopAndWait() }) + select { + case err := <-fatalErrChan: + log.Error("shutting down due to fatal error", "err", err) + defer log.Error("shut down due to fatal error", "err", err) + return 1 + case <-success: } } @@ -527,7 +496,7 @@ func mainImpl() int { return 0 } - chainInfo, err := chaininfo.ProcessChainInfo(nodeConfig.Chain.ID, nodeConfig.Chain.Name, combinedL2ChainInfoFile, nodeConfig.Chain.InfoJson) + chainInfo, err := chaininfo.ProcessChainInfo(nodeConfig.Chain.ID, nodeConfig.Chain.Name, nodeConfig.Chain.InfoFiles, nodeConfig.Chain.InfoJson) if err != nil { log.Error("error processing l2 chain info", "err", err) return 1 @@ -582,7 +551,7 @@ func mainImpl() int { l1TransactionOptsBatchPoster, dataSigner, fatalErrChan, - big.NewInt(int64(nodeConfig.ParentChain.ID)), + new(big.Int).SetUint64(nodeConfig.ParentChain.ID), blobReader, ) if err != nil { @@ -647,6 +616,46 @@ func mainImpl() int { } } } + + // Before starting the node, wait until the transaction that deployed rollup is finalized + if nodeConfig.EnsureRollupDeployment && + nodeConfig.Node.ParentChainReader.Enable && + rollupAddrs.DeployedAt > 0 { + currentFinalized, err := l1Reader.LatestFinalizedBlockNr(ctx) + if err != nil && errors.Is(err, headerreader.ErrBlockNumberNotSupported) { + log.Info("Finality not supported by parent chain, disabling the check to verify if rollup deployment tx was finalized", "err", err) + } else { + newHeaders, unsubscribe := l1Reader.Subscribe(false) + retriesOnError := 10 + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) + for currentFinalized < rollupAddrs.DeployedAt && retriesOnError > 0 { + select { + case <-newHeaders: + if finalized, err := l1Reader.LatestFinalizedBlockNr(ctx); err != nil { + if errors.Is(err, headerreader.ErrBlockNumberNotSupported) { + log.Error("Finality support was removed from parent chain mid way, disabling the check to verify if the rollup deployment tx was finalized", "err", err) + retriesOnError = 0 // Break out of for loop as well + break + } + log.Error("Error getting latestFinalizedBlockNr from l1Reader", "err", err) + retriesOnError-- + } else { + currentFinalized = finalized + log.Debug("Finalized block number updated", "finalized", finalized) + } + case <-ctx.Done(): + log.Error("Context done while checking if the rollup deployment tx was finalized") + return 1 + case <-sigint: + log.Info("shutting down because of sigint") + return 0 + } + } + unsubscribe() + } + } + gqlConf := nodeConfig.GraphQL if gqlConf.Enable { if err := graphql.New(stack, execNode.Backend.APIBackend(), execNode.FilterSystem, gqlConf.CORSDomain, gqlConf.VHosts); err != nil { @@ -671,10 +680,6 @@ func mainImpl() int { // remove previous deferFuncs, StopAndWait closes database and blockchain. deferFuncs = []func(){func() { currentNode.StopAndWait() }} } - if blocksReExecutor != nil && !nodeConfig.Init.ThenQuit { - blocksReExecutor.Start(ctx, nil) - deferFuncs = append(deferFuncs, func() { blocksReExecutor.StopAndWait() }) - } sigint := make(chan os.Signal, 1) signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) @@ -692,8 +697,12 @@ func mainImpl() int { if execNodeConfig.Sequencer.Enable && execNodeConfig.Sequencer.Timeboost.Enable { execNode.Sequencer.StartExpressLane( ctx, + execNode.Backend.APIBackend(), + execNode.FilterSystem, common.HexToAddress(execNodeConfig.Sequencer.Timeboost.AuctionContractAddress), - common.HexToAddress(execNodeConfig.Sequencer.Timeboost.AuctioneerAddress)) + common.HexToAddress(execNodeConfig.Sequencer.Timeboost.AuctioneerAddress), + execNodeConfig.Sequencer.Timeboost.EarlySubmissionGrace, + ) } err = nil @@ -718,53 +727,55 @@ func mainImpl() int { } type NodeConfig struct { - Conf genericconf.ConfConfig `koanf:"conf" reload:"hot"` - Node arbnode.Config `koanf:"node" reload:"hot"` - Execution gethexec.Config `koanf:"execution" reload:"hot"` - Validation valnode.Config `koanf:"validation" reload:"hot"` - ParentChain conf.ParentChainConfig `koanf:"parent-chain" reload:"hot"` - Chain conf.L2Config `koanf:"chain"` - LogLevel string `koanf:"log-level" reload:"hot"` - LogType string `koanf:"log-type" reload:"hot"` - FileLogging genericconf.FileLoggingConfig `koanf:"file-logging" reload:"hot"` - Persistent conf.PersistentConfig `koanf:"persistent"` - HTTP genericconf.HTTPConfig `koanf:"http"` - WS genericconf.WSConfig `koanf:"ws"` - IPC genericconf.IPCConfig `koanf:"ipc"` - Auth genericconf.AuthRPCConfig `koanf:"auth"` - GraphQL genericconf.GraphQLConfig `koanf:"graphql"` - Metrics bool `koanf:"metrics"` - MetricsServer genericconf.MetricsServerConfig `koanf:"metrics-server"` - PProf bool `koanf:"pprof"` - PprofCfg genericconf.PProf `koanf:"pprof-cfg"` - Init conf.InitConfig `koanf:"init"` - Rpc genericconf.RpcConfig `koanf:"rpc"` - BlocksReExecutor blocksreexecutor.Config `koanf:"blocks-reexecutor"` + Conf genericconf.ConfConfig `koanf:"conf" reload:"hot"` + Node arbnode.Config `koanf:"node" reload:"hot"` + Execution gethexec.Config `koanf:"execution" reload:"hot"` + Validation valnode.Config `koanf:"validation" reload:"hot"` + ParentChain conf.ParentChainConfig `koanf:"parent-chain" reload:"hot"` + Chain conf.L2Config `koanf:"chain"` + LogLevel string `koanf:"log-level" reload:"hot"` + LogType string `koanf:"log-type" reload:"hot"` + FileLogging genericconf.FileLoggingConfig `koanf:"file-logging" reload:"hot"` + Persistent conf.PersistentConfig `koanf:"persistent"` + HTTP genericconf.HTTPConfig `koanf:"http"` + WS genericconf.WSConfig `koanf:"ws"` + IPC genericconf.IPCConfig `koanf:"ipc"` + Auth genericconf.AuthRPCConfig `koanf:"auth"` + GraphQL genericconf.GraphQLConfig `koanf:"graphql"` + Metrics bool `koanf:"metrics"` + MetricsServer genericconf.MetricsServerConfig `koanf:"metrics-server"` + PProf bool `koanf:"pprof"` + PprofCfg genericconf.PProf `koanf:"pprof-cfg"` + Init conf.InitConfig `koanf:"init"` + Rpc genericconf.RpcConfig `koanf:"rpc"` + BlocksReExecutor blocksreexecutor.Config `koanf:"blocks-reexecutor"` + EnsureRollupDeployment bool `koanf:"ensure-rollup-deployment" reload:"hot"` } var NodeConfigDefault = NodeConfig{ - Conf: genericconf.ConfConfigDefault, - Node: arbnode.ConfigDefault, - Execution: gethexec.ConfigDefault, - Validation: valnode.DefaultValidationConfig, - ParentChain: conf.L1ConfigDefault, - Chain: conf.L2ConfigDefault, - LogLevel: "INFO", - LogType: "plaintext", - FileLogging: genericconf.DefaultFileLoggingConfig, - Persistent: conf.PersistentConfigDefault, - HTTP: genericconf.HTTPConfigDefault, - WS: genericconf.WSConfigDefault, - IPC: genericconf.IPCConfigDefault, - Auth: genericconf.AuthRPCConfigDefault, - GraphQL: genericconf.GraphQLConfigDefault, - Metrics: false, - MetricsServer: genericconf.MetricsServerConfigDefault, - Init: conf.InitConfigDefault, - Rpc: genericconf.DefaultRpcConfig, - PProf: false, - PprofCfg: genericconf.PProfDefault, - BlocksReExecutor: blocksreexecutor.DefaultConfig, + Conf: genericconf.ConfConfigDefault, + Node: arbnode.ConfigDefault, + Execution: gethexec.ConfigDefault, + Validation: valnode.DefaultValidationConfig, + ParentChain: conf.L1ConfigDefault, + Chain: conf.L2ConfigDefault, + LogLevel: "INFO", + LogType: "plaintext", + FileLogging: genericconf.DefaultFileLoggingConfig, + Persistent: conf.PersistentConfigDefault, + HTTP: genericconf.HTTPConfigDefault, + WS: genericconf.WSConfigDefault, + IPC: genericconf.IPCConfigDefault, + Auth: genericconf.AuthRPCConfigDefault, + GraphQL: genericconf.GraphQLConfigDefault, + Metrics: false, + MetricsServer: genericconf.MetricsServerConfigDefault, + Init: conf.InitConfigDefault, + Rpc: genericconf.DefaultRpcConfig, + PProf: false, + PprofCfg: genericconf.PProfDefault, + BlocksReExecutor: blocksreexecutor.DefaultConfig, + EnsureRollupDeployment: true, } func NodeConfigAddOptions(f *flag.FlagSet) { @@ -791,6 +802,7 @@ func NodeConfigAddOptions(f *flag.FlagSet) { conf.InitConfigAddOptions("init", f) genericconf.RpcConfigAddOptions("rpc", f) blocksreexecutor.ConfigAddOptions("blocks-reexecutor", f) + f.Bool("ensure-rollup-deployment", NodeConfigDefault.EnsureRollupDeployment, "before starting the node, wait until the transaction that deployed rollup is finalized") } func (c *NodeConfig) ResolveDirectoryNames() error { @@ -882,11 +894,10 @@ func ParseNode(ctx context.Context, args []string) (*NodeConfig, *genericconf.Wa l2ChainId := k.Int64("chain.id") l2ChainName := k.String("chain.name") - l2ChainInfoIpfsUrl := k.String("chain.info-ipfs-url") - l2ChainInfoIpfsDownloadPath := k.String("chain.info-ipfs-download-path") l2ChainInfoFiles := k.Strings("chain.info-files") l2ChainInfoJson := k.String("chain.info-json") - err = applyChainParameters(ctx, k, uint64(l2ChainId), l2ChainName, l2ChainInfoFiles, l2ChainInfoJson, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) + // #nosec G115 + err = applyChainParameters(k, uint64(l2ChainId), l2ChainName, l2ChainInfoFiles, l2ChainInfoJson) if err != nil { return nil, nil, err } @@ -949,20 +960,8 @@ func ParseNode(ctx context.Context, args []string) (*NodeConfig, *genericconf.Wa return &nodeConfig, &l2DevWallet, nil } -func aggregateL2ChainInfoFiles(ctx context.Context, l2ChainInfoFiles []string, l2ChainInfoIpfsUrl string, l2ChainInfoIpfsDownloadPath string) []string { - if l2ChainInfoIpfsUrl != "" { - l2ChainInfoIpfsFile, err := util.GetL2ChainInfoIpfsFile(ctx, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) - if err != nil { - log.Error("error getting l2 chain info file from ipfs", "err", err) - } - l2ChainInfoFiles = append(l2ChainInfoFiles, l2ChainInfoIpfsFile) - } - return l2ChainInfoFiles -} - -func applyChainParameters(ctx context.Context, k *koanf.Koanf, chainId uint64, chainName string, l2ChainInfoFiles []string, l2ChainInfoJson string, l2ChainInfoIpfsUrl string, l2ChainInfoIpfsDownloadPath string) error { - combinedL2ChainInfoFiles := aggregateL2ChainInfoFiles(ctx, l2ChainInfoFiles, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) - chainInfo, err := chaininfo.ProcessChainInfo(chainId, chainName, combinedL2ChainInfoFiles, l2ChainInfoJson) +func applyChainParameters(k *koanf.Koanf, chainId uint64, chainName string, l2ChainInfoFiles []string, l2ChainInfoJson string) error { + chainInfo, err := chaininfo.ProcessChainInfo(chainId, chainName, l2ChainInfoFiles, l2ChainInfoJson) if err != nil { return err } @@ -971,7 +970,7 @@ func applyChainParameters(ctx context.Context, k *koanf.Koanf, chainId uint64, c parentChainIsArbitrum = *chainInfo.ParentChainIsArbitrum } else { log.Warn("Chain info field parent-chain-is-arbitrum is missing, in the future this will be required", "chainId", chainInfo.ChainConfig.ChainID, "parentChainId", chainInfo.ParentChainId) - _, err := chaininfo.ProcessChainInfo(chainInfo.ParentChainId, "", combinedL2ChainInfoFiles, "") + _, err := chaininfo.ProcessChainInfo(chainInfo.ParentChainId, "", l2ChainInfoFiles, "") if err == nil { parentChainIsArbitrum = true } @@ -1031,13 +1030,16 @@ func applyChainParameters(ctx context.Context, k *koanf.Koanf, chainId uint64, c func initReorg(initConfig conf.InitConfig, chainConfig *params.ChainConfig, inboxTracker *arbnode.InboxTracker) error { var batchCount uint64 if initConfig.ReorgToBatch >= 0 { + // #nosec G115 batchCount = uint64(initConfig.ReorgToBatch) + 1 } else { var messageIndex arbutil.MessageIndex if initConfig.ReorgToMessageBatch >= 0 { + // #nosec G115 messageIndex = arbutil.MessageIndex(initConfig.ReorgToMessageBatch) } else if initConfig.ReorgToBlockBatch > 0 { genesis := chainConfig.ArbitrumChainParams.GenesisBlockNum + // #nosec G115 blockNum := uint64(initConfig.ReorgToBlockBatch) if blockNum < genesis { return fmt.Errorf("ReorgToBlockBatch %d before genesis %d", blockNum, genesis) @@ -1048,14 +1050,15 @@ func initReorg(initConfig conf.InitConfig, chainConfig *params.ChainConfig, inbo return nil } // Reorg out the batch containing the next message - var missing bool + var found bool var err error - batchCount, missing, err = inboxTracker.FindInboxBatchContainingMessage(messageIndex + 1) + batchCount, found, err = inboxTracker.FindInboxBatchContainingMessage(messageIndex + 1) if err != nil { return err } - if missing { - return fmt.Errorf("cannot reorg to unknown message index %v", messageIndex) + if !found { + log.Warn("init-reorg: no need to reorg, because message ahead of chain", "messageIndex", messageIndex) + return nil } } return inboxTracker.ReorgBatchesTo(batchCount) @@ -1068,3 +1071,57 @@ type NodeConfigFetcher struct { func (f *NodeConfigFetcher) Get() *arbnode.Config { return &f.LiveConfig.Get().Node } + +func checkWasmModuleRootCompatibility(ctx context.Context, wasmConfig valnode.WasmConfig, l1Client *ethclient.Client, rollupAddrs chaininfo.RollupAddresses) error { + // Fetch current on-chain WASM module root + rollupUserLogic, err := rollupgen.NewRollupUserLogic(rollupAddrs.Rollup, l1Client) + if err != nil { + return fmt.Errorf("failed to create RollupUserLogic: %w", err) + } + moduleRoot, err := rollupUserLogic.WasmModuleRoot(&bind.CallOpts{Context: ctx}) + if err != nil { + return fmt.Errorf("failed to get on-chain WASM module root: %w", err) + } + if (moduleRoot == common.Hash{}) { + return errors.New("on-chain WASM module root is zero") + } + // Check if the on-chain WASM module root belongs to the set of allowed module roots + allowedWasmModuleRoots := wasmConfig.AllowedWasmModuleRoots + if len(allowedWasmModuleRoots) > 0 { + moduleRootMatched := false + for _, root := range allowedWasmModuleRoots { + bytes, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err == nil { + if common.HexToHash(root) == common.BytesToHash(bytes) { + moduleRootMatched = true + break + } + continue + } + locator, locatorErr := server_common.NewMachineLocator(root) + if locatorErr != nil { + log.Warn("allowed-wasm-module-roots: value not a hex nor valid path:", "value", root, "locatorErr", locatorErr, "decodeErr", err) + continue + } + path := locator.GetMachinePath(moduleRoot) + if _, err := os.Stat(path); err == nil { + moduleRootMatched = true + break + } + } + if !moduleRootMatched { + return errors.New("on-chain WASM module root did not match with any of the allowed WASM module roots") + } + } else { + // If no allowed module roots were provided in config, check if we have a validator machine directory for the on-chain WASM module root + locator, err := server_common.NewMachineLocator(wasmConfig.RootPath) + if err != nil { + return fmt.Errorf("failed to create machine locator: %w", err) + } + path := locator.GetMachinePath(moduleRoot) + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("unable to find validator machine directory for the on-chain WASM module root: %w", err) + } + } + return nil +} diff --git a/cmd/pruning/pruning.go b/cmd/pruning/pruning.go index 096bb4b1ae..e89c79bc89 100644 --- a/cmd/pruning/pruning.go +++ b/cmd/pruning/pruning.go @@ -15,10 +15,12 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/pruner" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/arbutil" @@ -80,7 +82,7 @@ func (r *importantRoots) addHeader(header *types.Header, overwrite bool) error { var hashListRegex = regexp.MustCompile("^(0x)?[0-9a-fA-F]{64}(,(0x)?[0-9a-fA-F]{64})*$") // Finds important roots to retain while proving -func findImportantRoots(ctx context.Context, chainDb ethdb.Database, stack *node.Node, initConfig *conf.InitConfig, cacheConfig *core.CacheConfig, persistentConfig *conf.PersistentConfig, l1Client arbutil.L1Interface, rollupAddrs chaininfo.RollupAddresses, validatorRequired bool) ([]common.Hash, error) { +func findImportantRoots(ctx context.Context, chainDb ethdb.Database, stack *node.Node, initConfig *conf.InitConfig, cacheConfig *core.CacheConfig, persistentConfig *conf.PersistentConfig, l1Client *ethclient.Client, rollupAddrs chaininfo.RollupAddresses, validatorRequired bool) ([]common.Hash, error) { chainConfig := gethexec.TryReadStoredChainConfig(chainDb) if chainConfig == nil { return nil, errors.New("database doesn't have a chain config (was this node initialized?)") @@ -212,6 +214,7 @@ func findImportantRoots(ctx context.Context, chainDb ethdb.Database, stack *node } if meta.ParentChainBlock <= l1BlockNum { signedBlockNum := arbutil.MessageCountToBlockNumber(meta.MessageCount, genesisNum) + // #nosec G115 blockNum := uint64(signedBlockNum) l2Hash := rawdb.ReadCanonicalHash(chainDb, blockNum) l2Header := rawdb.ReadHeader(chainDb, l2Hash, blockNum) @@ -232,7 +235,7 @@ func findImportantRoots(ctx context.Context, chainDb ethdb.Database, stack *node return roots.roots, nil } -func PruneChainDb(ctx context.Context, chainDb ethdb.Database, stack *node.Node, initConfig *conf.InitConfig, cacheConfig *core.CacheConfig, persistentConfig *conf.PersistentConfig, l1Client arbutil.L1Interface, rollupAddrs chaininfo.RollupAddresses, validatorRequired bool) error { +func PruneChainDb(ctx context.Context, chainDb ethdb.Database, stack *node.Node, initConfig *conf.InitConfig, cacheConfig *core.CacheConfig, persistentConfig *conf.PersistentConfig, l1Client *ethclient.Client, rollupAddrs chaininfo.RollupAddresses, validatorRequired bool) error { if cacheConfig.StateScheme == rawdb.PathScheme { return nil } diff --git a/cmd/replay/db.go b/cmd/replay/db.go index 7147c48f75..3dc9f15da0 100644 --- a/cmd/replay/db.go +++ b/cmd/replay/db.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/wavmio" ) diff --git a/cmd/replay/main.go b/cmd/replay/main.go index 0fe56eb4c9..661040ea10 100644 --- a/cmd/replay/main.go +++ b/cmd/replay/main.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -291,7 +292,7 @@ func main() { message := readMessage(chainConfig.ArbitrumChainParams.DataAvailabilityCommittee) chainContext := WavmChainContext{} - newBlock, _, err = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false) + newBlock, _, err = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false, core.MessageReplayMode) if err != nil { panic(err) } diff --git a/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go b/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go index e963c0e96c..b6b5342ca2 100644 --- a/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go +++ b/cmd/seq-coordinator-manager/rediscoordinator/redis_coordinator.go @@ -5,7 +5,8 @@ import ( "errors" "strings" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" + "github.com/offchainlabs/nitro/util/redisutil" ) diff --git a/cmd/seq-coordinator-manager/seq-coordinator-manager.go b/cmd/seq-coordinator-manager/seq-coordinator-manager.go index 43d90441ef..7b5dc68699 100644 --- a/cmd/seq-coordinator-manager/seq-coordinator-manager.go +++ b/cmd/seq-coordinator-manager/seq-coordinator-manager.go @@ -7,11 +7,13 @@ import ( "strconv" "github.com/enescakir/emoji" - "github.com/ethereum/go-ethereum/log" "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/cmd/seq-coordinator-manager/rediscoordinator" "github.com/offchainlabs/nitro/util/redisutil" - "github.com/rivo/tview" ) // Tview diff --git a/cmd/staterecovery/staterecovery.go b/cmd/staterecovery/staterecovery.go index bb01477414..5486ba3726 100644 --- a/cmd/staterecovery/staterecovery.go +++ b/cmd/staterecovery/staterecovery.go @@ -60,6 +60,7 @@ func RecreateMissingStates(chainDb ethdb.Database, bc *core.BlockChain, cacheCon break } if time.Since(logged) > 1*time.Minute { + // #nosec G115 log.Info("Recreating missing states", "block", current, "target", target, "remaining", int64(target)-int64(current), "elapsed", time.Since(start), "recreated", recreated) logged = time.Now() } diff --git a/cmd/util/chaininfoutil.go b/cmd/util/chaininfoutil.go deleted file mode 100644 index 906aa234ed..0000000000 --- a/cmd/util/chaininfoutil.go +++ /dev/null @@ -1,29 +0,0 @@ -package util - -import ( - "context" - "fmt" - - "github.com/ethereum/go-ethereum/log" - "github.com/offchainlabs/nitro/cmd/ipfshelper" -) - -func GetL2ChainInfoIpfsFile(ctx context.Context, l2ChainInfoIpfsUrl string, l2ChainInfoIpfsDownloadPath string) (string, error) { - ipfsNode, err := ipfshelper.CreateIpfsHelper(ctx, l2ChainInfoIpfsDownloadPath, false, []string{}, ipfshelper.DefaultIpfsProfiles) - if err != nil { - return "", err - } - log.Info("Downloading l2 info file via IPFS", "url", l2ChainInfoIpfsDownloadPath) - l2ChainInfoFile, downloadErr := ipfsNode.DownloadFile(ctx, l2ChainInfoIpfsUrl, l2ChainInfoIpfsDownloadPath) - closeErr := ipfsNode.Close() - if downloadErr != nil { - if closeErr != nil { - log.Error("Failed to close IPFS node after download error", "err", closeErr) - } - return "", fmt.Errorf("failed to download file from IPFS: %w", downloadErr) - } - if closeErr != nil { - return "", fmt.Errorf("failed to close IPFS node: %w", err) - } - return l2ChainInfoFile, nil -} diff --git a/cmd/util/confighelpers/configuration.go b/cmd/util/confighelpers/configuration.go index 19b5b1a24c..8c4ef2a70b 100644 --- a/cmd/util/confighelpers/configuration.go +++ b/cmd/util/confighelpers/configuration.go @@ -209,6 +209,7 @@ func devFlagArgs() []string { "--init.empty=false", "--http.port", "8547", "--http.addr", "127.0.0.1", + "--http.api=net,web3,eth,arb,arbdebug,debug", } return args } diff --git a/contracts b/contracts index a815490fce..763bd77906 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit a815490fce7c7869f026df88efb437856caa46d8 +Subproject commit 763bd77906b7677da691eaa31c6e195d455197a4 diff --git a/das/aggregator.go b/das/aggregator.go index d944f8d48a..85fccb078f 100644 --- a/das/aggregator.go +++ b/das/aggregator.go @@ -15,11 +15,11 @@ import ( flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/offchainlabs/nitro/arbstate/daprovider" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/blsSignatures" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/solgen/go/bridgegen" @@ -114,7 +114,7 @@ func NewAggregator(ctx context.Context, config DataAvailabilityConfig, services func NewAggregatorWithL1Info( config DataAvailabilityConfig, services []ServiceDetails, - l1client arbutil.L1Interface, + l1client *ethclient.Client, seqInboxAddress common.Address, ) (*Aggregator, error) { seqInboxCaller, err := bridgegen.NewSequencerInboxCaller(seqInboxAddress, l1client) @@ -130,6 +130,7 @@ func NewAggregatorWithSeqInboxCaller( seqInboxCaller *bridgegen.SequencerInboxCaller, ) (*Aggregator, error) { + // #nosec G115 keysetHash, keysetBytes, err := KeysetHashFromServices(services, uint64(config.RPCAggregator.AssumedHonest)) if err != nil { return nil, err @@ -166,6 +167,7 @@ type storeResponse struct { // If Store gets not enough successful responses by the time its context is canceled // (eg via TimeoutWrapper) then it also returns an error. func (a *Aggregator) Store(ctx context.Context, message []byte, timeout uint64) (*daprovider.DataAvailabilityCertificate, error) { + // #nosec G115 log.Trace("das.Aggregator.Store", "message", pretty.FirstFewBytes(message), "timeout", time.Unix(int64(timeout), 0)) allBackendsSucceeded := false @@ -252,7 +254,7 @@ func (a *Aggregator) Store(ctx context.Context, message []byte, timeout uint64) var sigs []blsSignatures.Signature var aggSignersMask uint64 var successfullyStoredCount int - var returned bool + var returned int // 0-no status, 1-succeeded, 2-failed for i := 0; i < len(a.services); i++ { select { case <-ctx.Done(): @@ -274,26 +276,26 @@ func (a *Aggregator) Store(ctx context.Context, message []byte, timeout uint64) // certDetailsChan, so the Store function can return, but also continue // running until all responses are received (or the context is canceled) // in order to produce accurate logs/metrics. - if !returned { + if returned == 0 { if successfullyStoredCount >= a.requiredServicesForStore { cd := certDetails{} cd.pubKeys = append(cd.pubKeys, pubKeys...) cd.sigs = append(cd.sigs, sigs...) cd.aggSignersMask = aggSignersMask certDetailsChan <- cd - returned = true - if a.maxAllowedServiceStoreFailures > 0 && // Ignore the case where AssumedHonest = 1, probably a testnet - int(storeFailures.Load())+1 > a.maxAllowedServiceStoreFailures { - log.Error("das.Aggregator: storing the batch data succeeded to enough DAS commitee members to generate the Data Availability Cert, but if one more had failed then the cert would not have been able to be generated. Look for preceding logs with \"Error from backend\"") - } + returned = 1 } else if int(storeFailures.Load()) > a.maxAllowedServiceStoreFailures { cd := certDetails{} cd.err = fmt.Errorf("aggregator failed to store message to at least %d out of %d DASes (assuming %d are honest). %w", a.requiredServicesForStore, len(a.services), a.config.AssumedHonest, daprovider.ErrBatchToDasFailed) certDetailsChan <- cd - returned = true + returned = 2 } } - + } + if returned == 1 && + a.maxAllowedServiceStoreFailures > 0 && // Ignore the case where AssumedHonest = 1, probably a testnet + int(storeFailures.Load())+1 > a.maxAllowedServiceStoreFailures { + log.Error("das.Aggregator: storing the batch data succeeded to enough DAS commitee members to generate the Data Availability Cert, but if one more had failed then the cert would not have been able to be generated. Look for preceding logs with \"Error from backend\"") } }() diff --git a/das/aggregator_test.go b/das/aggregator_test.go index 4bc209513e..217315eef0 100644 --- a/das/aggregator_test.go +++ b/das/aggregator_test.go @@ -16,10 +16,10 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/blsSignatures" - - "github.com/ethereum/go-ethereum/log" ) func TestDAS_BasicAggregationLocal(t *testing.T) { diff --git a/das/cache_storage_service.go b/das/cache_storage_service.go index 439ccda086..0ba20ac55b 100644 --- a/das/cache_storage_service.go +++ b/das/cache_storage_service.go @@ -7,14 +7,15 @@ import ( "context" "fmt" - "github.com/offchainlabs/nitro/arbstate/daprovider" - "github.com/offchainlabs/nitro/das/dastree" - "github.com/offchainlabs/nitro/util/pretty" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/arbstate/daprovider" + "github.com/offchainlabs/nitro/das/dastree" + "github.com/offchainlabs/nitro/util/pretty" ) type CacheConfig struct { diff --git a/das/chain_fetch_das.go b/das/chain_fetch_das.go index 465b54f400..34b10d45ec 100644 --- a/das/chain_fetch_das.go +++ b/das/chain_fetch_das.go @@ -8,14 +8,14 @@ import ( "errors" "sync" - "github.com/offchainlabs/nitro/util/pretty" - "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" - "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/util/pretty" ) type syncedKeysetCache struct { @@ -42,7 +42,7 @@ type KeysetFetcher struct { keysetCache syncedKeysetCache } -func NewKeysetFetcher(l1client arbutil.L1Interface, seqInboxAddr common.Address) (*KeysetFetcher, error) { +func NewKeysetFetcher(l1client *ethclient.Client, seqInboxAddr common.Address) (*KeysetFetcher, error) { seqInbox, err := bridgegen.NewSequencerInbox(seqInboxAddr, l1client) if err != nil { return nil, err diff --git a/das/das.go b/das/das.go index 6bd02fbc75..e870761ac2 100644 --- a/das/das.go +++ b/das/das.go @@ -10,10 +10,11 @@ import ( "math" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" - flag "github.com/spf13/pflag" "github.com/offchainlabs/nitro/arbstate/daprovider" ) @@ -41,9 +42,10 @@ type DataAvailabilityConfig struct { LocalCache CacheConfig `koanf:"local-cache"` RedisCache RedisConfig `koanf:"redis-cache"` - LocalDBStorage LocalDBStorageConfig `koanf:"local-db-storage"` - LocalFileStorage LocalFileStorageConfig `koanf:"local-file-storage"` - S3Storage S3StorageServiceConfig `koanf:"s3-storage"` + LocalDBStorage LocalDBStorageConfig `koanf:"local-db-storage"` + LocalFileStorage LocalFileStorageConfig `koanf:"local-file-storage"` + S3Storage S3StorageServiceConfig `koanf:"s3-storage"` + GoogleCloudStorage GoogleCloudStorageServiceConfig `koanf:"google-cloud-storage"` MigrateLocalDBToFileStorage bool `koanf:"migrate-local-db-to-file-storage"` @@ -114,6 +116,7 @@ func dataAvailabilityConfigAddOptions(prefix string, f *flag.FlagSet, r role) { LocalDBStorageConfigAddOptions(prefix+".local-db-storage", f) LocalFileStorageConfigAddOptions(prefix+".local-file-storage", f) S3ConfigAddOptions(prefix+".s3-storage", f) + GoogleCloudConfigAddOptions(prefix+".google-cloud-storage", f) f.Bool(prefix+".migrate-local-db-to-file-storage", DefaultDataAvailabilityConfig.MigrateLocalDBToFileStorage, "daserver will migrate all data on startup from local-db-storage to local-file-storage, then mark local-db-storage as unusable") // Key config for storage diff --git a/das/dasRpcClient.go b/das/dasRpcClient.go index ca2ee8e7d4..3ea6c4e2c6 100644 --- a/das/dasRpcClient.go +++ b/das/dasRpcClient.go @@ -9,18 +9,31 @@ import ( "strings" "time" + "golang.org/x/sync/errgroup" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" - "golang.org/x/sync/errgroup" - + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/blsSignatures" "github.com/offchainlabs/nitro/util/pretty" "github.com/offchainlabs/nitro/util/signature" ) +var ( + rpcClientStoreRequestGauge = metrics.NewRegisteredGauge("arb/das/rpcclient/store/requests", nil) + rpcClientStoreSuccessGauge = metrics.NewRegisteredGauge("arb/das/rpcclient/store/success", nil) + rpcClientStoreFailureGauge = metrics.NewRegisteredGauge("arb/das/rpcclient/store/failure", nil) + rpcClientStoreStoredBytesGauge = metrics.NewRegisteredGauge("arb/das/rpcclient/store/bytes", nil) + rpcClientStoreDurationHistogram = metrics.NewRegisteredHistogram("arb/das/rpcclient/store/duration", nil, metrics.NewBoundedHistogramSample()) + + rpcClientSendChunkSuccessGauge = metrics.NewRegisteredGauge("arb/das/rpcclient/sendchunk/success", nil) + rpcClientSendChunkFailureGauge = metrics.NewRegisteredGauge("arb/das/rpcclient/sendchunk/failure", nil) +) + type DASRPCClient struct { // implements DataAvailabilityService clnt *rpc.Client url string @@ -58,7 +71,20 @@ func NewDASRPCClient(target string, signer signature.DataSignerFunc, maxStoreChu } func (c *DASRPCClient) Store(ctx context.Context, message []byte, timeout uint64) (*daprovider.DataAvailabilityCertificate, error) { - timestamp := uint64(time.Now().Unix()) + rpcClientStoreRequestGauge.Inc(1) + start := time.Now() + success := false + defer func() { + if success { + rpcClientStoreSuccessGauge.Inc(1) + } else { + rpcClientStoreFailureGauge.Inc(1) + } + rpcClientStoreDurationHistogram.Update(time.Since(start).Nanoseconds()) + }() + + // #nosec G115 + timestamp := uint64(start.Unix()) nChunks := uint64(len(message)) / c.chunkSize lastChunkSize := uint64(len(message)) % c.chunkSize if lastChunkSize > 0 { @@ -76,6 +102,7 @@ func (c *DASRPCClient) Store(ctx context.Context, message []byte, timeout uint64 var startChunkedStoreResult StartChunkedStoreResult if err := c.clnt.CallContext(ctx, &startChunkedStoreResult, "das_startChunkedStore", hexutil.Uint64(timestamp), hexutil.Uint64(nChunks), hexutil.Uint64(c.chunkSize), hexutil.Uint64(totalSize), hexutil.Uint64(timeout), hexutil.Bytes(startReqSig)); err != nil { if strings.Contains(err.Error(), "the method das_startChunkedStore does not exist") { + log.Info("Legacy store is used by the DAS client", "url", c.url) return c.legacyStore(ctx, message, timeout) } return nil, err @@ -115,6 +142,9 @@ func (c *DASRPCClient) Store(ctx context.Context, message []byte, timeout uint64 return nil, err } + rpcClientStoreStoredBytesGauge.Inc(int64(len(message))) + success = true + return &daprovider.DataAvailabilityCertificate{ DataHash: common.BytesToHash(storeResult.DataHash), Timeout: uint64(storeResult.Timeout), @@ -132,12 +162,15 @@ func (c *DASRPCClient) sendChunk(ctx context.Context, batchId, i uint64, chunk [ } if err := c.clnt.CallContext(ctx, nil, "das_sendChunk", hexutil.Uint64(batchId), hexutil.Uint64(i), hexutil.Bytes(chunk), hexutil.Bytes(chunkReqSig)); err != nil { + rpcClientSendChunkFailureGauge.Inc(1) return err } + rpcClientSendChunkSuccessGauge.Inc(1) return nil } func (c *DASRPCClient) legacyStore(ctx context.Context, message []byte, timeout uint64) (*daprovider.DataAvailabilityCertificate, error) { + // #nosec G115 log.Trace("das.DASRPCClient.Store(...)", "message", pretty.FirstFewBytes(message), "timeout", time.Unix(int64(timeout), 0), "this", *c) reqSig, err := applyDasSigner(c.signer, message, timeout) diff --git a/das/dasRpcServer.go b/das/dasRpcServer.go index 9e6228ca5d..adddf26571 100644 --- a/das/dasRpcServer.go +++ b/das/dasRpcServer.go @@ -17,7 +17,6 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/rpc" "github.com/offchainlabs/nitro/blsSignatures" @@ -108,6 +107,7 @@ type StoreResult struct { } func (s *DASRPCServer) Store(ctx context.Context, message hexutil.Bytes, timeout hexutil.Uint64, sig hexutil.Bytes) (*StoreResult, error) { + // #nosec G115 log.Trace("dasRpc.DASRPCServer.Store", "message", pretty.FirstFewBytes(message), "message length", len(message), "timeout", time.Unix(int64(timeout), 0), "sig", pretty.FirstFewBytes(sig), "this", s) rpcStoreRequestGauge.Inc(1) start := time.Now() @@ -152,7 +152,7 @@ type SendChunkResult struct { type batch struct { chunks [][]byte expectedChunks uint64 - seenChunks atomic.Int64 + seenChunks atomic.Uint64 expectedChunkSize, expectedSize uint64 timeout uint64 startTime time.Time @@ -247,7 +247,7 @@ func (b *batchBuilder) close(id uint64) ([]byte, uint64, time.Time, error) { return nil, 0, time.Time{}, fmt.Errorf("unknown batch(%d)", id) } - if batch.expectedChunks != uint64(batch.seenChunks.Load()) { + if batch.expectedChunks != batch.seenChunks.Load() { return nil, 0, time.Time{}, fmt.Errorf("incomplete batch(%d): got %d/%d chunks", id, batch.seenChunks.Load(), batch.expectedChunks) } @@ -277,6 +277,7 @@ func (s *DASRPCServer) StartChunkedStore(ctx context.Context, timestamp, nChunks } // Prevent replay of old messages + // #nosec G115 if time.Since(time.Unix(int64(timestamp), 0)).Abs() > time.Minute { return nil, errors.New("too much time has elapsed since request was signed") } diff --git a/das/das_test.go b/das/das_test.go index 179734c8b1..4971d454e5 100644 --- a/das/das_test.go +++ b/das/das_test.go @@ -55,6 +55,7 @@ func testDASStoreRetrieveMultipleInstances(t *testing.T, storageType string) { Require(t, err, "no das") var daReader DataAvailabilityServiceReader = storageService + // #nosec G115 timeout := uint64(time.Now().Add(time.Hour * 24).Unix()) messageSaved := []byte("hello world") cert, err := daWriter.Store(firstCtx, messageSaved, timeout) @@ -146,6 +147,7 @@ func testDASMissingMessage(t *testing.T, storageType string) { var daReader DataAvailabilityServiceReader = storageService messageSaved := []byte("hello world") + // #nosec G115 timeout := uint64(time.Now().Add(time.Hour * 24).Unix()) cert, err := daWriter.Store(ctx, messageSaved, timeout) Require(t, err, "Error storing message") diff --git a/das/dastree/dastree.go b/das/dastree/dastree.go index d873f0568d..29a6b2495c 100644 --- a/das/dastree/dastree.go +++ b/das/dastree/dastree.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -61,12 +62,13 @@ func RecordHash(record func(bytes32, []byte, arbutil.PreimageType), preimage ... return arbmath.FlipBit(keccord(prepend(LeafByte, keccord([]byte{}).Bytes())), 0) } - length := uint32(len(unrolled)) + length := len(unrolled) leaves := []node{} - for bin := uint32(0); bin < length; bin += BinSize { + for bin := 0; bin < length; bin += BinSize { end := arbmath.MinInt(bin+BinSize, length) hash := keccord(prepend(LeafByte, keccord(unrolled[bin:end]).Bytes())) - leaves = append(leaves, node{hash, end - bin}) + // #nosec G115 + leaves = append(leaves, node{hash, uint32(end - bin)}) } layer := leaves @@ -186,7 +188,9 @@ func Content(root bytes32, oracle func(bytes32) ([]byte, error)) ([]byte, error) leaves = append(leaves, leaf) case NodeByte: count := binary.BigEndian.Uint32(data[64:]) - power := uint32(arbmath.NextOrCurrentPowerOf2(uint64(count))) + power := arbmath.NextOrCurrentPowerOf2(uint64(count)) + // #nosec G115 + halfPower := uint32(power / 2) if place.size != count { return nil, fmt.Errorf("invalid size data: %v vs %v for %v", count, place.size, data) @@ -194,11 +198,11 @@ func Content(root bytes32, oracle func(bytes32) ([]byte, error)) ([]byte, error) prior := node{ hash: common.BytesToHash(data[:32]), - size: power / 2, + size: halfPower, } after := node{ hash: common.BytesToHash(data[32:64]), - size: count - power/2, + size: count - halfPower, } // we want to expand leftward so we reverse their order diff --git a/das/dastree/dastree_test.go b/das/dastree/dastree_test.go index 4d24c9ae98..b24d6ce69b 100644 --- a/das/dastree/dastree_test.go +++ b/das/dastree/dastree_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/colors" "github.com/offchainlabs/nitro/util/pretty" diff --git a/das/db_storage_service.go b/das/db_storage_service.go index e3b6183c37..0e38505a13 100644 --- a/das/db_storage_service.go +++ b/das/db_storage_service.go @@ -8,18 +8,21 @@ import ( "context" "errors" "fmt" + "math" "os" "path/filepath" "time" badger "github.com/dgraph-io/badger/v4" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/util/pretty" "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" ) type LocalDBStorageConfig struct { @@ -172,7 +175,8 @@ func (dbs *DBStorageService) Put(ctx context.Context, data []byte, timeout uint6 return dbs.db.Update(func(txn *badger.Txn) error { e := badger.NewEntry(dastree.HashBytes(data), data) - if dbs.discardAfterTimeout { + if dbs.discardAfterTimeout && timeout <= math.MaxInt64 { + // #nosec G115 e = e.WithTTL(time.Until(time.Unix(int64(timeout), 0))) } return txn.SetEntry(e) @@ -265,6 +269,7 @@ func (dbs *DBStorageService) String() string { func (dbs *DBStorageService) HealthCheck(ctx context.Context) error { testData := []byte("Test-Data") + // #nosec G115 err := dbs.Put(ctx, testData, uint64(time.Now().Add(time.Minute).Unix())) if err != nil { return err diff --git a/das/factory.go b/das/factory.go index 5742a39479..3e9771f932 100644 --- a/das/factory.go +++ b/das/factory.go @@ -7,11 +7,10 @@ import ( "context" "errors" "fmt" - "math" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/signature" @@ -66,6 +65,15 @@ func CreatePersistentStorageService( storageServices = append(storageServices, s) } + if config.GoogleCloudStorage.Enable { + s, err := NewGoogleCloudStorageService(config.GoogleCloudStorage) + if err != nil { + return nil, nil, err + } + lifecycleManager.Register(s) + storageServices = append(storageServices, s) + } + if len(storageServices) > 1 { s, err := NewRedundantStorageService(ctx, storageServices) if err != nil { @@ -113,7 +121,7 @@ func CreateBatchPosterDAS( ctx context.Context, config *DataAvailabilityConfig, dataSigner signature.DataSignerFunc, - l1Reader arbutil.L1Interface, + l1Reader *ethclient.Client, sequencerInboxAddr common.Address, ) (DataAvailabilityServiceWriter, DataAvailabilityServiceReader, *KeysetFetcher, *LifecycleManager, error) { if !config.Enable { @@ -187,12 +195,7 @@ func CreateDAComponentsForDaserver( dasLifecycleManager.Register(restAgg) syncConf := &config.RestAggregator.SyncToStorage - var retentionPeriodSeconds uint64 - if uint64(syncConf.RetentionPeriod) == math.MaxUint64 { - retentionPeriodSeconds = math.MaxUint64 - } else { - retentionPeriodSeconds = uint64(syncConf.RetentionPeriod.Seconds()) - } + retentionPeriodSeconds := uint64(syncConf.RetentionPeriod.Seconds()) if syncConf.Eager { if l1Reader == nil || seqInboxAddress == nil { diff --git a/das/fallback_storage_service.go b/das/fallback_storage_service.go index 49f961da60..64bc3c2a7a 100644 --- a/das/fallback_storage_service.go +++ b/das/fallback_storage_service.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/util/arbmath" @@ -85,6 +86,7 @@ func (f *FallbackStorageService) GetByHash(ctx context.Context, key common.Hash) } if dastree.ValidHash(key, data) { putErr := f.StorageService.Put( + // #nosec G115 ctx, data, arbmath.SaturatingUAdd(uint64(time.Now().Unix()), f.backupRetentionSeconds), ) if putErr != nil && !f.ignoreRetentionWriteErrors { diff --git a/das/fallback_storage_service_test.go b/das/fallback_storage_service_test.go index b73df31624..4c7c0351e9 100644 --- a/das/fallback_storage_service_test.go +++ b/das/fallback_storage_service_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common/math" + "github.com/offchainlabs/nitro/das/dastree" ) diff --git a/das/google_cloud_storage_service.go b/das/google_cloud_storage_service.go new file mode 100644 index 0000000000..829f4b5265 --- /dev/null +++ b/das/google_cloud_storage_service.go @@ -0,0 +1,205 @@ +package das + +import ( + "context" + "fmt" + "io" + "math" + "sort" + "time" + + googlestorage "cloud.google.com/go/storage" + "github.com/google/go-cmp/cmp" + flag "github.com/spf13/pflag" + "google.golang.org/api/option" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/arbstate/daprovider" + "github.com/offchainlabs/nitro/das/dastree" + "github.com/offchainlabs/nitro/util/pretty" +) + +type GoogleCloudStorageOperator interface { + Bucket(name string) *googlestorage.BucketHandle + Upload(ctx context.Context, bucket, objectPrefix string, value []byte) error + Download(ctx context.Context, bucket, objectPrefix string, key common.Hash) ([]byte, error) + Close(ctx context.Context) error +} + +type GoogleCloudStorageClient struct { + client *googlestorage.Client +} + +func (g *GoogleCloudStorageClient) Bucket(name string) *googlestorage.BucketHandle { + return g.client.Bucket(name) +} + +func (g *GoogleCloudStorageClient) Upload(ctx context.Context, bucket, objectPrefix string, value []byte) error { + obj := g.client.Bucket(bucket).Object(objectPrefix + EncodeStorageServiceKey(dastree.Hash(value))) + w := obj.NewWriter(ctx) + + if _, err := fmt.Fprintln(w, value); err != nil { + return err + } + return w.Close() + +} + +func (g *GoogleCloudStorageClient) Download(ctx context.Context, bucket, objectPrefix string, key common.Hash) ([]byte, error) { + obj := g.client.Bucket(bucket).Object(objectPrefix + EncodeStorageServiceKey(key)) + reader, err := obj.NewReader(ctx) + if err != nil { + return nil, err + } + return io.ReadAll(reader) +} + +func (g *GoogleCloudStorageClient) Close(ctx context.Context) error { + return g.client.Close() +} + +type GoogleCloudStorageServiceConfig struct { + Enable bool `koanf:"enable"` + AccessToken string `koanf:"access-token"` + Bucket string `koanf:"bucket"` + ObjectPrefix string `koanf:"object-prefix"` + EnableExpiry bool `koanf:"enable-expiry"` + MaxRetention time.Duration `koanf:"max-retention"` +} + +var DefaultGoogleCloudStorageServiceConfig = GoogleCloudStorageServiceConfig{} + +func GoogleCloudConfigAddOptions(prefix string, f *flag.FlagSet) { + f.Bool(prefix+".enable", DefaultGoogleCloudStorageServiceConfig.Enable, "EXPERIMENTAL/unsupported - enable storage/retrieval of sequencer batch data from an Google Cloud Storage bucket") + f.String(prefix+".access-token", DefaultGoogleCloudStorageServiceConfig.AccessToken, "Google Cloud Storage access token") + f.String(prefix+".bucket", DefaultGoogleCloudStorageServiceConfig.Bucket, "Google Cloud Storage bucket") + f.String(prefix+".object-prefix", DefaultGoogleCloudStorageServiceConfig.ObjectPrefix, "prefix to add to Google Cloud Storage objects") + f.Bool(prefix+".enable-expiry", DefaultLocalFileStorageConfig.EnableExpiry, "enable expiry of batches") + f.Duration(prefix+".max-retention", DefaultLocalFileStorageConfig.MaxRetention, "store requests with expiry times farther in the future than max-retention will be rejected") + +} + +type GoogleCloudStorageService struct { + operator GoogleCloudStorageOperator + bucket string + objectPrefix string + enableExpiry bool + maxRetention time.Duration +} + +func NewGoogleCloudStorageService(config GoogleCloudStorageServiceConfig) (StorageService, error) { + var client *googlestorage.Client + var err error + // Note that if the credentials are not specified, the client library will find credentials using ADC(Application Default Credentials) + // https://cloud.google.com/docs/authentication/provide-credentials-adc. + if config.AccessToken == "" { + client, err = googlestorage.NewClient(context.Background()) + } else { + client, err = googlestorage.NewClient(context.Background(), option.WithCredentialsJSON([]byte(config.AccessToken))) + } + if err != nil { + return nil, fmt.Errorf("error creating Google Cloud Storage client: %w", err) + } + service := &GoogleCloudStorageService{ + operator: &GoogleCloudStorageClient{client: client}, + bucket: config.Bucket, + objectPrefix: config.ObjectPrefix, + enableExpiry: config.EnableExpiry, + maxRetention: config.MaxRetention, + } + if config.EnableExpiry { + lifecycleRule := googlestorage.LifecycleRule{ + Action: googlestorage.LifecycleAction{Type: "Delete"}, + Condition: googlestorage.LifecycleCondition{AgeInDays: int64(config.MaxRetention.Hours() / 24)}, // Objects older than 30 days + } + ctx := context.Background() + bucket := service.operator.Bucket(service.bucket) + // check if bucket exists (and others), and update expiration policy if enabled + attrs, err := bucket.Attrs(ctx) + if err != nil { + return nil, fmt.Errorf("error getting bucket attributes: %w", err) + } + attrs.Lifecycle.Rules = append(attrs.Lifecycle.Rules, lifecycleRule) + + bucketAttrsToUpdate := googlestorage.BucketAttrsToUpdate{ + Lifecycle: &attrs.Lifecycle, + } + if _, err := bucket.Update(ctx, bucketAttrsToUpdate); err != nil { + return nil, fmt.Errorf("failed to update bucket lifecycle: %w", err) + } + } + return service, nil +} + +func (gcs *GoogleCloudStorageService) Put(ctx context.Context, data []byte, expiry uint64) error { + logPut("das.GoogleCloudStorageService.Store", data, expiry, gcs) + if expiry > math.MaxInt64 { + return fmt.Errorf("request expiry time (%v) exceeds max int64", expiry) + } + // #nosec G115 + expiryTime := time.Unix(int64(expiry), 0) + currentTimePlusRetention := time.Now().Add(gcs.maxRetention) + if expiryTime.After(currentTimePlusRetention) { + return fmt.Errorf("requested expiry time (%v) exceeds current time plus maximum allowed retention period(%v)", expiryTime, currentTimePlusRetention) + } + if err := gcs.operator.Upload(ctx, gcs.bucket, gcs.objectPrefix, data); err != nil { + log.Error("das.GoogleCloudStorageService.Store", "err", err) + return err + } + return nil +} + +func (gcs *GoogleCloudStorageService) GetByHash(ctx context.Context, key common.Hash) ([]byte, error) { + log.Trace("das.GoogleCloudStorageService.GetByHash", "key", pretty.PrettyHash(key), "this", gcs) + buf, err := gcs.operator.Download(ctx, gcs.bucket, gcs.objectPrefix, key) + if err != nil { + log.Error("das.GoogleCloudStorageService.GetByHash", "err", err) + return nil, err + } + return buf, nil +} + +func (gcs *GoogleCloudStorageService) ExpirationPolicy(ctx context.Context) (daprovider.ExpirationPolicy, error) { + if gcs.enableExpiry { + return daprovider.KeepForever, nil + } + return daprovider.DiscardAfterDataTimeout, nil +} + +func (gcs *GoogleCloudStorageService) Sync(ctx context.Context) error { + return nil +} + +func (gcs *GoogleCloudStorageService) Close(ctx context.Context) error { + return gcs.operator.Close(ctx) +} + +func (gcs *GoogleCloudStorageService) String() string { + return fmt.Sprintf("GoogleCloudStorageService(:%s)", gcs.bucket) +} + +func (gcs *GoogleCloudStorageService) HealthCheck(ctx context.Context) error { + bucket := gcs.operator.Bucket(gcs.bucket) + // check if we have bucket permissions + permissions := []string{ + "storage.buckets.get", + "storage.buckets.list", + "storage.objects.create", + "storage.objects.delete", + "storage.objects.list", + "storage.objects.get", + } + perms, err := bucket.IAM().TestPermissions(ctx, permissions) + if err != nil { + return fmt.Errorf("could not check permissions: %w", err) + } + sort.Strings(permissions) + sort.Strings(perms) + if !cmp.Equal(perms, permissions) { + return fmt.Errorf("permissions mismatch (-want +got):\n%s", cmp.Diff(permissions, perms)) + } + + return nil +} diff --git a/das/google_cloud_storage_service_test.go b/das/google_cloud_storage_service_test.go new file mode 100644 index 0000000000..94d6f3ee44 --- /dev/null +++ b/das/google_cloud_storage_service_test.go @@ -0,0 +1,87 @@ +package das + +import ( + "bytes" + "context" + "errors" + "testing" + "time" + + googlestorage "cloud.google.com/go/storage" + + "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/das/dastree" +) + +type mockGCSClient struct { + storage map[string][]byte +} + +func (c *mockGCSClient) Bucket(name string) *googlestorage.BucketHandle { + return nil +} + +func (c *mockGCSClient) Download(ctx context.Context, bucket, objectPrefix string, key common.Hash) ([]byte, error) { + value, ok := c.storage[objectPrefix+EncodeStorageServiceKey(key)] + if !ok { + return nil, ErrNotFound + } + return value, nil +} + +func (c *mockGCSClient) Close(ctx context.Context) error { + return nil +} + +func (c *mockGCSClient) Upload(ctx context.Context, bucket, objectPrefix string, value []byte) error { + key := objectPrefix + EncodeStorageServiceKey(dastree.Hash(value)) + c.storage[key] = value + return nil +} + +func NewTestGoogleCloudStorageService(ctx context.Context, googleCloudStorageConfig GoogleCloudStorageServiceConfig) (StorageService, error) { + return &GoogleCloudStorageService{ + bucket: googleCloudStorageConfig.Bucket, + objectPrefix: googleCloudStorageConfig.ObjectPrefix, + operator: &mockGCSClient{ + storage: make(map[string][]byte), + }, + maxRetention: googleCloudStorageConfig.MaxRetention, + }, nil +} + +func TestNewGoogleCloudStorageService(t *testing.T) { + ctx := context.Background() + // #nosec G115 + expiry := uint64(time.Now().Add(time.Hour).Unix()) + googleCloudStorageServiceConfig := DefaultGoogleCloudStorageServiceConfig + googleCloudStorageServiceConfig.Enable = true + googleCloudStorageServiceConfig.MaxRetention = time.Hour * 24 + googleCloudService, err := NewTestGoogleCloudStorageService(ctx, googleCloudStorageServiceConfig) + Require(t, err) + + val1 := []byte("The first value") + val1CorrectKey := dastree.Hash(val1) + val2IncorrectKey := dastree.Hash(append(val1, 0)) + + _, err = googleCloudService.GetByHash(ctx, val1CorrectKey) + if !errors.Is(err, ErrNotFound) { + t.Fatal(err) + } + + err = googleCloudService.Put(ctx, val1, expiry) + Require(t, err) + + _, err = googleCloudService.GetByHash(ctx, val2IncorrectKey) + if !errors.Is(err, ErrNotFound) { + t.Fatal(err) + } + + val, err := googleCloudService.GetByHash(ctx, val1CorrectKey) + Require(t, err) + if !bytes.Equal(val, val1) { + t.Fatal(val, val1) + } + +} diff --git a/das/key_utils.go b/das/key_utils.go index 33f29788b6..0262e7f666 100644 --- a/das/key_utils.go +++ b/das/key_utils.go @@ -11,6 +11,7 @@ import ( "os" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/blsSignatures" ) diff --git a/das/local_file_storage_service.go b/das/local_file_storage_service.go index 65ca6fe15c..71c98c7879 100644 --- a/das/local_file_storage_service.go +++ b/das/local_file_storage_service.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "math" "os" "path" "path/filepath" @@ -19,14 +20,16 @@ import ( "syscall" "time" + flag "github.com/spf13/pflag" + "golang.org/x/sys/unix" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/util/pretty" "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" - "golang.org/x/sys/unix" ) type LocalFileStorageConfig struct { @@ -133,6 +136,10 @@ func (s *LocalFileStorageService) GetByHash(ctx context.Context, key common.Hash func (s *LocalFileStorageService) Put(ctx context.Context, data []byte, expiry uint64) error { logPut("das.LocalFileStorageService.Store", data, expiry, s) + if expiry > math.MaxInt64 { + return fmt.Errorf("request expiry time (%v) exceeds max int64", expiry) + } + // #nosec G115 expiryTime := time.Unix(int64(expiry), 0) currentTimePlusRetention := time.Now().Add(s.config.MaxRetention) if expiryTime.After(currentTimePlusRetention) { @@ -182,6 +189,7 @@ func (s *LocalFileStorageService) Put(ctx context.Context, data []byte, expiry u // new flat layout files, set their modification time accordingly. if s.enableLegacyLayout { tv := syscall.Timeval{ + // #nosec G115 Sec: int64(expiry - uint64(s.legacyLayout.retention.Seconds())), Usec: 0, } @@ -371,6 +379,7 @@ func migrate(fl *flatLayout, tl *trieLayout) error { return err } + // #nosec G115 expiryPath := tl.expiryPath(batch.key, uint64(batch.expiry.Unix())) if err = createHardLink(newPath, expiryPath); err != nil { return err diff --git a/das/local_file_storage_service_test.go b/das/local_file_storage_service_test.go index cc27e293e3..8a36664670 100644 --- a/das/local_file_storage_service_test.go +++ b/das/local_file_storage_service_test.go @@ -78,6 +78,7 @@ func TestMigrationNoExpiry(t *testing.T) { Require(t, err) s.enableLegacyLayout = true + // #nosec G115 now := uint64(time.Now().Unix()) err = s.Put(ctx, []byte("a"), now+1) @@ -99,6 +100,7 @@ func TestMigrationNoExpiry(t *testing.T) { getByHashAndCheck(t, s, "a", "b", "c", "d") // Can still iterate by timestamp even if expiry disabled + // #nosec G115 countTimestampEntries(t, &s.layout, time.Unix(int64(now+11), 0), 4) } @@ -120,14 +122,19 @@ func TestMigrationExpiry(t *testing.T) { now := time.Now() // Use increments of expiry divisor in order to span multiple by-expiry-timestamp dirs + // #nosec G115 err = s.Put(ctx, []byte("a"), uint64(now.Add(-2*time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("b"), uint64(now.Add(-1*time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("c"), uint64(now.Add(time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("d"), uint64(now.Add(time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("e"), uint64(now.Add(2*time.Second*expiryDivisor).Unix())) Require(t, err) @@ -170,19 +177,26 @@ func TestExpiryDuplicates(t *testing.T) { now := time.Now() // Use increments of expiry divisor in order to span multiple by-expiry-timestamp dirs + // #nosec G115 err = s.Put(ctx, []byte("a"), uint64(now.Add(-2*time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("a"), uint64(now.Add(-1*time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("a"), uint64(now.Add(time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("d"), uint64(now.Add(time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("e"), uint64(now.Add(2*time.Second*expiryDivisor).Unix())) Require(t, err) + // #nosec G115 err = s.Put(ctx, []byte("f"), uint64(now.Add(3*time.Second*expiryDivisor).Unix())) Require(t, err) // Put the same entry and expiry again, should have no effect + // #nosec G115 err = s.Put(ctx, []byte("f"), uint64(now.Add(3*time.Second*expiryDivisor).Unix())) Require(t, err) diff --git a/das/memory_backed_storage_service.go b/das/memory_backed_storage_service.go index c013b501b9..8a2df28902 100644 --- a/das/memory_backed_storage_service.go +++ b/das/memory_backed_storage_service.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" ) diff --git a/das/panic_wrapper.go b/das/panic_wrapper.go index 3530cb651d..4729792c33 100644 --- a/das/panic_wrapper.go +++ b/das/panic_wrapper.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" ) diff --git a/das/reader_aggregator_strategies.go b/das/reader_aggregator_strategies.go index 8e10d52c16..e072fdd85c 100644 --- a/das/reader_aggregator_strategies.go +++ b/das/reader_aggregator_strategies.go @@ -5,6 +5,7 @@ package das import ( "errors" + "maps" "math/rand" "sort" "sync" @@ -33,10 +34,7 @@ func (s *abstractAggregatorStrategy) update(readers []daprovider.DASReader, stat s.readers = make([]daprovider.DASReader, len(readers)) copy(s.readers, readers) - s.stats = make(map[daprovider.DASReader]readerStats) - for k, v := range stats { - s.stats[k] = v - } + s.stats = maps.Clone(stats) } // Exponentially growing Explore Exploit Strategy diff --git a/das/reader_aggregator_strategies_test.go b/das/reader_aggregator_strategies_test.go index cdb85b25e9..e211ee38fe 100644 --- a/das/reader_aggregator_strategies_test.go +++ b/das/reader_aggregator_strategies_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbstate/daprovider" ) @@ -72,8 +73,10 @@ func TestDAS_SimpleExploreExploit(t *testing.T) { } for i := 0; i < len(was) && doMatch; i++ { - if expected[i].(*dummyReader).int != was[i].(*dummyReader).int { - Fail(t, fmt.Sprintf("expected %d, was %d", expected[i].(*dummyReader).int, was[i].(*dummyReader).int)) + expR, expOK := expected[i].(*dummyReader) + wasR, wasOK := was[i].(*dummyReader) + if !expOK || !wasOK || expR.int != wasR.int { + Fail(t, fmt.Sprintf("expected %d, was %d", expected[i], was[i])) } } } diff --git a/das/redis_storage_service.go b/das/redis_storage_service.go index 210d5cb2d4..cdd18ea974 100644 --- a/das/redis_storage_service.go +++ b/das/redis_storage_service.go @@ -10,17 +10,17 @@ import ( "fmt" "time" + "github.com/redis/go-redis/v9" + flag "github.com/spf13/pflag" "golang.org/x/crypto/sha3" - "github.com/go-redis/redis/v8" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" "github.com/offchainlabs/nitro/util/pretty" "github.com/offchainlabs/nitro/util/redisutil" - flag "github.com/spf13/pflag" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" ) type RedisConfig struct { diff --git a/das/redis_storage_service_test.go b/das/redis_storage_service_test.go index 55f3ecd82c..41ca6bac90 100644 --- a/das/redis_storage_service_test.go +++ b/das/redis_storage_service_test.go @@ -11,11 +11,13 @@ import ( "time" "github.com/alicebob/miniredis/v2" + "github.com/offchainlabs/nitro/das/dastree" ) func TestRedisStorageService(t *testing.T) { ctx := context.Background() + // #nosec G115 timeout := uint64(time.Now().Add(time.Hour).Unix()) baseStorageService := NewMemoryBackedStorageService(ctx) server, err := miniredis.Run() diff --git a/das/redundant_storage_service.go b/das/redundant_storage_service.go index 3158d28076..85274188d6 100644 --- a/das/redundant_storage_service.go +++ b/das/redundant_storage_service.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/util/pretty" ) diff --git a/das/redundant_storage_test.go b/das/redundant_storage_test.go index b56f62ee24..11d3b58264 100644 --- a/das/redundant_storage_test.go +++ b/das/redundant_storage_test.go @@ -17,6 +17,7 @@ const NumServices = 3 func TestRedundantStorageService(t *testing.T) { ctx := context.Background() + // #nosec G115 timeout := uint64(time.Now().Add(time.Hour).Unix()) services := []StorageService{} for i := 0; i < NumServices; i++ { diff --git a/das/restful_client.go b/das/restful_client.go index b65426e7cd..3004ea1b59 100644 --- a/das/restful_client.go +++ b/das/restful_client.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/das/dastree" ) diff --git a/das/restful_server.go b/das/restful_server.go index b1607729e2..6c5e2ec453 100644 --- a/das/restful_server.go +++ b/das/restful_server.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/util/pretty" diff --git a/das/restful_server_test.go b/das/restful_server_test.go index 1d3675749a..e6982f9db5 100644 --- a/das/restful_server_test.go +++ b/das/restful_server_test.go @@ -48,6 +48,7 @@ func TestRestfulClientServer(t *testing.T) { server, port, err := NewRestfulDasServerOnRandomPort(LocalServerAddressForTest, storage) Require(t, err) + // #nosec G115 err = storage.Put(ctx, data, uint64(time.Now().Add(time.Hour).Unix())) Require(t, err) diff --git a/das/rpc_aggregator.go b/das/rpc_aggregator.go index 24a470be5b..916637aac6 100644 --- a/das/rpc_aggregator.go +++ b/das/rpc_aggregator.go @@ -14,14 +14,15 @@ import ( "github.com/knadh/koanf" "github.com/knadh/koanf/providers/confmap" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/blsSignatures" "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/util/metricsutil" "github.com/offchainlabs/nitro/util/signature" - - "github.com/ethereum/go-ethereum/common" - "github.com/offchainlabs/nitro/arbutil" ) type BackendConfig struct { @@ -83,7 +84,7 @@ func NewRPCAggregator(ctx context.Context, config DataAvailabilityConfig, signer return NewAggregator(ctx, config, services) } -func NewRPCAggregatorWithL1Info(config DataAvailabilityConfig, l1client arbutil.L1Interface, seqInboxAddress common.Address, signer signature.DataSignerFunc) (*Aggregator, error) { +func NewRPCAggregatorWithL1Info(config DataAvailabilityConfig, l1client *ethclient.Client, seqInboxAddress common.Address, signer signature.DataSignerFunc) (*Aggregator, error) { services, err := ParseServices(config.RPCAggregator, signer) if err != nil { return nil, err @@ -119,7 +120,7 @@ func ParseServices(config AggregatorConfig, signer signature.DataSignerFunc) ([] return nil, err } - d, err := NewServiceDetails(service, *pubKey, 1< bound { @@ -134,7 +136,9 @@ func (api *ArbDebugAPI) evenlySpaceBlocks(start, end rpc.BlockNumber) (uint64, u return 0, 0, 0, 0, fmt.Errorf("invalid block range: %v to %v", start.Int64(), end.Int64()) } + // #nosec G115 first := uint64(end.Int64() - step*(blocks-1)) // minus 1 to include the fact that we start from the last + // #nosec G115 return first, uint64(step), uint64(end), uint64(blocks), nil } @@ -268,11 +272,13 @@ func (api *ArbDebugAPI) TimeoutQueue(ctx context.Context, blockNum rpc.BlockNumb blockNum, _ = api.blockchain.ClipToPostNitroGenesis(blockNum) queue := TimeoutQueue{ + // #nosec G115 BlockNumber: uint64(blockNum), Tickets: []common.Hash{}, Timeouts: []uint64{}, } + // #nosec G115 state, _, err := stateAndHeader(api.blockchain, uint64(blockNum)) if err != nil { return queue, err diff --git a/execution/gethexec/arb_interface.go b/execution/gethexec/arb_interface.go index 7e43338f08..375d650359 100644 --- a/execution/gethexec/arb_interface.go +++ b/execution/gethexec/arb_interface.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/arbitrum_types" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/timeboost" ) diff --git a/execution/gethexec/block_recorder.go b/execution/gethexec/block_recorder.go index 8879c90702..2e3d51fec9 100644 --- a/execution/gethexec/block_recorder.go +++ b/execution/gethexec/block_recorder.go @@ -6,11 +6,15 @@ import ( "sync" "testing" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/arbitrum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" @@ -25,6 +29,8 @@ import ( // Most recent/advanced header we ever computed (lastHdr) // Hopefully - some recent valid block. For that we always keep one candidate block until it becomes validated. type BlockRecorder struct { + config *BlockRecorderConfig + recordingDatabase *arbitrum.RecordingDatabase execEngine *ExecutionEngine @@ -39,28 +45,53 @@ type BlockRecorder struct { preparedLock sync.Mutex } -func NewBlockRecorder(config *arbitrum.RecordingDatabaseConfig, execEngine *ExecutionEngine, ethDb ethdb.Database) *BlockRecorder { +type BlockRecorderConfig struct { + TrieDirtyCache int `koanf:"trie-dirty-cache"` + TrieCleanCache int `koanf:"trie-clean-cache"` + MaxPrepared int `koanf:"max-prepared"` +} + +var DefaultBlockRecorderConfig = BlockRecorderConfig{ + TrieDirtyCache: 1024, + TrieCleanCache: 16, + MaxPrepared: 1000, +} + +func BlockRecorderConfigAddOptions(prefix string, f *flag.FlagSet) { + f.Int(prefix+".trie-dirty-cache", DefaultBlockRecorderConfig.TrieDirtyCache, "like trie-dirty-cache for the separate, recording database (used for validation)") + f.Int(prefix+".trie-clean-cache", DefaultBlockRecorderConfig.TrieCleanCache, "like trie-clean-cache for the separate, recording database (used for validation)") + f.Int(prefix+".max-prepared", DefaultBlockRecorderConfig.MaxPrepared, "max references to store in the recording database") +} + +func NewBlockRecorder(config *BlockRecorderConfig, execEngine *ExecutionEngine, ethDb ethdb.Database) *BlockRecorder { + dbConfig := arbitrum.RecordingDatabaseConfig{ + TrieDirtyCache: config.TrieDirtyCache, + TrieCleanCache: config.TrieCleanCache, + } recorder := &BlockRecorder{ + config: config, execEngine: execEngine, - recordingDatabase: arbitrum.NewRecordingDatabase(config, ethDb, execEngine.bc), + recordingDatabase: arbitrum.NewRecordingDatabase(&dbConfig, ethDb, execEngine.bc), } execEngine.SetRecorder(recorder) return recorder } -func stateLogFunc(targetHeader, header *types.Header, hasState bool) { - if targetHeader == nil || header == nil { - return - } - gap := targetHeader.Number.Int64() - header.Number.Int64() - step := int64(500) - stage := "computing state" - if !hasState { - step = 3000 - stage = "looking for full block" - } - if (gap >= step) && (gap%step == 0) { - log.Info("Setting up validation", "stage", stage, "current", header.Number, "target", targetHeader.Number) +func stateLogFunc(targetHeader *types.Header) arbitrum.StateBuildingLogFunction { + return func(header *types.Header, hasState bool) { + if targetHeader == nil || header == nil { + return + } + gap := targetHeader.Number.Int64() - header.Number.Int64() + step := int64(500) + stage := "computing state" + if !hasState { + step = 3000 + stage = "looking for full block" + } + if (gap >= step) && (gap%step == 0) { + log.Info("Setting up validation", "stage", stage, "current", header.Number, "target", targetHeader.Number) + } } } @@ -82,7 +113,7 @@ func (r *BlockRecorder) RecordBlockCreation( } } - recordingdb, chaincontext, recordingKV, err := r.recordingDatabase.PrepareRecording(ctx, prevHeader, stateLogFunc) + recordingdb, chaincontext, recordingKV, err := r.recordingDatabase.PrepareRecording(ctx, prevHeader, stateLogFunc(prevHeader)) if err != nil { return nil, err } @@ -128,6 +159,7 @@ func (r *BlockRecorder) RecordBlockCreation( chaincontext, chainConfig, false, + core.MessageReplayMode, ) if err != nil { return nil, err @@ -293,7 +325,7 @@ func (r *BlockRecorder) PrepareForRecord(ctx context.Context, start, end arbutil log.Warn("prepareblocks asked for non-found block", "hdrNum", hdrNum) break } - _, err := r.recordingDatabase.GetOrRecreateState(ctx, header, stateLogFunc) + _, err := r.recordingDatabase.GetOrRecreateState(ctx, header, stateLogFunc(header)) if err != nil { log.Warn("prepareblocks failed to get state for block", "hdrNum", hdrNum, "err", err) break @@ -303,7 +335,7 @@ func (r *BlockRecorder) PrepareForRecord(ctx context.Context, start, end arbutil r.updateLastHdr(header) hdrNum++ } - r.preparedAddTrim(references, 1000) + r.preparedAddTrim(references, r.config.MaxPrepared) return nil } diff --git a/execution/gethexec/blockchain.go b/execution/gethexec/blockchain.go index 996b87a9e6..53b494a3c2 100644 --- a/execution/gethexec/blockchain.go +++ b/execution/gethexec/blockchain.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" @@ -26,20 +27,21 @@ import ( ) type CachingConfig struct { - Archive bool `koanf:"archive"` - BlockCount uint64 `koanf:"block-count"` - BlockAge time.Duration `koanf:"block-age"` - TrieTimeLimit time.Duration `koanf:"trie-time-limit"` - TrieDirtyCache int `koanf:"trie-dirty-cache"` - TrieCleanCache int `koanf:"trie-clean-cache"` - SnapshotCache int `koanf:"snapshot-cache"` - DatabaseCache int `koanf:"database-cache"` - SnapshotRestoreGasLimit uint64 `koanf:"snapshot-restore-gas-limit"` - MaxNumberOfBlocksToSkipStateSaving uint32 `koanf:"max-number-of-blocks-to-skip-state-saving"` - MaxAmountOfGasToSkipStateSaving uint64 `koanf:"max-amount-of-gas-to-skip-state-saving"` - StylusLRUCache uint32 `koanf:"stylus-lru-cache"` - StateScheme string `koanf:"state-scheme"` - StateHistory uint64 `koanf:"state-history"` + Archive bool `koanf:"archive"` + BlockCount uint64 `koanf:"block-count"` + BlockAge time.Duration `koanf:"block-age"` + TrieTimeLimit time.Duration `koanf:"trie-time-limit"` + TrieDirtyCache int `koanf:"trie-dirty-cache"` + TrieCleanCache int `koanf:"trie-clean-cache"` + SnapshotCache int `koanf:"snapshot-cache"` + DatabaseCache int `koanf:"database-cache"` + SnapshotRestoreGasLimit uint64 `koanf:"snapshot-restore-gas-limit"` + MaxNumberOfBlocksToSkipStateSaving uint32 `koanf:"max-number-of-blocks-to-skip-state-saving"` + MaxAmountOfGasToSkipStateSaving uint64 `koanf:"max-amount-of-gas-to-skip-state-saving"` + StylusLRUCacheCapacity uint32 `koanf:"stylus-lru-cache-capacity"` + DisableStylusCacheMetricsCollection bool `koanf:"disable-stylus-cache-metrics-collection"` + StateScheme string `koanf:"state-scheme"` + StateHistory uint64 `koanf:"state-history"` } func CachingConfigAddOptions(prefix string, f *flag.FlagSet) { @@ -54,12 +56,14 @@ func CachingConfigAddOptions(prefix string, f *flag.FlagSet) { f.Uint64(prefix+".snapshot-restore-gas-limit", DefaultCachingConfig.SnapshotRestoreGasLimit, "maximum gas rolled back to recover snapshot") f.Uint32(prefix+".max-number-of-blocks-to-skip-state-saving", DefaultCachingConfig.MaxNumberOfBlocksToSkipStateSaving, "maximum number of blocks to skip state saving to persistent storage (archive node only) -- warning: this option seems to cause issues") f.Uint64(prefix+".max-amount-of-gas-to-skip-state-saving", DefaultCachingConfig.MaxAmountOfGasToSkipStateSaving, "maximum amount of gas in blocks to skip saving state to Persistent storage (archive node only) -- warning: this option seems to cause issues") - f.Uint32(prefix+".stylus-lru-cache", DefaultCachingConfig.StylusLRUCache, "initialized stylus programs to keep in LRU cache") + f.Uint32(prefix+".stylus-lru-cache-capacity", DefaultCachingConfig.StylusLRUCacheCapacity, "capacity, in megabytes, of the LRU cache that keeps initialized stylus programs") + f.Bool(prefix+".disable-stylus-cache-metrics-collection", DefaultCachingConfig.DisableStylusCacheMetricsCollection, "disable metrics collection for the stylus cache") f.String(prefix+".state-scheme", DefaultCachingConfig.StateScheme, "scheme to use for state trie storage (hash, path)") f.Uint64(prefix+".state-history", DefaultCachingConfig.StateHistory, "number of recent blocks to retain state history for (path state-scheme only)") } func getStateHistory(maxBlockSpeed time.Duration) uint64 { + // #nosec G115 return uint64(24 * time.Hour / maxBlockSpeed) } @@ -75,7 +79,7 @@ var DefaultCachingConfig = CachingConfig{ SnapshotRestoreGasLimit: 300_000_000_000, MaxNumberOfBlocksToSkipStateSaving: 0, MaxAmountOfGasToSkipStateSaving: 0, - StylusLRUCache: 256, + StylusLRUCacheCapacity: 256, StateScheme: rawdb.HashScheme, StateHistory: getStateHistory(DefaultSequencerConfig.MaxBlockSpeed), } diff --git a/execution/gethexec/executionengine.go b/execution/gethexec/executionengine.go index 193e8e7414..6756a6c693 100644 --- a/execution/gethexec/executionengine.go +++ b/execution/gethexec/executionengine.go @@ -7,11 +7,12 @@ package gethexec /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #cgo LDFLAGS: ${SRCDIR}/../../target/lib/libstylus.a -ldl -lm #include "arbitrator.h" */ import "C" + import ( "bytes" "context" @@ -26,6 +27,8 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" @@ -33,14 +36,14 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/params" - "github.com/google/uuid" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/execution" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/sharedmetrics" @@ -89,6 +92,8 @@ type ExecutionEngine struct { reorgSequencing bool + disableStylusCacheMetricsCollection bool + prefetchBlock bool cachedL1PriceData *L1PriceData @@ -140,7 +145,7 @@ func (s *ExecutionEngine) MarkFeedStart(to arbutil.MessageIndex) { defer s.cachedL1PriceData.mutex.Unlock() if to < s.cachedL1PriceData.startOfL1PriceDataCache { - log.Info("trying to trim older cache which doesnt exist anymore") + log.Debug("trying to trim older L1 price data cache which doesnt exist anymore") } else if to >= s.cachedL1PriceData.endOfL1PriceDataCache { s.cachedL1PriceData.startOfL1PriceDataCache = 0 s.cachedL1PriceData.endOfL1PriceDataCache = 0 @@ -152,23 +157,44 @@ func (s *ExecutionEngine) MarkFeedStart(to arbutil.MessageIndex) { } } -func (s *ExecutionEngine) Initialize(rustCacheSize uint32, targetConfig *StylusTargetConfig) error { - if rustCacheSize != 0 { - programs.ResizeWasmLruCache(rustCacheSize) +func PopulateStylusTargetCache(targetConfig *StylusTargetConfig) error { + localTarget := rawdb.LocalTarget() + targets := targetConfig.WasmTargets() + var nativeSet bool + for _, target := range targets { + var effectiveStylusTarget string + switch target { + case rawdb.TargetWavm: + // skip wavm target + continue + case rawdb.TargetArm64: + effectiveStylusTarget = targetConfig.Arm64 + case rawdb.TargetAmd64: + effectiveStylusTarget = targetConfig.Amd64 + case rawdb.TargetHost: + effectiveStylusTarget = targetConfig.Host + default: + return fmt.Errorf("unsupported stylus target: %v", target) + } + isNative := target == localTarget + err := programs.SetTarget(target, effectiveStylusTarget, isNative) + if err != nil { + return fmt.Errorf("failed to set stylus target: %w", err) + } + nativeSet = nativeSet || isNative } - var effectiveStylusTarget string - target := rawdb.LocalTarget() - switch target { - case rawdb.TargetArm64: - effectiveStylusTarget = targetConfig.Arm64 - case rawdb.TargetAmd64: - effectiveStylusTarget = targetConfig.Amd64 - case rawdb.TargetHost: - effectiveStylusTarget = targetConfig.Host + if !nativeSet { + return fmt.Errorf("local target %v missing in list of archs %v", localTarget, targets) } - err := programs.SetTarget(target, effectiveStylusTarget, true) - if err != nil { - return fmt.Errorf("Failed to set stylus target: %w", err) + return nil +} + +func (s *ExecutionEngine) Initialize(rustCacheCapacityMB uint32, targetConfig *StylusTargetConfig) error { + if rustCacheCapacityMB != 0 { + programs.SetWasmLruCacheCapacity(arbmath.SaturatingUMul(uint64(rustCacheCapacityMB), 1024*1024)) + } + if err := PopulateStylusTargetCache(targetConfig); err != nil { + return fmt.Errorf("error populating stylus target cache: %w", err) } return nil } @@ -203,6 +229,16 @@ func (s *ExecutionEngine) EnableReorgSequencing() { s.reorgSequencing = true } +func (s *ExecutionEngine) DisableStylusCacheMetricsCollection() { + if s.Started() { + panic("trying to disable stylus cache metrics collection after start") + } + if s.disableStylusCacheMetricsCollection { + panic("trying to disable stylus cache metrics collection when already set") + } + s.disableStylusCacheMetricsCollection = true +} + func (s *ExecutionEngine) EnablePrefetchBlock() { if s.Started() { panic("trying to enable prefetch block after start") @@ -510,6 +546,7 @@ func (s *ExecutionEngine) sequenceTransactionsWithBlockMutex(header *arbostypes. s.bc.Config(), hooks, false, + core.MessageCommitMode, ) if err != nil { return nil, err @@ -685,6 +722,10 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith statedb.StartPrefetcher("TransactionStreamer") defer statedb.StopPrefetcher() + runMode := core.MessageCommitMode + if isMsgForPrefetch { + runMode = core.MessageReplayMode + } block, receipts, err := arbos.ProduceBlock( msg.Message, msg.DelayedMessagesRead, @@ -693,6 +734,7 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith s.bc, s.bc.Config(), isMsgForPrefetch, + runMode, ) return block, statedb, receipts, err @@ -792,7 +834,8 @@ func (s *ExecutionEngine) cacheL1PriceDataOfMsg(seqNum arbutil.MessageIndex, rec gasUsedForL1 += receipts[i].GasUsedForL1 } for _, tx := range block.Transactions() { - callDataUnits += tx.CalldataUnits + _, cachedUnits := tx.GetRawCachedCalldataUnits() + callDataUnits += cachedUnits } } l1GasCharged := gasUsedForL1 * block.BaseFee().Uint64() @@ -907,7 +950,7 @@ func (s *ExecutionEngine) digestMessageWithBlockMutex(num arbutil.MessageIndex, timestamp = time.Unix(int64(timestampInt), 0) timeUntilUpgrade = time.Until(timestamp) } - maxSupportedVersion := params.ArbitrumDevTestChainConfig().ArbitrumChainParams.InitialArbOSVersion + maxSupportedVersion := chaininfo.ArbitrumDevTestChainConfig().ArbitrumChainParams.InitialArbOSVersion logLevel := log.Warn if timeUntilUpgrade < time.Hour*24 { logLevel = log.Error @@ -987,4 +1030,17 @@ func (s *ExecutionEngine) Start(ctx_in context.Context) { } } }) + if !s.disableStylusCacheMetricsCollection { + // periodically update stylus cache metrics + s.LaunchThread(func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Minute): + programs.UpdateWasmCacheMetrics() + } + } + }) + } } diff --git a/execution/gethexec/express_lane_service.go b/execution/gethexec/express_lane_service.go index 0412edfed7..c8339feac7 100644 --- a/execution/gethexec/express_lane_service.go +++ b/execution/gethexec/express_lane_service.go @@ -6,23 +6,31 @@ package gethexec import ( "context" "fmt" + "math" + "math/big" "sync" "time" + "github.com/pkg/errors" + + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/arbitrum" "github.com/ethereum/go-ethereum/arbitrum_types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/timeboost" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/pkg/errors" ) type expressLaneControl struct { @@ -30,35 +38,109 @@ type expressLaneControl struct { controller common.Address } +type transactionPublisher interface { + PublishTimeboostedTransaction(context.Context, *types.Transaction, *arbitrum_types.ConditionalOptions) error +} + type expressLaneService struct { stopwaiter.StopWaiter sync.RWMutex + transactionPublisher transactionPublisher auctionContractAddr common.Address - initialTimestamp time.Time - roundDuration time.Duration - auctionClosing time.Duration + apiBackend *arbitrum.APIBackend + roundTimingInfo timeboost.RoundTimingInfo + earlySubmissionGrace time.Duration chainConfig *params.ChainConfig logs chan []*types.Log - seqClient *ethclient.Client auctionContract *express_lane_auctiongen.ExpressLaneAuction - roundControl lru.BasicLRU[uint64, *expressLaneControl] + roundControl *lru.Cache[uint64, *expressLaneControl] // thread safe messagesBySequenceNumber map[uint64]*timeboost.ExpressLaneSubmission } +type contractAdapter struct { + *filters.FilterAPI + bind.ContractTransactor // We leave this member unset as it is not used. + + apiBackend *arbitrum.APIBackend +} + +func (a *contractAdapter) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + logPointers, err := a.GetLogs(ctx, filters.FilterCriteria(q)) + if err != nil { + return nil, err + } + logs := make([]types.Log, 0, len(logPointers)) + for _, log := range logPointers { + logs = append(logs, *log) + } + return logs, nil +} + +func (a *contractAdapter) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + panic("contractAdapter doesn't implement SubscribeFilterLogs - shouldn't be needed") +} + +func (a *contractAdapter) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + panic("contractAdapter doesn't implement CodeAt - shouldn't be needed") +} + +func (a *contractAdapter) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + var num rpc.BlockNumber = rpc.LatestBlockNumber + if blockNumber != nil { + num = rpc.BlockNumber(blockNumber.Int64()) + } + + state, header, err := a.apiBackend.StateAndHeaderByNumber(ctx, num) + if err != nil { + return nil, err + } + + msg := &core.Message{ + From: call.From, + To: call.To, + Value: big.NewInt(0), + GasLimit: math.MaxUint64, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + Data: call.Data, + AccessList: call.AccessList, + SkipAccountChecks: true, + TxRunMode: core.MessageEthcallMode, // Indicate this is an eth_call + SkipL1Charging: true, // Skip L1 data fees + } + + evm := a.apiBackend.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, nil) + gp := new(core.GasPool).AddGas(math.MaxUint64) + result, err := core.ApplyMessage(evm, msg, gp) + if err != nil { + return nil, err + } + + return result.ReturnData, nil +} + func newExpressLaneService( + transactionPublisher transactionPublisher, + apiBackend *arbitrum.APIBackend, + filterSystem *filters.FilterSystem, auctionContractAddr common.Address, - sequencerClient *ethclient.Client, bc *core.BlockChain, + earlySubmissionGrace time.Duration, ) (*expressLaneService, error) { chainConfig := bc.Config() - auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, sequencerClient) + + var contractBackend bind.ContractBackend = &contractAdapter{filters.NewFilterAPI(filterSystem), nil, apiBackend} + + auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, contractBackend) if err != nil { return nil, err } retries := 0 + pending: - roundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) if err != nil { const maxRetries = 5 if errors.Is(err, bind.ErrNoCode) && retries < maxRetries { @@ -70,18 +152,20 @@ pending: } return nil, err } - initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0) - roundDuration := time.Duration(roundTimingInfo.RoundDurationSeconds) * time.Second - auctionClosingDuration := time.Duration(roundTimingInfo.AuctionClosingSeconds) * time.Second + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { + return nil, err + } + return &expressLaneService{ + transactionPublisher: transactionPublisher, auctionContract: auctionContract, + apiBackend: apiBackend, chainConfig: chainConfig, - initialTimestamp: initialTimestamp, - auctionClosing: auctionClosingDuration, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), // Keep 8 rounds cached. + roundTimingInfo: *roundTimingInfo, + earlySubmissionGrace: earlySubmissionGrace, + roundControl: lru.NewCache[uint64, *expressLaneControl](8), // Keep 8 rounds cached. auctionContractAddr: auctionContractAddr, - roundDuration: roundDuration, - seqClient: sequencerClient, logs: make(chan []*types.Log, 10_000), messagesBySequenceNumber: make(map[uint64]*timeboost.ExpressLaneSubmission), }, nil @@ -93,17 +177,26 @@ func (es *expressLaneService) Start(ctxIn context.Context) { // Log every new express lane auction round. es.LaunchThread(func(ctx context.Context) { log.Info("Watching for new express lane rounds") - now := time.Now() - waitTime := es.roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) - time.Sleep(waitTime) - ticker := time.NewTicker(time.Minute) + waitTime := es.roundTimingInfo.TimeTilNextRound() + // Wait until the next round starts + select { + case <-ctx.Done(): + return + case <-time.After(waitTime): + // First tick happened, now set up regular ticks + } + + ticker := time.NewTicker(es.roundTimingInfo.Round) defer ticker.Stop() + for { select { case <-ctx.Done(): return case t := <-ticker.C: - round := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) + round := es.roundTimingInfo.RoundNumber() + // TODO (BUG?) is there a race here where messages for a new round can come + // in before this tick has been processed? log.Info( "New express lane auction round", "round", round, @@ -120,7 +213,7 @@ func (es *expressLaneService) Start(ctxIn context.Context) { log.Info("Monitoring express lane auction contract") // Monitor for auction resolutions from the auction manager smart contract // and set the express lane controller for the upcoming round accordingly. - latestBlock, err := es.seqClient.HeaderByNumber(ctx, nil) + latestBlock, err := es.apiBackend.HeaderByNumber(ctx, rpc.LatestBlockNumber) if err != nil { // TODO: Should not be a crit. log.Crit("Could not get latest header", "err", err) @@ -131,7 +224,7 @@ func (es *expressLaneService) Start(ctxIn context.Context) { case <-ctx.Done(): return case <-time.After(time.Millisecond * 250): - latestBlock, err := es.seqClient.HeaderByNumber(ctx, nil) + latestBlock, err := es.apiBackend.HeaderByNumber(ctx, rpc.LatestBlockNumber) if err != nil { log.Crit("Could not get latest header", "err", err) } @@ -151,45 +244,66 @@ func (es *expressLaneService) Start(ctxIn context.Context) { } for it.Next() { log.Info( - "New express lane controller assigned", + "AuctionResolved: New express lane controller assigned", "round", it.Event.Round, "controller", it.Event.FirstPriceExpressLaneController, ) - es.Lock() es.roundControl.Add(it.Event.Round, &expressLaneControl{ controller: it.Event.FirstPriceExpressLaneController, sequence: 0, }) - es.Unlock() } + setExpressLaneIterator, err := es.auctionContract.FilterSetExpressLaneController(filterOpts, nil, nil, nil) if err != nil { log.Error("Could not filter express lane controller transfer event", "error", err) continue } + for setExpressLaneIterator.Next() { + if (setExpressLaneIterator.Event.PreviousExpressLaneController == common.Address{}) { + // The ExpressLaneAuction contract emits both AuctionResolved and SetExpressLaneController + // events when an auction is resolved. They contain redundant information so + // the SetExpressLaneController event can be skipped if it's related to a new round, as + // indicated by an empty PreviousExpressLaneController field (a new round has no + // previous controller). + // It is more explicit and thus clearer to use the AuctionResovled event only for the + // new round setup logic and SetExpressLaneController event only for transfers, rather + // than trying to overload everything onto SetExpressLaneController. + continue + } round := setExpressLaneIterator.Event.Round - es.RLock() roundInfo, ok := es.roundControl.Get(round) - es.RUnlock() if !ok { - log.Warn("Could not find round info for express lane controller transfer event", "round", round) + log.Warn("Could not find round info for ExpressLaneConroller transfer event", "round", round) continue } prevController := setExpressLaneIterator.Event.PreviousExpressLaneController if roundInfo.controller != prevController { - log.Warn("New express lane controller did not match previous controller", + log.Warn("Previous ExpressLaneController in SetExpressLaneController event does not match Sequencer previous controller, continuing with transfer to new controller anyway", + "round", round, + "sequencerRoundController", roundInfo.controller, + "previous", setExpressLaneIterator.Event.PreviousExpressLaneController, + "new", setExpressLaneIterator.Event.NewExpressLaneController) + } + if roundInfo.controller == setExpressLaneIterator.Event.NewExpressLaneController { + log.Warn("SetExpressLaneController: Previous and New ExpressLaneControllers are the same, not transferring control.", "round", round, "previous", setExpressLaneIterator.Event.PreviousExpressLaneController, "new", setExpressLaneIterator.Event.NewExpressLaneController) continue } + es.Lock() - newController := setExpressLaneIterator.Event.NewExpressLaneController - es.roundControl.Add(it.Event.Round, &expressLaneControl{ - controller: newController, + // Changes to roundControl by itself are atomic but we need to udpate both roundControl + // and messagesBySequenceNumber atomically here. + es.roundControl.Add(round, &expressLaneControl{ + controller: setExpressLaneIterator.Event.NewExpressLaneController, sequence: 0, }) + // Since the sequence number for this round has been reset to zero, the map of messages + // by sequence number must be reset otherwise old messages would be replayed. + es.messagesBySequenceNumber = make(map[uint64]*timeboost.ExpressLaneSubmission) es.Unlock() } fromBlock = toBlock @@ -199,59 +313,43 @@ func (es *expressLaneService) Start(ctxIn context.Context) { } func (es *expressLaneService) currentRoundHasController() bool { - es.Lock() - defer es.Unlock() - currRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) - control, ok := es.roundControl.Get(currRound) + control, ok := es.roundControl.Get(es.roundTimingInfo.RoundNumber()) if !ok { return false } return control.controller != (common.Address{}) } -func (es *expressLaneService) isWithinAuctionCloseWindow(arrivalTime time.Time) bool { - // Calculate the next round start time - elapsedTime := arrivalTime.Sub(es.initialTimestamp) - elapsedRounds := elapsedTime / es.roundDuration - nextRoundStart := es.initialTimestamp.Add((elapsedRounds + 1) * es.roundDuration) - // Calculate the time to the next round - timeToNextRound := nextRoundStart.Sub(arrivalTime) - // Check if the arrival timestamp is within AUCTION_CLOSING_DURATION of TIME_TO_NEXT_ROUND - return timeToNextRound <= es.auctionClosing -} - // Sequence express lane submission skips validation of the express lane message itself, // as the core validator logic is handled in `validateExpressLaneTx“ func (es *expressLaneService) sequenceExpressLaneSubmission( ctx context.Context, msg *timeboost.ExpressLaneSubmission, - publishTxFn func( - parentCtx context.Context, - tx *types.Transaction, - options *arbitrum_types.ConditionalOptions, - delay bool, - ) error, ) error { es.Lock() defer es.Unlock() + // Although access to roundControl by itself is thread-safe, when the round control is transferred + // we need to reset roundControl and messagesBySequenceNumber atomically, so the following access + // must be within the lock. control, ok := es.roundControl.Get(msg.Round) if !ok { return timeboost.ErrNoOnchainController } + // Check if the submission nonce is too low. - if msg.Sequence < control.sequence { + if msg.SequenceNumber < control.sequence { return timeboost.ErrSequenceNumberTooLow } // Check if a duplicate submission exists already, and reject if so. - if _, exists := es.messagesBySequenceNumber[msg.Sequence]; exists { + if _, exists := es.messagesBySequenceNumber[msg.SequenceNumber]; exists { return timeboost.ErrDuplicateSequenceNumber } // Log an informational warning if the message's sequence number is in the future. - if msg.Sequence > control.sequence { - log.Warn("Received express lane submission with future sequence number", "sequence", msg.Sequence) + if msg.SequenceNumber > control.sequence { + log.Info("Received express lane submission with future sequence number", "SequenceNumber", msg.SequenceNumber) } // Put into the sequence number map. - es.messagesBySequenceNumber[msg.Sequence] = msg + es.messagesBySequenceNumber[msg.SequenceNumber] = msg for { // Get the next message in the sequence. @@ -259,15 +357,14 @@ func (es *expressLaneService) sequenceExpressLaneSubmission( if !exists { break } - if err := publishTxFn( + delete(es.messagesBySequenceNumber, nextMsg.SequenceNumber) + if err := es.transactionPublisher.PublishTimeboostedTransaction( ctx, nextMsg.Transaction, - msg.Options, - false, /* no delay, as it should go through express lane */ + nextMsg.Options, ); err != nil { - // If the tx failed, clear it from the sequence map. - delete(es.messagesBySequenceNumber, msg.Sequence) - return err + // If the tx fails we return an error with all the necessary info for the controller to successfully try again + return fmt.Errorf("express lane transaction of sequence number: %d and transaction hash: %v, failed with an error: %w", nextMsg.SequenceNumber, nextMsg.Transaction.Hash(), err) } // Increase the global round sequence number. control.sequence += 1 @@ -286,13 +383,25 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu if msg.AuctionContractAddress != es.auctionContractAddr { return errors.Wrapf(timeboost.ErrWrongAuctionContract, "msg auction contract address %s does not match sequencer auction contract address %s", msg.AuctionContractAddress, es.auctionContractAddr) } + + for { + currentRound := es.roundTimingInfo.RoundNumber() + if msg.Round == currentRound { + break + } + + timeTilNextRound := es.roundTimingInfo.TimeTilNextRound() + // We allow txs to come in for the next round if it is close enough to that round, + // but we sleep until the round starts. + if msg.Round == currentRound+1 && timeTilNextRound <= es.earlySubmissionGrace { + time.Sleep(timeTilNextRound) + } else { + return errors.Wrapf(timeboost.ErrBadRoundNumber, "express lane tx round %d does not match current round %d", msg.Round, currentRound) + } + } if !es.currentRoundHasController() { return timeboost.ErrNoOnchainController } - currentRound := timeboost.CurrentRound(es.initialTimestamp, es.roundDuration) - if msg.Round != currentRound { - return errors.Wrapf(timeboost.ErrBadRoundNumber, "express lane tx round %d does not match current round %d", msg.Round, currentRound) - } // Reconstruct the message being signed over and recover the sender address. signingMessage, err := msg.ToMessageBytes() if err != nil { @@ -317,8 +426,6 @@ func (es *expressLaneService) validateExpressLaneTx(msg *timeboost.ExpressLaneSu return timeboost.ErrMalformedData } sender := crypto.PubkeyToAddress(*pubkey) - es.RLock() - defer es.RUnlock() control, ok := es.roundControl.Get(msg.Round) if !ok { return timeboost.ErrNoOnchainController diff --git a/execution/gethexec/express_lane_service_test.go b/execution/gethexec/express_lane_service_test.go index 39b7751b4f..86484db62a 100644 --- a/execution/gethexec/express_lane_service_test.go +++ b/execution/gethexec/express_lane_service_test.go @@ -12,17 +12,19 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/arbitrum_types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/timeboost" - "github.com/stretchr/testify/require" ) -var testPriv *ecdsa.PrivateKey +var testPriv, testPriv2 *ecdsa.PrivateKey func init() { privKey, err := crypto.HexToECDSA("93be75cc4df7acbb636b6abe6de2c0446235ac1dc7da9f290a70d83f088b486d") @@ -30,6 +32,20 @@ func init() { panic(err) } testPriv = privKey + privKey2, err := crypto.HexToECDSA("93be75cc4df7acbb636b6abe6de2c0446235ac1dc7da9f290a70d83f088b486e") + if err != nil { + panic(err) + } + testPriv2 = privKey2 +} + +func defaultTestRoundTimingInfo(offset time.Time) timeboost.RoundTimingInfo { + return timeboost.RoundTimingInfo{ + Offset: offset, + Round: time.Minute, + AuctionClosing: time.Second * 15, + ReserveSubmission: time.Second * 15, + } } func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { @@ -45,7 +61,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "nil msg", sub: nil, es: &expressLaneService{ - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, expectedErr: timeboost.ErrMalformedData, }, @@ -53,7 +69,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "nil tx", sub: &timeboost.ExpressLaneSubmission{}, es: &expressLaneService{ - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, expectedErr: timeboost.ErrMalformedData, }, @@ -63,7 +79,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { Transaction: &types.Transaction{}, }, es: &expressLaneService{ - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, expectedErr: timeboost.ErrMalformedData, }, @@ -73,7 +89,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, sub: &timeboost.ExpressLaneSubmission{ ChainId: big.NewInt(2), @@ -89,7 +105,7 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, sub: &timeboost.ExpressLaneSubmission{ ChainId: big.NewInt(1), @@ -103,10 +119,11 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "no onchain controller", es: &expressLaneService{ auctionContractAddr: common.Address{'a'}, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, sub: &timeboost.ExpressLaneSubmission{ ChainId: big.NewInt(1), @@ -120,12 +137,11 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "bad round number", es: &expressLaneService{ auctionContractAddr: common.Address{'a'}, - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, control: expressLaneControl{ controller: common.Address{'b'}, @@ -143,12 +159,11 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "malformed signature", es: &expressLaneService{ auctionContractAddr: common.Address{'a'}, - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, control: expressLaneControl{ controller: common.Address{'b'}, @@ -166,12 +181,11 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "wrong signature", es: &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, control: expressLaneControl{ controller: common.Address{'b'}, @@ -183,34 +197,32 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { name: "not express lane controller", es: &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, control: expressLaneControl{ controller: common.Address{'b'}, }, - sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv), + sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv, 0), expectedErr: timeboost.ErrNotExpressLaneController, }, { name: "OK", es: &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), }, control: expressLaneControl{ controller: crypto.PubkeyToAddress(testPriv.PublicKey), }, - sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv), + sub: buildValidSubmission(t, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv, 0), valid: true, }, } @@ -231,23 +243,89 @@ func Test_expressLaneService_validateExpressLaneTx(t *testing.T) { } } +func Test_expressLaneService_validateExpressLaneTx_gracePeriod(t *testing.T) { + auctionContractAddr := common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6") + es := &expressLaneService{ + auctionContractAddr: auctionContractAddr, + roundTimingInfo: timeboost.RoundTimingInfo{ + Offset: time.Now(), + Round: time.Second * 10, + AuctionClosing: time.Second * 5, + }, + earlySubmissionGrace: time.Second * 2, + chainConfig: ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + }, + roundControl: lru.NewCache[uint64, *expressLaneControl](8), + } + es.roundControl.Add(0, &expressLaneControl{ + controller: crypto.PubkeyToAddress(testPriv.PublicKey), + }) + es.roundControl.Add(1, &expressLaneControl{ + controller: crypto.PubkeyToAddress(testPriv2.PublicKey), + }) + + sub1 := buildValidSubmission(t, auctionContractAddr, testPriv, 0) + err := es.validateExpressLaneTx(sub1) + require.NoError(t, err) + + // Send req for next round + sub2 := buildValidSubmission(t, auctionContractAddr, testPriv2, 1) + err = es.validateExpressLaneTx(sub2) + require.ErrorIs(t, err, timeboost.ErrBadRoundNumber) + + // Sleep til 2 seconds before grace + time.Sleep(time.Second * 6) + err = es.validateExpressLaneTx(sub2) + require.ErrorIs(t, err, timeboost.ErrBadRoundNumber) + + // Send req for next round within grace period + time.Sleep(time.Second * 2) + err = es.validateExpressLaneTx(sub2) + require.NoError(t, err) +} + +type stubPublisher struct { + els *expressLaneService + publishedTxOrder []uint64 +} + +func makeStubPublisher(els *expressLaneService) *stubPublisher { + return &stubPublisher{ + els: els, + publishedTxOrder: make([]uint64, 0), + } +} + +var emptyTx = types.NewTransaction(0, common.MaxAddress, big.NewInt(0), 0, big.NewInt(0), nil) + +func (s *stubPublisher) PublishTimeboostedTransaction(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions) error { + if tx.Hash() != emptyTx.Hash() { + return errors.New("oops, bad tx") + } + control, _ := s.els.roundControl.Get(0) + s.publishedTxOrder = append(s.publishedTxOrder, control.sequence) + return nil + +} + func Test_expressLaneService_sequenceExpressLaneSubmission_nonceTooLow(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() els := &expressLaneService{ messagesBySequenceNumber: make(map[uint64]*timeboost.ExpressLaneSubmission), - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), } + stubPublisher := makeStubPublisher(els) + els.transactionPublisher = stubPublisher els.roundControl.Add(0, &expressLaneControl{ sequence: 1, }) msg := &timeboost.ExpressLaneSubmission{ - Sequence: 0, - } - publishFn := func(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, delay bool) error { - return nil + SequenceNumber: 0, } - err := els.sequenceExpressLaneSubmission(ctx, msg, publishFn) + + err := els.sequenceExpressLaneSubmission(ctx, msg) require.ErrorIs(t, err, timeboost.ErrSequenceNumberTooLow) } @@ -255,27 +333,24 @@ func Test_expressLaneService_sequenceExpressLaneSubmission_duplicateNonce(t *tes ctx, cancel := context.WithCancel(context.Background()) defer cancel() els := &expressLaneService{ - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), messagesBySequenceNumber: make(map[uint64]*timeboost.ExpressLaneSubmission), } + stubPublisher := makeStubPublisher(els) + els.transactionPublisher = stubPublisher els.roundControl.Add(0, &expressLaneControl{ sequence: 1, }) msg := &timeboost.ExpressLaneSubmission{ - Sequence: 2, - } - numPublished := 0 - publishFn := func(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, delay bool) error { - numPublished += 1 - return nil + SequenceNumber: 2, } - err := els.sequenceExpressLaneSubmission(ctx, msg, publishFn) + err := els.sequenceExpressLaneSubmission(ctx, msg) require.NoError(t, err) // Because the message is for a future sequence number, it // should get queued, but not yet published. - require.Equal(t, 0, numPublished) + require.Equal(t, 0, len(stubPublisher.publishedTxOrder)) // Sending it again should give us an error. - err = els.sequenceExpressLaneSubmission(ctx, msg, publishFn) + err = els.sequenceExpressLaneSubmission(ctx, msg) require.ErrorIs(t, err, timeboost.ErrDuplicateSequenceNumber) } @@ -283,113 +358,100 @@ func Test_expressLaneService_sequenceExpressLaneSubmission_outOfOrder(t *testing ctx, cancel := context.WithCancel(context.Background()) defer cancel() els := &expressLaneService{ - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), messagesBySequenceNumber: make(map[uint64]*timeboost.ExpressLaneSubmission), } + stubPublisher := makeStubPublisher(els) + els.transactionPublisher = stubPublisher + els.roundControl.Add(0, &expressLaneControl{ sequence: 1, }) - numPublished := 0 - publishedTxOrder := make([]uint64, 0) - control, _ := els.roundControl.Get(0) - publishFn := func(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, delay bool) error { - numPublished += 1 - publishedTxOrder = append(publishedTxOrder, control.sequence) - return nil - } + messages := []*timeboost.ExpressLaneSubmission{ { - Sequence: 10, + SequenceNumber: 10, + Transaction: emptyTx, }, { - Sequence: 5, + SequenceNumber: 5, + Transaction: emptyTx, }, { - Sequence: 1, + SequenceNumber: 1, + Transaction: emptyTx, }, { - Sequence: 4, + SequenceNumber: 4, + Transaction: emptyTx, }, { - Sequence: 2, + SequenceNumber: 2, + Transaction: emptyTx, }, } for _, msg := range messages { - err := els.sequenceExpressLaneSubmission(ctx, msg, publishFn) + err := els.sequenceExpressLaneSubmission(ctx, msg) require.NoError(t, err) } // We should have only published 2, as we are missing sequence number 3. - require.Equal(t, 2, numPublished) - require.Equal(t, len(messages), len(els.messagesBySequenceNumber)) + require.Equal(t, 2, len(stubPublisher.publishedTxOrder)) + require.Equal(t, 3, len(els.messagesBySequenceNumber)) // Processed txs are deleted - err := els.sequenceExpressLaneSubmission(ctx, &timeboost.ExpressLaneSubmission{Sequence: 3}, publishFn) + err := els.sequenceExpressLaneSubmission(ctx, &timeboost.ExpressLaneSubmission{SequenceNumber: 3, Transaction: emptyTx}) require.NoError(t, err) - require.Equal(t, 5, numPublished) + require.Equal(t, 5, len(stubPublisher.publishedTxOrder)) } func Test_expressLaneService_sequenceExpressLaneSubmission_erroredTx(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() els := &expressLaneService{ - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), messagesBySequenceNumber: make(map[uint64]*timeboost.ExpressLaneSubmission), } els.roundControl.Add(0, &expressLaneControl{ sequence: 1, }) - numPublished := 0 - publishedTxOrder := make([]uint64, 0) - control, _ := els.roundControl.Get(0) - publishFn := func(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, delay bool) error { - if tx == nil { - return errors.New("oops, bad tx") - } - numPublished += 1 - publishedTxOrder = append(publishedTxOrder, control.sequence) - return nil - } + stubPublisher := makeStubPublisher(els) + els.transactionPublisher = stubPublisher + messages := []*timeboost.ExpressLaneSubmission{ { - Sequence: 1, - Transaction: &types.Transaction{}, + SequenceNumber: 1, + Transaction: emptyTx, }, { - Sequence: 3, - Transaction: &types.Transaction{}, + SequenceNumber: 3, + Transaction: emptyTx, }, { - Sequence: 2, - Transaction: nil, + SequenceNumber: 2, + Transaction: types.NewTransaction(0, common.MaxAddress, big.NewInt(0), 0, big.NewInt(0), []byte{1}), }, { - Sequence: 2, - Transaction: &types.Transaction{}, + SequenceNumber: 2, + Transaction: emptyTx, }, } for _, msg := range messages { - if msg.Transaction == nil { - err := els.sequenceExpressLaneSubmission(ctx, msg, publishFn) + if msg.Transaction.Hash() != emptyTx.Hash() { + err := els.sequenceExpressLaneSubmission(ctx, msg) require.ErrorContains(t, err, "oops, bad tx") } else { - err := els.sequenceExpressLaneSubmission(ctx, msg, publishFn) + err := els.sequenceExpressLaneSubmission(ctx, msg) require.NoError(t, err) } } // One tx out of the four should have failed, so we should have only published 3. - require.Equal(t, 3, numPublished) - require.Equal(t, []uint64{1, 2, 3}, publishedTxOrder) + require.Equal(t, 3, len(stubPublisher.publishedTxOrder)) + require.Equal(t, []uint64{1, 2, 3}, stubPublisher.publishedTxOrder) } +// TODO this test is just for RoundTimingInfo func TestIsWithinAuctionCloseWindow(t *testing.T) { initialTimestamp := time.Date(2024, 8, 8, 15, 0, 0, 0, time.UTC) - roundDuration := 1 * time.Minute - auctionClosing := 15 * time.Second - - es := &expressLaneService{ - initialTimestamp: initialTimestamp, - roundDuration: roundDuration, - auctionClosing: auctionClosing, - } + roundTimingInfo := defaultTestRoundTimingInfo(initialTimestamp) tests := []struct { name string @@ -425,9 +487,9 @@ func TestIsWithinAuctionCloseWindow(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := es.isWithinAuctionCloseWindow(tt.arrivalTime) + actual := roundTimingInfo.IsWithinAuctionCloseWindow(tt.arrivalTime) if actual != tt.expectedBool { - t.Errorf("isWithinAuctionCloseWindow(%v) = %v; want %v", tt.arrivalTime, actual, tt.expectedBool) + t.Errorf("IsWithinAuctionCloseWindow(%v) = %v; want %v", tt.arrivalTime, actual, tt.expectedBool) } }) } @@ -438,9 +500,8 @@ func Benchmark_expressLaneService_validateExpressLaneTx(b *testing.B) { addr := crypto.PubkeyToAddress(testPriv.PublicKey) es := &expressLaneService{ auctionContractAddr: common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), - initialTimestamp: time.Now(), - roundDuration: time.Minute, - roundControl: lru.NewBasicLRU[uint64, *expressLaneControl](8), + roundTimingInfo: defaultTestRoundTimingInfo(time.Now()), + roundControl: lru.NewCache[uint64, *expressLaneControl](8), chainConfig: ¶ms.ChainConfig{ ChainID: big.NewInt(1), }, @@ -449,7 +510,7 @@ func Benchmark_expressLaneService_validateExpressLaneTx(b *testing.B) { sequence: 1, controller: addr, }) - sub := buildValidSubmission(b, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv) + sub := buildValidSubmission(b, common.HexToAddress("0x2Aef36410182881a4b13664a1E079762D7F716e6"), testPriv, 0) b.StartTimer() for i := 0; i < b.N; i++ { err := es.validateExpressLaneTx(sub) @@ -498,13 +559,14 @@ func buildValidSubmission( t testing.TB, auctionContractAddr common.Address, privKey *ecdsa.PrivateKey, + round uint64, ) *timeboost.ExpressLaneSubmission { b := &timeboost.ExpressLaneSubmission{ ChainId: big.NewInt(1), AuctionContractAddress: auctionContractAddr, Transaction: types.NewTransaction(0, common.Address{}, big.NewInt(0), 0, big.NewInt(0), nil), Signature: make([]byte, 65), - Round: 0, + Round: round, } data, err := b.ToMessageBytes() require.NoError(t, err) diff --git a/execution/gethexec/forwarder.go b/execution/gethexec/forwarder.go index 9779dec446..e7a829a431 100644 --- a/execution/gethexec/forwarder.go +++ b/execution/gethexec/forwarder.go @@ -14,9 +14,6 @@ import ( "sync/atomic" "time" - "github.com/offchainlabs/nitro/timeboost" - "github.com/offchainlabs/nitro/util/redisutil" - "github.com/offchainlabs/nitro/util/stopwaiter" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/arbitrum" @@ -25,6 +22,10 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + + "github.com/offchainlabs/nitro/timeboost" + "github.com/offchainlabs/nitro/util/redisutil" + "github.com/offchainlabs/nitro/util/stopwaiter" ) type ForwarderConfig struct { diff --git a/execution/gethexec/node.go b/execution/gethexec/node.go index 2f1623d10f..d699b75a20 100644 --- a/execution/gethexec/node.go +++ b/execution/gethexec/node.go @@ -5,19 +5,25 @@ import ( "errors" "fmt" "reflect" + "sort" "sync/atomic" "testing" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/arbitrum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbutil" @@ -25,43 +31,77 @@ import ( "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/dbutil" "github.com/offchainlabs/nitro/util/headerreader" - flag "github.com/spf13/pflag" ) type StylusTargetConfig struct { - Arm64 string `koanf:"arm64"` - Amd64 string `koanf:"amd64"` - Host string `koanf:"host"` + Arm64 string `koanf:"arm64"` + Amd64 string `koanf:"amd64"` + Host string `koanf:"host"` + ExtraArchs []string `koanf:"extra-archs"` + + wasmTargets []ethdb.WasmTarget +} + +func (c *StylusTargetConfig) WasmTargets() []ethdb.WasmTarget { + return c.wasmTargets +} + +func (c *StylusTargetConfig) Validate() error { + targetsSet := make(map[ethdb.WasmTarget]bool, len(c.ExtraArchs)) + for _, arch := range c.ExtraArchs { + target := ethdb.WasmTarget(arch) + if !rawdb.IsSupportedWasmTarget(target) { + return fmt.Errorf("unsupported architecture: %v, possible values: %s, %s, %s, %s", arch, rawdb.TargetWavm, rawdb.TargetArm64, rawdb.TargetAmd64, rawdb.TargetHost) + } + targetsSet[target] = true + } + if !targetsSet[rawdb.TargetWavm] { + return fmt.Errorf("%s target not found in archs list, archs: %v", rawdb.TargetWavm, c.ExtraArchs) + } + targetsSet[rawdb.LocalTarget()] = true + targets := make([]ethdb.WasmTarget, 0, len(c.ExtraArchs)+1) + for target := range targetsSet { + targets = append(targets, target) + } + sort.Slice( + targets, + func(i, j int) bool { + return targets[i] < targets[j] + }) + c.wasmTargets = targets + return nil } var DefaultStylusTargetConfig = StylusTargetConfig{ - Arm64: programs.DefaultTargetDescriptionArm, - Amd64: programs.DefaultTargetDescriptionX86, - Host: "", + Arm64: programs.DefaultTargetDescriptionArm, + Amd64: programs.DefaultTargetDescriptionX86, + Host: "", + ExtraArchs: []string{string(rawdb.TargetWavm)}, } func StylusTargetConfigAddOptions(prefix string, f *flag.FlagSet) { f.String(prefix+".arm64", DefaultStylusTargetConfig.Arm64, "stylus programs compilation target for arm64 linux") f.String(prefix+".amd64", DefaultStylusTargetConfig.Amd64, "stylus programs compilation target for amd64 linux") f.String(prefix+".host", DefaultStylusTargetConfig.Host, "stylus programs compilation target for system other than 64-bit ARM or 64-bit x86") + f.StringSlice(prefix+".extra-archs", DefaultStylusTargetConfig.ExtraArchs, fmt.Sprintf("Comma separated list of extra architectures to cross-compile stylus program to and cache in wasm store (additionally to local target). Currently must include at least %s. (supported targets: %s, %s, %s, %s)", rawdb.TargetWavm, rawdb.TargetWavm, rawdb.TargetArm64, rawdb.TargetAmd64, rawdb.TargetHost)) } type Config struct { - ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` - Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"` - RecordingDatabase arbitrum.RecordingDatabaseConfig `koanf:"recording-database"` - TxPreChecker TxPreCheckerConfig `koanf:"tx-pre-checker" reload:"hot"` - Forwarder ForwarderConfig `koanf:"forwarder"` - ForwardingTarget string `koanf:"forwarding-target"` - SecondaryForwardingTarget []string `koanf:"secondary-forwarding-target"` - Caching CachingConfig `koanf:"caching"` - RPC arbitrum.Config `koanf:"rpc"` - TxLookupLimit uint64 `koanf:"tx-lookup-limit"` - EnablePrefetchBlock bool `koanf:"enable-prefetch-block"` - SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"` - StylusTarget StylusTargetConfig `koanf:"stylus-target"` - BlockMetadataApiCacheSize uint64 `koanf:"block-metadata-api-cache-size"` - BlockMetadataApiBlocksLimit uint64 `koanf:"block-metadata-api-blocks-limit"` + ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` + Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"` + RecordingDatabase BlockRecorderConfig `koanf:"recording-database"` + TxPreChecker TxPreCheckerConfig `koanf:"tx-pre-checker" reload:"hot"` + Forwarder ForwarderConfig `koanf:"forwarder"` + ForwardingTarget string `koanf:"forwarding-target"` + SecondaryForwardingTarget []string `koanf:"secondary-forwarding-target"` + Caching CachingConfig `koanf:"caching"` + RPC arbitrum.Config `koanf:"rpc"` + TxLookupLimit uint64 `koanf:"tx-lookup-limit"` + EnablePrefetchBlock bool `koanf:"enable-prefetch-block"` + SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"` + StylusTarget StylusTargetConfig `koanf:"stylus-target"` + BlockMetadataApiCacheSize uint64 `koanf:"block-metadata-api-cache-size"` + BlockMetadataApiBlocksLimit uint64 `koanf:"block-metadata-api-blocks-limit"` forwardingTarget string } @@ -84,6 +124,9 @@ func (c *Config) Validate() error { if c.forwardingTarget != "" && c.Sequencer.Enable { return errors.New("ForwardingTarget set and sequencer enabled") } + if err := c.StylusTarget.Validate(); err != nil { + return err + } return nil } @@ -91,7 +134,7 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) { arbitrum.ConfigAddOptions(prefix+".rpc", f) SequencerConfigAddOptions(prefix+".sequencer", f) headerreader.AddOptions(prefix+".parent-chain-reader", f) - arbitrum.RecordingDatabaseConfigAddOptions(prefix+".recording-database", f) + BlockRecorderConfigAddOptions(prefix+".recording-database", f) f.String(prefix+".forwarding-target", ConfigDefault.ForwardingTarget, "transaction forwarding target URL, or \"null\" to disable forwarding (iff not sequencer)") f.StringSlice(prefix+".secondary-forwarding-target", ConfigDefault.SecondaryForwardingTarget, "secondary transaction forwarding target URL") AddOptionsForNodeForwarderConfig(prefix+".forwarder", f) @@ -109,7 +152,7 @@ var ConfigDefault = Config{ RPC: arbitrum.DefaultConfig, Sequencer: DefaultSequencerConfig, ParentChainReader: headerreader.DefaultConfig, - RecordingDatabase: arbitrum.DefaultRecordingDatabaseConfig, + RecordingDatabase: DefaultBlockRecorderConfig, ForwardingTarget: "", SecondaryForwardingTarget: []string{}, TxPreChecker: DefaultTxPreCheckerConfig, @@ -146,7 +189,7 @@ func CreateExecutionNode( stack *node.Node, chainDB ethdb.Database, l2BlockChain *core.BlockChain, - l1client arbutil.L1Interface, + l1client *ethclient.Client, configFetcher ConfigFetcher, ) (*ExecutionNode, error) { config := configFetcher() @@ -154,6 +197,9 @@ func CreateExecutionNode( if config.EnablePrefetchBlock { execEngine.EnablePrefetchBlock() } + if config.Caching.DisableStylusCacheMetricsCollection { + execEngine.DisableStylusCacheMetricsCollection() + } if err != nil { return nil, err } @@ -297,7 +343,7 @@ func (n *ExecutionNode) MarkFeedStart(to arbutil.MessageIndex) { func (n *ExecutionNode) Initialize(ctx context.Context) error { config := n.ConfigFetcher() - err := n.ExecEngine.Initialize(config.Caching.StylusLRUCache, &config.StylusTarget) + err := n.ExecEngine.Initialize(config.Caching.StylusLRUCacheCapacity, &config.StylusTarget) if err != nil { return fmt.Errorf("error initializing execution engine: %w", err) } diff --git a/execution/gethexec/sequencer.go b/execution/gethexec/sequencer.go index 7a0dd5e98a..ab77dc5e69 100644 --- a/execution/gethexec/sequencer.go +++ b/execution/gethexec/sequencer.go @@ -15,12 +15,6 @@ import ( "sync/atomic" "time" - "github.com/offchainlabs/nitro/arbutil" - "github.com/offchainlabs/nitro/execution" - "github.com/offchainlabs/nitro/timeboost" - "github.com/offchainlabs/nitro/util/arbmath" - "github.com/offchainlabs/nitro/util/containers" - "github.com/offchainlabs/nitro/util/headerreader" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/arbitrum" @@ -32,15 +26,21 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto/kzg4844" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/execution" + "github.com/offchainlabs/nitro/timeboost" + "github.com/offchainlabs/nitro/util/arbmath" + "github.com/offchainlabs/nitro/util/containers" + "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/stopwaiter" ) @@ -90,6 +90,7 @@ type TimeboostConfig struct { AuctioneerAddress string `koanf:"auctioneer-address"` ExpressLaneAdvantage time.Duration `koanf:"express-lane-advantage"` SequencerHTTPEndpoint string `koanf:"sequencer-http-endpoint"` + EarlySubmissionGrace time.Duration `koanf:"early-submission-grace"` } var DefaultTimeboostConfig = TimeboostConfig{ @@ -98,6 +99,7 @@ var DefaultTimeboostConfig = TimeboostConfig{ AuctioneerAddress: "", ExpressLaneAdvantage: time.Millisecond * 200, SequencerHTTPEndpoint: "http://localhost:8547", + EarlySubmissionGrace: time.Second * 2, } func (c *SequencerConfig) Validate() error { @@ -191,6 +193,7 @@ func TimeboostAddOptions(prefix string, f *flag.FlagSet) { f.String(prefix+".auctioneer-address", DefaultTimeboostConfig.AuctioneerAddress, "Address of the Timeboost Autonomous Auctioneer") f.Duration(prefix+".express-lane-advantage", DefaultTimeboostConfig.ExpressLaneAdvantage, "specify the express lane advantage") f.String(prefix+".sequencer-http-endpoint", DefaultTimeboostConfig.SequencerHTTPEndpoint, "this sequencer's http endpoint") + f.Duration(prefix+".early-submission-grace", DefaultTimeboostConfig.EarlySubmissionGrace, "period of time before the next round where submissions for the next round will be queued") } type txQueueItem struct { @@ -333,12 +336,36 @@ func (c nonceFailureCache) Add(err NonceError, queueItem txQueueItem) { } } +type synchronizedTxQueue struct { + queue containers.Queue[txQueueItem] + mutex sync.RWMutex +} + +func (q *synchronizedTxQueue) Push(item txQueueItem) { + q.mutex.Lock() + q.queue.Push(item) + q.mutex.Unlock() +} + +func (q *synchronizedTxQueue) Pop() txQueueItem { + q.mutex.Lock() + defer q.mutex.Unlock() + return q.queue.Pop() + +} + +func (q *synchronizedTxQueue) Len() int { + q.mutex.RLock() + defer q.mutex.RUnlock() + return q.queue.Len() +} + type Sequencer struct { stopwaiter.StopWaiter execEngine *ExecutionEngine txQueue chan txQueueItem - txRetryQueue containers.Queue[txQueueItem] + txRetryQueue synchronizedTxQueue l1Reader *headerreader.HeaderReader config SequencerConfigFetcher senderWhitelist map[common.Address]struct{} @@ -362,7 +389,7 @@ type Sequencer struct { expectedSurplus int64 expectedSurplusUpdated bool auctioneerAddr common.Address - timeboostAuctionResolutionTxQueue containers.Queue[txQueueItem] + timeboostAuctionResolutionTxQueue chan txQueueItem } func NewSequencer(execEngine *ExecutionEngine, l1Reader *headerreader.HeaderReader, configFetcher SequencerConfigFetcher) (*Sequencer, error) { @@ -378,15 +405,16 @@ func NewSequencer(execEngine *ExecutionEngine, l1Reader *headerreader.HeaderRead senderWhitelist[common.HexToAddress(address)] = struct{}{} } s := &Sequencer{ - execEngine: execEngine, - txQueue: make(chan txQueueItem, config.QueueSize), - l1Reader: l1Reader, - config: configFetcher, - senderWhitelist: senderWhitelist, - nonceCache: newNonceCache(config.NonceCacheSize), - l1Timestamp: 0, - pauseChan: nil, - onForwarderSet: make(chan struct{}, 1), + execEngine: execEngine, + txQueue: make(chan txQueueItem, config.QueueSize), + l1Reader: l1Reader, + config: configFetcher, + senderWhitelist: senderWhitelist, + nonceCache: newNonceCache(config.NonceCacheSize), + l1Timestamp: 0, + pauseChan: nil, + onForwarderSet: make(chan struct{}, 1), + timeboostAuctionResolutionTxQueue: make(chan txQueueItem, 10), // There should never be more than 1 outstanding auction resolutions } s.nonceFailures = &nonceFailureCache{ containers.NewLruCacheWithOnEvict(config.NonceCacheSize, s.onNonceFailureEvict), @@ -434,10 +462,14 @@ func ctxWithTimeout(ctx context.Context, timeout time.Duration) (context.Context } func (s *Sequencer) PublishTransaction(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions) error { - return s.publishTransactionImpl(parentCtx, tx, options, true /* delay tx if express lane is active */) + return s.publishTransactionImpl(parentCtx, tx, options, false /* delay tx if express lane is active */) } -func (s *Sequencer) publishTransactionImpl(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, delay bool) error { +func (s *Sequencer) PublishTimeboostedTransaction(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions) error { + return s.publishTransactionImpl(parentCtx, tx, options, true) +} + +func (s *Sequencer) publishTransactionImpl(parentCtx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions, isExpressLaneController bool) error { config := s.config() // Only try to acquire Rlock and check for hard threshold if l1reader is not nil // And hard threshold was enabled, this prevents spamming of read locks when not needed @@ -483,7 +515,7 @@ func (s *Sequencer) publishTransactionImpl(parentCtx context.Context, tx *types. } if s.config().Timeboost.Enable && s.expressLaneService != nil { - if delay && s.expressLaneService.currentRoundHasController() { + if !isExpressLaneController && s.expressLaneService.currentRoundHasController() { time.Sleep(s.config().Timeboost.ExpressLaneAdvantage) } } @@ -505,7 +537,7 @@ func (s *Sequencer) publishTransactionImpl(parentCtx context.Context, tx *types. &atomic.Bool{}, queueCtx, time.Now(), - !delay, + isExpressLaneController, } select { case s.txQueue <- queueItem: @@ -538,7 +570,7 @@ func (s *Sequencer) PublishExpressLaneTransaction(ctx context.Context, msg *time if err := s.expressLaneService.validateExpressLaneTx(msg); err != nil { return err } - return s.expressLaneService.sequenceExpressLaneSubmission(ctx, msg, s.publishTransactionImpl) + return s.expressLaneService.sequenceExpressLaneSubmission(ctx, msg) } func (s *Sequencer) PublishAuctionResolutionTransaction(ctx context.Context, tx *types.Transaction) error { @@ -564,7 +596,7 @@ func (s *Sequencer) PublishAuctionResolutionTransaction(ctx context.Context, tx if sender != auctioneerAddr { return fmt.Errorf("sender %#x is not the auctioneer address %#x", sender, auctioneerAddr) } - if !s.expressLaneService.isWithinAuctionCloseWindow(arrivalTime) { + if !s.expressLaneService.roundTimingInfo.IsWithinAuctionCloseWindow(arrivalTime) { return fmt.Errorf("transaction arrival time not within auction closure window: %v", arrivalTime) } txBytes, err := tx.MarshalBinary() @@ -572,7 +604,7 @@ func (s *Sequencer) PublishAuctionResolutionTransaction(ctx context.Context, tx return err } log.Info("Prioritizing auction resolution transaction from auctioneer", "txHash", tx.Hash().Hex()) - s.timeboostAuctionResolutionTxQueue.Push(txQueueItem{ + s.timeboostAuctionResolutionTxQueue <- txQueueItem{ tx: tx, txSize: len(txBytes), options: nil, @@ -581,7 +613,7 @@ func (s *Sequencer) PublishAuctionResolutionTransaction(ctx context.Context, tx ctx: context.TODO(), firstAppearance: time.Now(), isTimeboosted: true, - }) + } return nil } @@ -909,10 +941,12 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { for { var queueItem txQueueItem - if s.timeboostAuctionResolutionTxQueue.Len() > 0 { - queueItem = s.timeboostAuctionResolutionTxQueue.Pop() - log.Info("Popped the auction resolution tx", queueItem.tx.Hash()) - } else if s.txRetryQueue.Len() > 0 { + + if s.txRetryQueue.Len() > 0 { + // The txRetryQueue is not modeled as a channel because it is only added to from + // this function (Sequencer.createBlock). So it is sufficient to check its + // len at the start of this loop, since items can't be added to it asynchronously, + // which is not true for the main txQueue or timeboostAuctionResolutionQueue. queueItem = s.txRetryQueue.Pop() } else if len(queueItems) == 0 { var nextNonceExpiryChan <-chan time.Time @@ -921,6 +955,8 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { } select { case queueItem = <-s.txQueue: + case queueItem = <-s.timeboostAuctionResolutionTxQueue: + log.Info("Popped the auction resolution tx", "txHash", queueItem.tx.Hash()) case <-nextNonceExpiryChan: // No need to stop the previous timer since it already elapsed nextNonceExpiryTimer = s.expireNonceFailures() @@ -939,6 +975,8 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { done := false select { case queueItem = <-s.txQueue: + case queueItem = <-s.timeboostAuctionResolutionTxQueue: + log.Info("Popped the auction resolution tx", "txHash", queueItem.tx.Hash()) default: done = true } @@ -1010,11 +1048,12 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { for _, queueItem := range queueItems { s.txRetryQueue.Push(queueItem) } + // #nosec G115 log.Error( "cannot sequence: unknown L1 block or L1 timestamp too far from local clock time", "l1Block", l1Block, "l1Timestamp", time.Unix(int64(l1Timestamp), 0), - "localTimestamp", time.Unix(int64(timestamp), 0), + "localTimestamp", time.Unix(timestamp, 0), ) return true } @@ -1023,7 +1062,7 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { Kind: arbostypes.L1MessageType_L2Message, Poster: l1pricing.BatchPosterAddress, BlockNumber: l1Block, - Timestamp: uint64(timestamp), + Timestamp: arbmath.SaturatingUCast[uint64](timestamp), RequestId: nil, L1BaseFee: nil, } @@ -1160,10 +1199,14 @@ func (s *Sequencer) updateExpectedSurplus(ctx context.Context) (int64, error) { if err != nil { return 0, fmt.Errorf("error encountered getting l1 pricing surplus while updating expectedSurplus: %w", err) } + // #nosec G115 backlogL1GasCharged := int64(s.execEngine.backlogL1GasCharged()) + // #nosec G115 backlogCallDataUnits := int64(s.execEngine.backlogCallDataUnits()) + // #nosec G115 expectedSurplus := int64(surplus) + backlogL1GasCharged - backlogCallDataUnits*int64(l1GasPrice) // update metrics + // #nosec G115 l1GasPriceGauge.Update(int64(l1GasPrice)) callDataUnitsBacklogGauge.Update(backlogCallDataUnits) unusedL1GasChargeGauge.Update(backlogL1GasCharged) @@ -1244,20 +1287,25 @@ func (s *Sequencer) Start(ctxIn context.Context) error { return nil } -func (s *Sequencer) StartExpressLane(ctx context.Context, auctionContractAddr common.Address, auctioneerAddr common.Address) { +func (s *Sequencer) StartExpressLane( + ctx context.Context, + apiBackend *arbitrum.APIBackend, + filterSystem *filters.FilterSystem, + auctionContractAddr common.Address, + auctioneerAddr common.Address, + earlySubmissionGrace time.Duration, +) { if !s.config().Timeboost.Enable { log.Crit("Timeboost is not enabled, but StartExpressLane was called") } - rpcClient, err := rpc.DialContext(ctx, s.config().Timeboost.SequencerHTTPEndpoint) - if err != nil { - log.Crit("Failed to connect to sequencer RPC client", "err", err) - } - seqClient := ethclient.NewClient(rpcClient) els, err := newExpressLaneService( + s, + apiBackend, + filterSystem, auctionContractAddr, - seqClient, s.execEngine.bc, + earlySubmissionGrace, ) if err != nil { log.Crit("Failed to create express lane service", "err", err, "auctionContractAddr", auctionContractAddr) @@ -1272,11 +1320,18 @@ func (s *Sequencer) StopAndWait() { if s.config().Timeboost.Enable && s.expressLaneService != nil { s.expressLaneService.StopAndWait() } - if s.txRetryQueue.Len() == 0 && len(s.txQueue) == 0 && s.nonceFailures.Len() == 0 { + if s.txRetryQueue.Len() == 0 && + len(s.txQueue) == 0 && + s.nonceFailures.Len() == 0 && + len(s.timeboostAuctionResolutionTxQueue) == 0 { return } // this usually means that coordinator's safe-shutdown-delay is too low - log.Warn("Sequencer has queued items while shutting down", "txQueue", len(s.txQueue), "retryQueue", s.txRetryQueue.Len(), "nonceFailures", s.nonceFailures.Len()) + log.Warn("Sequencer has queued items while shutting down", + "txQueue", len(s.txQueue), + "retryQueue", s.txRetryQueue.Len(), + "nonceFailures", s.nonceFailures.Len(), + "timeboostAuctionResolutionTxQueue", len(s.timeboostAuctionResolutionTxQueue)) _, forwarder := s.GetPauseAndForwarder() if forwarder != nil { var wg sync.WaitGroup @@ -1297,6 +1352,8 @@ func (s *Sequencer) StopAndWait() { select { case item = <-s.txQueue: source = "txQueue" + case item = <-s.timeboostAuctionResolutionTxQueue: + source = "timeboostAuctionResolutionTxQueue" default: break emptyqueues } diff --git a/execution/gethexec/stylus_tracer.go b/execution/gethexec/stylus_tracer.go new file mode 100644 index 0000000000..8a024941d3 --- /dev/null +++ b/execution/gethexec/stylus_tracer.go @@ -0,0 +1,200 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package gethexec + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "strings" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/util/containers" +) + +func init() { + tracers.DefaultDirectory.Register("stylusTracer", newStylusTracer, false) +} + +// stylusTracer captures Stylus HostIOs and returns them in a structured format to be used in Cargo +// Stylus Replay. +type stylusTracer struct { + open *containers.Stack[HostioTraceInfo] + stack *containers.Stack[*containers.Stack[HostioTraceInfo]] + interrupt atomic.Bool + reason error +} + +// HostioTraceInfo contains the captured HostIO log returned by stylusTracer. +type HostioTraceInfo struct { + // Name of the HostIO. + Name string `json:"name"` + + // Arguments of the HostIO encoded as binary. + // For details about the encoding check the HostIO implemenation on + // arbitrator/wasm-libraries/user-host-trait. + Args hexutil.Bytes `json:"args"` + + // Outputs of the HostIO encoded as binary. + // For details about the encoding check the HostIO implemenation on + // arbitrator/wasm-libraries/user-host-trait. + Outs hexutil.Bytes `json:"outs"` + + // Amount of Ink before executing the HostIO. + StartInk uint64 `json:"startInk"` + + // Amount of Ink after executing the HostIO. + EndInk uint64 `json:"endInk"` + + // For *call HostIOs, the address of the called contract. + Address *common.Address `json:"address,omitempty"` + + // For *call HostIOs, the steps performed by the called contract. + Steps *containers.Stack[HostioTraceInfo] `json:"steps,omitempty"` +} + +// nestsHostios contains the hostios with nested calls. +var nestsHostios = map[string]bool{ + "call_contract": true, + "delegate_call_contract": true, + "static_call_contract": true, +} + +func newStylusTracer(ctx *tracers.Context, _ json.RawMessage) (*tracers.Tracer, error) { + t := &stylusTracer{ + open: containers.NewStack[HostioTraceInfo](), + stack: containers.NewStack[*containers.Stack[HostioTraceInfo]](), + } + + return &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnEnter: t.OnEnter, + OnExit: t.OnExit, + CaptureStylusHostio: t.CaptureStylusHostio, + }, + GetResult: t.GetResult, + Stop: t.Stop, + }, nil +} + +func (t *stylusTracer) CaptureStylusHostio(name string, args, outs []byte, startInk, endInk uint64) { + if t.interrupt.Load() { + return + } + info := HostioTraceInfo{ + Name: name, + Args: args, + Outs: outs, + StartInk: startInk, + EndInk: endInk, + } + if nestsHostios[name] { + last, err := t.open.Pop() + if err != nil { + t.Stop(err) + return + } + if !strings.HasPrefix(last.Name, "evm_") || last.Name[4:] != info.Name { + t.Stop(fmt.Errorf("trace inconsistency for %v: last opcode is %v", info.Name, last.Name)) + return + } + if last.Steps == nil { + t.Stop(fmt.Errorf("trace inconsistency for %v: nil steps", info.Name)) + return + } + info.Address = last.Address + info.Steps = last.Steps + } + t.open.Push(info) +} +func (t *stylusTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if t.interrupt.Load() { + return + } + if depth == 0 { + return + } + + // This function adds the prefix evm_ because it assumes the opcode came from the EVM. + // If the opcode comes from WASM, the CaptureStylusHostio function will remove the evm prefix. + var name string + switch vm.OpCode(typ) { + case vm.CALL: + name = "evm_call_contract" + case vm.DELEGATECALL: + name = "evm_delegate_call_contract" + case vm.STATICCALL: + name = "evm_static_call_contract" + case vm.CREATE: + name = "evm_create1" + case vm.CREATE2: + name = "evm_create2" + case vm.SELFDESTRUCT: + name = "evm_self_destruct" + } + + inner := containers.NewStack[HostioTraceInfo]() + info := HostioTraceInfo{ + Name: name, + Address: &to, + Steps: inner, + } + t.open.Push(info) + t.stack.Push(t.open) + t.open = inner +} + +func (t *stylusTracer) OnExit(depth int, output []byte, gasUsed uint64, _ error, reverted bool) { + if t.interrupt.Load() { + return + } + if depth == 0 { + return + } + var err error + t.open, err = t.stack.Pop() + if err != nil { + t.Stop(err) + } +} + +func (t *stylusTracer) GetResult() (json.RawMessage, error) { + if t.reason != nil { + return nil, t.reason + } + + var internalErr error + if t.open == nil { + internalErr = errors.Join(internalErr, fmt.Errorf("tracer.open is nil")) + } + if t.stack == nil { + internalErr = errors.Join(internalErr, fmt.Errorf("tracer.stack is nil")) + } + if !t.stack.Empty() { + internalErr = errors.Join(internalErr, fmt.Errorf("tracer.stack should be empty, but has %d values", t.stack.Len())) + } + if internalErr != nil { + log.Error("stylusTracer: internal error when generating a trace", "error", internalErr) + return nil, fmt.Errorf("internal error: %w", internalErr) + } + + msg, err := json.Marshal(t.open) + if err != nil { + return nil, err + } + return msg, nil +} + +func (t *stylusTracer) Stop(err error) { + t.reason = err + t.interrupt.Store(true) +} diff --git a/execution/gethexec/sync_monitor.go b/execution/gethexec/sync_monitor.go index 86949c7767..7f04b2ee4a 100644 --- a/execution/gethexec/sync_monitor.go +++ b/execution/gethexec/sync_monitor.go @@ -3,9 +3,10 @@ package gethexec import ( "context" - "github.com/offchainlabs/nitro/execution" "github.com/pkg/errors" flag "github.com/spf13/pflag" + + "github.com/offchainlabs/nitro/execution" ) type SyncMonitorConfig struct { diff --git a/execution/gethexec/tx_pre_checker.go b/execution/gethexec/tx_pre_checker.go index c8e27a8bd1..4c1270fa0d 100644 --- a/execution/gethexec/tx_pre_checker.go +++ b/execution/gethexec/tx_pre_checker.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/arbitrum_types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -15,12 +17,12 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/timeboost" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/headerreader" - flag "github.com/spf13/pflag" ) var ( @@ -162,6 +164,7 @@ func PreCheckTx(bc *core.BlockChain, chainConfig *params.ChainConfig, header *ty oldHeader := header blocksTraversed := uint(0) // find a block that's old enough + // #nosec G115 for now-int64(oldHeader.Time) < config.RequiredStateAge && (config.RequiredStateMaxBlocks <= 0 || blocksTraversed < config.RequiredStateMaxBlocks) && oldHeader.Number.Uint64() > 0 { diff --git a/execution/gethexec/wasmstorerebuilder.go b/execution/gethexec/wasmstorerebuilder.go index dcbee45a3f..b40a7cd128 100644 --- a/execution/gethexec/wasmstorerebuilder.go +++ b/execution/gethexec/wasmstorerebuilder.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" + "github.com/offchainlabs/nitro/arbos/arbosState" ) @@ -59,9 +60,14 @@ func WriteToKeyValueStore[T any](store ethdb.KeyValueStore, key []byte, val T) e // It also stores a special value that is only set once when rebuilding commenced in RebuildingStartBlockHashKey as the block // time of the latest block when rebuilding was first called, this is used to avoid recomputing of assembly and module of // contracts that were created after rebuilding commenced since they would anyway already be added during sync. -func RebuildWasmStore(ctx context.Context, wasmStore ethdb.KeyValueStore, chainDb ethdb.Database, maxRecreateStateDepth int64, l2Blockchain *core.BlockChain, position, rebuildingStartBlockHash common.Hash) error { +func RebuildWasmStore(ctx context.Context, wasmStore ethdb.KeyValueStore, chainDb ethdb.Database, maxRecreateStateDepth int64, targetConfig *StylusTargetConfig, l2Blockchain *core.BlockChain, position, rebuildingStartBlockHash common.Hash) error { var err error var stateDb *state.StateDB + + if err := PopulateStylusTargetCache(targetConfig); err != nil { + return fmt.Errorf("error populating stylus target cache: %w", err) + } + latestHeader := l2Blockchain.CurrentBlock() // Attempt to get state at the start block when rebuilding commenced, if not available (in case of non-archival nodes) use latest state rebuildingStartHeader := l2Blockchain.GetHeaderByHash(rebuildingStartBlockHash) diff --git a/execution/interface.go b/execution/interface.go index 01f71d4422..dfe5dd27b0 100644 --- a/execution/interface.go +++ b/execution/interface.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" ) diff --git a/execution/nodeInterface/NodeInterface.go b/execution/nodeInterface/NodeInterface.go index 9179a52718..20282f8231 100644 --- a/execution/nodeInterface/NodeInterface.go +++ b/execution/nodeInterface/NodeInterface.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "math" "math/big" "sort" @@ -20,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/retryables" @@ -234,6 +236,7 @@ func (n NodeInterface) ConstructOutboxProof(c ctx, evm mech, size, leaf uint64) } balanced := size == arbmath.NextPowerOf2(size)/2 + // #nosec G115 treeLevels := int(arbmath.Log2ceil(size)) // the # of levels in the tree proofLevels := treeLevels - 1 // the # of levels where a hash is needed (all but root) walkLevels := treeLevels // the # of levels we need to consider when building walks @@ -249,6 +252,7 @@ func (n NodeInterface) ConstructOutboxProof(c ctx, evm mech, size, leaf uint64) place := leaf // where we are in the tree for level := 0; level < walkLevels; level++ { sibling := place ^ which + // #nosec G115 position := merkletree.NewLevelAndLeaf(uint64(level), sibling) if sibling < size { @@ -272,6 +276,7 @@ func (n NodeInterface) ConstructOutboxProof(c ctx, evm mech, size, leaf uint64) total += power // The leaf for a given partial is the sum of the powers leaf := total - 1 // of 2 preceding it. It's 1 less since we count from 0 + // #nosec G115 partial := merkletree.NewLevelAndLeaf(uint64(level), leaf) query = append(query, partial) @@ -297,6 +302,7 @@ func (n NodeInterface) ConstructOutboxProof(c ctx, evm mech, size, leaf uint64) mid := (lo + hi) / 2 + // #nosec G115 block, err := n.backend.BlockByNumber(n.context, rpc.BlockNumber(mid)) if err != nil { searchErr = err @@ -405,6 +411,7 @@ func (n NodeInterface) ConstructOutboxProof(c ctx, evm mech, size, leaf uint64) step.Leaf += 1 << step.Level // we start on the min partial's zero-hash sibling known[step] = hash0 + // #nosec G115 for step.Level < uint64(treeLevels) { curr, ok := known[step] @@ -516,10 +523,14 @@ func (n NodeInterface) GasEstimateL1Component( args.Gas = (*hexutil.Uint64)(&randomGas) // We set the run mode to eth_call mode here because we want an exact estimate, not a padded estimate - msg, err := args.ToMessage(randomGas, n.header, evm.StateDB.(*state.StateDB), core.MessageEthcallMode) - if err != nil { + if err := args.CallDefaults(randomGas, evm.Context.BaseFee, evm.ChainConfig().ChainID); err != nil { return 0, nil, nil, err } + sdb, ok := evm.StateDB.(*state.StateDB) + if !ok { + return 0, nil, nil, errors.New("failed to cast to stateDB") + } + msg := args.ToMessage(evm.Context.BaseFee, randomGas, n.header, sdb, core.MessageEthcallMode) pricing := c.State.L1PricingState() l1BaseFeeEstimate, err := pricing.PricePerUnit() @@ -572,10 +583,14 @@ func (n NodeInterface) GasEstimateComponents( // Setting the gas currently doesn't affect the PosterDataCost, // but we do it anyways for accuracy with potential future changes. args.Gas = &totalRaw - msg, err := args.ToMessage(gasCap, n.header, evm.StateDB.(*state.StateDB), core.MessageGasEstimationMode) - if err != nil { + if err := args.CallDefaults(gasCap, evm.Context.BaseFee, evm.ChainConfig().ChainID); err != nil { return 0, 0, nil, nil, err } + sdb, ok := evm.StateDB.(*state.StateDB) + if !ok { + return 0, 0, nil, nil, errors.New("failed to cast to stateDB") + } + msg := args.ToMessage(evm.Context.BaseFee, gasCap, n.header, sdb, core.MessageGasEstimationMode) brotliCompressionLevel, err := c.State.BrotliCompressionLevel() if err != nil { return 0, 0, nil, nil, fmt.Errorf("failed to get brotli compression level: %w", err) @@ -643,6 +658,10 @@ func (n NodeInterface) LegacyLookupMessageBatchProof(c ctx, evm mech, batchNum h // L2BlockRangeForL1 fetches the L1 block number of a given l2 block number. // c ctx and evm mech arguments are not used but supplied to match the precompile function type in NodeInterface contract func (n NodeInterface) BlockL1Num(c ctx, evm mech, l2BlockNum uint64) (uint64, error) { + if l2BlockNum > math.MaxInt64 { + return 0, fmt.Errorf("requested l2 block number %d out of range for int64", l2BlockNum) + } + // #nosec G115 blockHeader, err := n.backend.HeaderByNumber(n.context, rpc.BlockNumber(l2BlockNum)) if err != nil { return 0, err diff --git a/execution/nodeInterface/NodeInterfaceDebug.go b/execution/nodeInterface/NodeInterfaceDebug.go index ae9c157ce4..7066bf2ed2 100644 --- a/execution/nodeInterface/NodeInterfaceDebug.go +++ b/execution/nodeInterface/NodeInterfaceDebug.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" ) diff --git a/execution/nodeInterface/virtual-contracts.go b/execution/nodeInterface/virtual-contracts.go index d04be10857..5b9f4b3474 100644 --- a/execution/nodeInterface/virtual-contracts.go +++ b/execution/nodeInterface/virtual-contracts.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/l1pricing" @@ -23,7 +24,6 @@ import ( "github.com/offchainlabs/nitro/precompiles" "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" - "github.com/offchainlabs/nitro/util/arbmath" ) type addr = common.Address @@ -115,35 +115,28 @@ func init() { return msg, nil, nil } - core.InterceptRPCGasCap = func(gascap *uint64, msg *core.Message, header *types.Header, statedb *state.StateDB) { - if *gascap == 0 { - // It's already unlimited - return - } + core.RPCPostingGasHook = func(msg *core.Message, header *types.Header, statedb *state.StateDB) (uint64, error) { arbosVersion := arbosState.ArbOSVersion(statedb) if arbosVersion == 0 { // ArbOS hasn't been installed, so use the vanilla gas cap - return + return 0, nil } state, err := arbosState.OpenSystemArbosState(statedb, nil, true) if err != nil { - log.Error("failed to open ArbOS state", "err", err) - return + return 0, err } if header.BaseFee.Sign() == 0 { // if gas is free or there's no reimbursable poster, the user won't pay for L1 data costs - return + return 0, nil } brotliCompressionLevel, err := state.BrotliCompressionLevel() if err != nil { - log.Error("failed to get brotli compression level", "err", err) - return + return 0, err } posterCost, _ := state.L1PricingState().PosterDataCost(msg, l1pricing.BatchPosterAddress, brotliCompressionLevel) // Use estimate mode because this is used to raise the gas cap, so we don't want to underestimate. - posterCostInL2Gas := arbos.GetPosterGas(state, header.BaseFee, core.MessageGasEstimationMode, posterCost) - *gascap = arbmath.SaturatingUAdd(*gascap, posterCostInL2Gas) + return arbos.GetPosterGas(state, header.BaseFee, core.MessageGasEstimationMode, posterCost), nil } core.GetArbOSSpeedLimitPerSecond = func(statedb *state.StateDB) (uint64, error) { diff --git a/gethhook/geth-hook.go b/gethhook/geth-hook.go index 776e8cc452..3ad275b352 100644 --- a/gethhook/geth-hook.go +++ b/gethhook/geth-hook.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/precompiles" ) diff --git a/gethhook/geth_test.go b/gethhook/geth_test.go index 57ce2ddec0..381c128071 100644 --- a/gethhook/geth_test.go +++ b/gethhook/geth_test.go @@ -20,6 +20,7 @@ import ( "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -49,7 +50,7 @@ var testChainConfig = ¶ms.ChainConfig{ MuirGlacierBlock: big.NewInt(0), BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), - ArbitrumChainParams: params.ArbitrumDevTestParams(), + ArbitrumChainParams: chaininfo.ArbitrumDevTestParams(), } func TestEthDepositMessage(t *testing.T) { diff --git a/go-ethereum b/go-ethereum index 68cb86d1ec..2b9bafa37d 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 68cb86d1eca03353375c24befaa7814f145d425a +Subproject commit 2b9bafa37da7b4b7a47833573d5b6969c0b216e4 diff --git a/go.mod b/go.mod index 51bdaf06ae..417d2f7fa1 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,41 @@ module github.com/offchainlabs/nitro -go 1.21 +go 1.23 replace github.com/VictoriaMetrics/fastcache => ./fastcache replace github.com/ethereum/go-ethereum => ./go-ethereum +replace github.com/offchainlabs/bold => ./bold + require ( + cloud.google.com/go/storage v1.43.0 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible github.com/Shopify/toxiproxy v2.1.4+incompatible github.com/alicebob/miniredis/v2 v2.32.1 github.com/andybalholm/brotli v1.0.4 - github.com/aws/aws-sdk-go-v2 v1.21.2 - github.com/aws/aws-sdk-go-v2/config v1.18.45 - github.com/aws/aws-sdk-go-v2/credentials v1.13.43 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.10 - github.com/aws/aws-sdk-go-v2/service/s3 v1.26.9 + github.com/aws/aws-sdk-go-v2 v1.31.0 + github.com/aws/aws-sdk-go-v2/config v1.27.40 + github.com/aws/aws-sdk-go-v2/credentials v1.17.38 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.27 + github.com/aws/aws-sdk-go-v2/service/s3 v1.64.1 github.com/cavaliergopher/grab/v3 v3.0.1 - github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 + github.com/ccoveille/go-safecast v1.1.0 + github.com/cockroachdb/pebble v1.1.0 github.com/codeclysm/extract/v3 v3.0.2 github.com/dgraph-io/badger/v4 v4.2.0 github.com/enescakir/emoji v1.0.0 - github.com/ethereum/go-ethereum v1.10.26 + github.com/ethereum/go-ethereum v1.13.15 github.com/fatih/structtag v1.2.0 github.com/gdamore/tcell/v2 v2.7.1 - github.com/go-redis/redis/v8 v8.11.5 github.com/gobwas/httphead v0.1.0 github.com/gobwas/ws v1.2.1 github.com/gobwas/ws-examples v0.0.0-20190625122829-a9e8908d9484 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/btree v1.1.2 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/holiman/uint256 v1.2.4 github.com/jmoiron/sqlx v1.4.0 @@ -39,57 +43,88 @@ require ( github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f github.com/mattn/go-sqlite3 v1.14.22 github.com/mitchellh/mapstructure v1.4.1 + github.com/offchainlabs/bold v0.0.0-00010101000000-000000000000 github.com/pkg/errors v0.9.1 github.com/r3labs/diff/v3 v3.0.1 + github.com/redis/go-redis/v9 v9.6.1 github.com/rivo/tview v0.0.0-20240307173318-e804876934a1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/wealdtech/go-merkletree v1.0.0 - golang.org/x/crypto v0.21.0 - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/sync v0.5.0 - golang.org/x/sys v0.18.0 - golang.org/x/term v0.18.0 - golang.org/x/tools v0.16.0 + golang.org/x/crypto v0.24.0 + golang.org/x/sync v0.8.0 + golang.org/x/sys v0.21.0 + golang.org/x/term v0.21.0 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d + google.golang.org/api v0.187.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) -require github.com/google/go-querystring v1.1.0 // indirect +require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/auth v0.6.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.8 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/glog v1.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) require ( - github.com/DataDog/zstd v1.4.5 // indirect + github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect - github.com/aws/smithy-go v1.15.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.23.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.31.4 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cockroachdb/errors v1.9.1 // indirect - github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect - github.com/cockroachdb/redact v1.1.3 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // 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-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect - github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -98,27 +133,23 @@ require ( github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/ethereum/c-kzg-4844 v0.4.0 // indirect - github.com/fjl/memsize v0.0.2 // indirect + github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect github.com/gdamore/encoding v1.0.0 // indirect - github.com/getsentry/sentry-go v0.12.0 // indirect + github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/golang/glog v1.0.0 // indirect - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/flatbuffers v1.12.1 // indirect github.com/google/go-github/v62 v62.0.0 github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/graph-gophers/graphql-go v1.3.0 // indirect github.com/h2non/filetype v1.0.6 // indirect @@ -127,7 +158,6 @@ require ( github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 // indirect github.com/juju/loggo v0.0.0-20180524022052-584905176618 // indirect github.com/klauspost/compress v1.17.2 // indirect @@ -137,7 +167,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -147,14 +177,14 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/common v0.39.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/rhnvrm/simples3 v0.6.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect @@ -165,13 +195,10 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - go.opencensus.io v0.22.5 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.23.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.22.0 - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index 49f31efc0f..1e1dc34653 100644 --- a/go.sum +++ b/go.sum @@ -1,65 +1,33 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38= +cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= +cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= +cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= -github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= -github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= -github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= @@ -70,89 +38,80 @@ github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/arduino/go-paths-helper v1.2.0 h1:qDW93PR5IZUN/jzO4rCtexiwF8P4OIcOmcSgAYLZfY4= github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.16.3/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= -github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= -github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZMvr6MrSficMtjY2oS0WOORXTlxwU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= +github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= +github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 h1:xDAuZTn4IMm8o1LnBZvmrL8JA1io4o3YWNXgohbf20g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5/go.mod h1:wYSv6iDS621sEFLfKvpPE2ugjTuGlAG7iROg0hLOkfc= github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/config v1.15.5/go.mod h1:ZijHHh0xd/A+ZY53az0qzC5tT46kt4JVCePf2NX9Lk4= -github.com/aws/aws-sdk-go-v2/config v1.18.45 h1:Aka9bI7n8ysuwPeFdm77nfbyHCAKQ3z9ghB3S/38zes= -github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/config v1.27.40 h1:sie4mPBGFOO+Z27+yHzvyN31G20h/bf2xb5mCbpLv2Q= +github.com/aws/aws-sdk-go-v2/config v1.27.40/go.mod h1:4KW7Aa5tNo+0VHnuLnnE1vPHtwMurlNZNS65IdcewHA= github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/credentials v1.12.0/go.mod h1:9YWk7VW+eyKsoIL6/CljkTrNVWBSK9pkqOPUuijid4A= -github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= -github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.38 h1:iM90eRhCeZtlkzCNCG1JysOzJXGYf5rx80aD1lUgNDU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.38/go.mod h1:TCVYPZeQuLaYNEkf/TVn6k5k/zdVZZ7xH9po548VNNg= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.4/go.mod h1:u/s5/Z+ohUQOPXl00m2yJVyioWDECsbpXTQlaqSlufc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.10 h1:JL7cY85hyjlgfA29MMyAlItX+JYIH9XsxgMBS7jtlqA= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.10/go.mod h1:p+ul5bLZSDRRXCZ/vePvfmZBH9akozXBJA5oMshWa5U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.10/go.mod h1:F+EZtuIwjlv35kRJPyBGcsA4f7bnSoz15zOQ2lJq1Z4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.4/go.mod h1:8glyUqVIM4AmeenIsPo0oVh3+NUwnsQml2OFupfQW+0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.27 h1:1oLpQSTuqbizOUEYdxAwH+Eveg+FOCOkg84Yijba6Kc= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.27/go.mod h1:afo0vF9P3pjy1ny+cb45lzBjtKeEb5t5MPRxeTXpujw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.11/go.mod h1:0MR+sS1b/yxsfAPvAESrw8NfwUoxMinDyw6EYR9BS2U= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.1 h1:C21IDZCm9Yu5xqjb3fKmxDoYvJXtw1DNlOmLZEIlY1M= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.1/go.mod h1:l/BbcfqDCT3hePawhy4ZRtewjtdkl6GWtd9/U+1penQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 h1:OWYvKL53l1rbsUmW7bQyJVsYU/Ii3bbAAQIIFNbM0Tk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18/go.mod h1:CUx0G1v3wG6l01tUB+j7Y8kclA8NSqK4ef0YG79a4cg= github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 h1:T4pFel53bkHjL2mMo+4DKE6r6AuoZnM0fg7k1/ratr4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.5 h1:9LSZqt4v1JiehyZTrQnRFf2mY/awmyYNNY/b7zqtduU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.5/go.mod h1:S8TVP66AAkMMdYYCNZGvrdEq9YRm+qLXjio4FqRnrEE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 h1:rTWjG6AvWekO2B1LHeM3ktU7MqyX9rzWQ7hgzneZW7E= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20/go.mod h1:RGW2DDpVc8hu6Y6yG8G5CHVmVOAn1oV8rNKOHRJyswg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.4/go.mod h1:uKkN7qmSIsNJVyMtxNQoCEYMvFEXbOg9fwCJPdfp2u8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.4 h1:RE/DlZLYrz1OOmq8F28IXHLksuuvlpzUbvJ+SESCZBI= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.4/go.mod h1:oudbsSdDtazNj47z1ut1n37re9hDsKpk2ZI3v7KSxq0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.26.9 h1:LCQKnopq2t4oQS3VKivlYTzAHCTJZZoQICM9fny7KHY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.26.9/go.mod h1:iMYipLPXlWpBJ0KFX7QJHZ84rBydHBY8as2aQICTPWk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.64.1 h1:jjHf+M6vCp/WzbyFEroY4/Nx8dJac520A0EPwlYk0Do= +github.com/aws/aws-sdk-go-v2/service/s3 v1.64.1/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q= github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sso v1.11.4/go.mod h1:cPDwJwsP4Kff9mldCXAmddjJL6JGQqtA3Mzer2zyr88= -github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= -github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.4 h1:ck/Y8XWNR1gHa4BFkwE3oSu7XDJGwl+8TI7E/RB2EcQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.4/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.4 h1:4f2/JKYZHAZbQ7koBpZ012bKi32NHPY0m7TDuJgsbug= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.4/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/aws-sdk-go-v2/service/sts v1.16.4/go.mod h1:lfSYenAXtavyX2A1LsViglqlG9eEFYxNryTZS5rn3QE= -github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.4 h1:uK6dUUdJtqutK1XO/tmNaQMJiPLCJY/eAeOOmqQ6ygY= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.4/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= -github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= -github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 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/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4= github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4= +github.com/ccoveille/go-safecast v1.1.0 h1:iHKNWaZm+OznO7Eh6EljXPjGfGQsSfa6/sxPlIEKO+g= +github.com/ccoveille/go-safecast v1.1.0/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -161,37 +120,30 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= -github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= -github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74= -github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 h1:aPEJyR4rPBvDmeyi+l/FS/VtA00IWvjeFvjen1m1l1A= -github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593/go.mod h1:6hk1eMY/u5t+Cf18q5lFMUA1Rc+Sm5I6Ra1QuPyxXCo= -github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= -github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= +github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 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/codeclysm/extract/v3 v3.0.2 h1:sB4LcE3Php7LkhZwN0n2p8GCwZe92PEQutdbGURf5xc= github.com/codeclysm/extract/v3 v3.0.2/go.mod h1:NKsw+hqua9H+Rlwy/w/3Qgt9jDonYEgB6wJu+25eOKw= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 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= github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= -github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= -github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -202,7 +154,6 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5il github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= @@ -221,31 +172,26 @@ github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 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/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= -github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= -github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA= -github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= @@ -254,80 +200,48 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= -github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk= -github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gobwas/ws-examples v0.0.0-20190625122829-a9e8908d9484 h1:XC9N1eiAyO1zg62dpOU8bex8emB/zluUtKcbLNjJxGI= github.com/gobwas/ws-examples v0.0.0-20190625122829-a9e8908d9484/go.mod h1:5nDZF4afNA1S7ZKcBXCMvDo4nuCTp1931DND7/W4aXo= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -336,17 +250,12 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= @@ -355,46 +264,36 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0= github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/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= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= @@ -417,7 +316,6 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -437,35 +335,15 @@ github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXei github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= -github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= -github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= -github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= -github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/clock v0.0.0-20180524022203-d293bb356ca4/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok= @@ -478,27 +356,13 @@ github.com/juju/testing v0.0.0-20200510222523-6c8c298c77a0 h1:+WWUkhnTjV6RNOxkcw github.com/juju/testing v0.0.0-20200510222523-6c8c298c77a0/go.mod h1:hpGvhGHPVbNBraRLZEhoQwFLMrjK8PSlO4D3nDjKYXo= github.com/juju/utils v0.0.0-20180808125547-9dfc6dbfb02b/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= -github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= -github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= -github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= -github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/knadh/koanf v1.4.0 h1:/k0Bh49SqLyLNfte9r6cvuZWrApOQhglOmhIU3L/zDw= github.com/knadh/koanf v1.4.0/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -510,29 +374,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f h1:4+gHs0jJFJ06bfN8PshnM6cHcxGjRUVRLo5jndDiKRQ= github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f/go.mod h1:tHCZHV8b2A90ObojrEAzY0Lb03gxUxjDHr5IJyAh4ew= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -541,12 +394,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +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= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -566,81 +415,47 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= +github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rhnvrm/simples3 v0.6.1 h1:H0DJwybR6ryQE+Odi9eqkHuzjYAeJgtGcGtuBwOhsH8= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/tview v0.0.0-20240307173318-e804876934a1 h1:bWLHTRekAy497pE7+nXSuzXwwFHI0XauRzz6roUvY+s= @@ -649,48 +464,36 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= -github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -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= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -701,154 +504,75 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wealdtech/go-merkletree v1.0.0 h1:DsF1xMzj5rK3pSQM6mPv8jlyJyHXhFxpnA2bwEjMMBY= github.com/wealdtech/go-merkletree v1.0.0/go.mod h1:cdil512d/8ZC7Kx3bfrDvGMQXB25NTKbsm0rFrmDax4= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -857,78 +581,32 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -941,157 +619,68 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo= +google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls= +google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1100,57 +689,36 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4 h1:hILp2hNrRnYjZpmIbx70psAHbBSEcQ1NIzDcUbJ1b6g= gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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= 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= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/linters/koanf/handlers.go b/linters/koanf/handlers.go index 5ee3b80f9f..e3f7c67f68 100644 --- a/linters/koanf/handlers.go +++ b/linters/koanf/handlers.go @@ -126,7 +126,11 @@ func checkFlagDefs(pass *analysis.Pass, f *ast.FuncDecl, cnt map[string]int) Res if !ok { continue } - handleSelector(pass, callE.Args[1].(*ast.SelectorExpr), -1, cnt) + sel, ok := callE.Args[1].(*ast.SelectorExpr) + if !ok { + continue + } + handleSelector(pass, sel, -1, cnt) if normSL := normalizeTag(sl); !strings.EqualFold(normSL, s) { res.Errors = append(res.Errors, koanfError{ Pos: f.Pos(), diff --git a/linters/linters.go b/linters/linters.go index a6c9f6d55e..8d2807c0b2 100644 --- a/linters/linters.go +++ b/linters/linters.go @@ -1,11 +1,12 @@ package main import ( + "golang.org/x/tools/go/analysis/multichecker" + "github.com/offchainlabs/nitro/linters/koanf" "github.com/offchainlabs/nitro/linters/pointercheck" "github.com/offchainlabs/nitro/linters/rightshift" "github.com/offchainlabs/nitro/linters/structinit" - "golang.org/x/tools/go/analysis/multichecker" ) func main() { diff --git a/nitro-testnode b/nitro-testnode index 9dc0588c50..c177f28234 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit 9dc0588c5066e2efd25c09adf12df7b28ef18cb6 +Subproject commit c177f282340285bcdae2d6a784547e2bb8b97498 diff --git a/precompiles/ArbAddressTable.go b/precompiles/ArbAddressTable.go index 05f2275fd7..102fd55c3b 100644 --- a/precompiles/ArbAddressTable.go +++ b/precompiles/ArbAddressTable.go @@ -33,7 +33,7 @@ func (con ArbAddressTable) Decompress(c ctx, evm mech, buf []uint8, offset huge) return addr{}, nil, errors.New("invalid offset in ArbAddressTable.Decompress") } result, nbytes, err := c.State.AddressTable().Decompress(buf[ioffset:]) - return result, big.NewInt(int64(nbytes)), err + return result, new(big.Int).SetUint64(nbytes), err } // Lookup the index of an address in the table @@ -45,7 +45,7 @@ func (con ArbAddressTable) Lookup(c ctx, evm mech, addr addr) (huge, error) { if !exists { return nil, errors.New("address does not exist in AddressTable") } - return big.NewInt(int64(result)), nil + return new(big.Int).SetUint64(result), nil } // LookupIndex for an address in the table by index @@ -66,11 +66,11 @@ func (con ArbAddressTable) LookupIndex(c ctx, evm mech, index huge) (addr, error // Register adds an account to the table, shrinking its compressed representation func (con ArbAddressTable) Register(c ctx, evm mech, addr addr) (huge, error) { slot, err := c.State.AddressTable().Register(addr) - return big.NewInt(int64(slot)), err + return new(big.Int).SetUint64(slot), err } // Size gets the number of addresses in the table func (con ArbAddressTable) Size(c ctx, evm mech) (huge, error) { size, err := c.State.AddressTable().Size() - return big.NewInt(int64(size)), err + return new(big.Int).SetUint64(size), err } diff --git a/precompiles/ArbAddressTable_test.go b/precompiles/ArbAddressTable_test.go index b01a460636..3feaca6279 100644 --- a/precompiles/ArbAddressTable_test.go +++ b/precompiles/ArbAddressTable_test.go @@ -12,9 +12,10 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -47,6 +48,12 @@ func TestAddressTable1(t *testing.T) { addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + exists, err := atab.AddressExists(context, evm, addr) + Require(t, err) + if exists { + t.Fatal("Address shouldn't exist") + } + // register addr slot, err := atab.Register(context, evm, addr) Require(t, err) @@ -61,6 +68,12 @@ func TestAddressTable1(t *testing.T) { t.Fatal() } + exists, err = atab.AddressExists(context, evm, addr) + Require(t, err) + if !exists { + t.Fatal("Address should exist") + } + // verify Lookup of addr returns 0 index, err := atab.Lookup(context, evm, addr) Require(t, err) @@ -161,7 +174,7 @@ func newMockEVMForTestingWithVersionAndRunMode(version *uint64, runMode core.Mes } func newMockEVMForTestingWithVersion(version *uint64) *vm.EVM { - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() if version != nil { chainConfig.ArbitrumChainParams.InitialArbOSVersion = *version } diff --git a/precompiles/ArbAggregator.go b/precompiles/ArbAggregator.go index b74e280fe8..cee395189c 100644 --- a/precompiles/ArbAggregator.go +++ b/precompiles/ArbAggregator.go @@ -36,6 +36,7 @@ func (con ArbAggregator) GetBatchPosters(c ctx, evm mech) ([]addr, error) { return c.State.L1PricingState().BatchPosterTable().AllPosters(65536) } +// Adds additional batch poster address func (con ArbAggregator) AddBatchPoster(c ctx, evm mech, newBatchPoster addr) error { isOwner, err := c.State.ChainOwners().IsMember(c.caller) if err != nil { @@ -90,12 +91,14 @@ func (con ArbAggregator) SetFeeCollector(c ctx, evm mech, batchPoster addr, newF } // GetTxBaseFee gets an aggregator's current fixed fee to submit a tx +// Deprecated: always returns zero func (con ArbAggregator) GetTxBaseFee(c ctx, evm mech, aggregator addr) (huge, error) { // This is deprecated and now always returns zero. return big.NewInt(0), nil } // SetTxBaseFee sets an aggregator's fixed fee (caller must be the aggregator, its fee collector, or an owner) +// Deprecated: no-op func (con ArbAggregator) SetTxBaseFee(c ctx, evm mech, aggregator addr, feeInL1Gas huge) error { // This is deprecated and is now a no-op. return nil diff --git a/precompiles/ArbAggregator_test.go b/precompiles/ArbAggregator_test.go index ce1cebde5d..eb72f12f25 100644 --- a/precompiles/ArbAggregator_test.go +++ b/precompiles/ArbAggregator_test.go @@ -9,37 +9,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/l1pricing" ) -func TestArbAggregatorBatchPosters(t *testing.T) { - evm := newMockEVMForTesting() - context := testContext(common.Address{}, evm) - - addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) - - // initially should have one batch poster - bps, err := ArbAggregator{}.GetBatchPosters(context, evm) - Require(t, err) - if len(bps) != 1 { - Fail(t) - } - - // add addr as a batch poster - Require(t, ArbDebug{}.BecomeChainOwner(context, evm)) - Require(t, ArbAggregator{}.AddBatchPoster(context, evm, addr)) - - // there should now be two batch posters, and addr should be one of them - bps, err = ArbAggregator{}.GetBatchPosters(context, evm) - Require(t, err) - if len(bps) != 2 { - Fail(t) - } - if bps[0] != addr && bps[1] != addr { - Fail(t) - } -} - func TestFeeCollector(t *testing.T) { evm := newMockEVMForTesting() agg := ArbAggregator{} diff --git a/precompiles/ArbDebug.go b/precompiles/ArbDebug.go index bf85d5e18f..60e520da3e 100644 --- a/precompiles/ArbDebug.go +++ b/precompiles/ArbDebug.go @@ -24,6 +24,7 @@ type ArbDebug struct { UnusedError func() error } +// Emits events with values based on the args provided func (con ArbDebug) Events(c ctx, evm mech, paid huge, flag bool, value bytes32) (addr, huge, error) { // Emits 2 events that cover each case // Basic tests an index'd value & a normal value @@ -42,11 +43,13 @@ func (con ArbDebug) Events(c ctx, evm mech, paid huge, flag bool, value bytes32) return c.caller, paid, nil } +// Tries (and fails) to emit logs in a view context func (con ArbDebug) EventsView(c ctx, evm mech) error { _, _, err := con.Events(c, evm, common.Big0, true, bytes32{}) return err } +// Throws a custom error func (con ArbDebug) CustomRevert(c ctx, number uint64) error { return con.CustomError(number, "This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\", true) } @@ -61,6 +64,7 @@ func (con ArbDebug) Panic(c ctx, evm mech) error { panic("called ArbDebug's debug-only Panic method") } +// Throws a hardcoded error func (con ArbDebug) LegacyError(c ctx) error { return errors.New("example legacy error") } diff --git a/precompiles/ArbGasInfo.go b/precompiles/ArbGasInfo.go index b41dfda8a2..c85ed93f39 100644 --- a/precompiles/ArbGasInfo.go +++ b/precompiles/ArbGasInfo.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/util/arbmath" @@ -28,7 +29,7 @@ func (con ArbGasInfo) GetPricesInWeiWithAggregator( evm mech, aggregator addr, ) (huge, huge, huge, huge, huge, huge, error) { - if c.State.ArbOSVersion() < 4 { + if c.State.ArbOSVersion() < params.ArbosVersion_4 { return con._preVersion4_GetPricesInWeiWithAggregator(c, evm, aggregator) } @@ -104,7 +105,7 @@ func (con ArbGasInfo) GetPricesInWei(c ctx, evm mech) (huge, huge, huge, huge, h // GetPricesInArbGasWithAggregator gets prices in ArbGas when using the provided aggregator func (con ArbGasInfo) GetPricesInArbGasWithAggregator(c ctx, evm mech, aggregator addr) (huge, huge, huge, error) { - if c.State.ArbOSVersion() < 4 { + if c.State.ArbOSVersion() < params.ArbosVersion_4 { return con._preVersion4_GetPricesInArbGasWithAggregator(c, evm, aggregator) } l1GasPrice, err := c.State.L1PricingState().PricePerUnit() @@ -219,7 +220,7 @@ func (con ArbGasInfo) GetGasBacklogTolerance(c ctx, evm mech) (uint64, error) { // GetL1PricingSurplus gets the surplus of funds for L1 batch posting payments (may be negative) func (con ArbGasInfo) GetL1PricingSurplus(c ctx, evm mech) (*big.Int, error) { - if c.State.ArbOSVersion() < 10 { + if c.State.ArbOSVersion() < params.ArbosVersion_10 { return con._preversion10_GetL1PricingSurplus(c, evm) } ps := c.State.L1PricingState() diff --git a/precompiles/ArbGasInfo_test.go b/precompiles/ArbGasInfo_test.go new file mode 100644 index 0000000000..76489c3c9a --- /dev/null +++ b/precompiles/ArbGasInfo_test.go @@ -0,0 +1,141 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package precompiles + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/storage" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func setupArbGasInfo( + t *testing.T, +) ( + *vm.EVM, + *arbosState.ArbosState, + *Context, + *ArbGasInfo, +) { + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + tracer := util.NewTracingInfo(evm, testhelpers.RandomAddress(), types.ArbosAddress, util.TracingDuringEVM) + state, err := arbosState.OpenArbosState(evm.StateDB, burn.NewSystemBurner(tracer, false)) + Require(t, err) + + arbGasInfo := &ArbGasInfo{} + callCtx := testContext(caller, evm) + + return evm, state, callCtx, arbGasInfo +} + +func TestGetGasBacklog(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + backlog := uint64(1000) + err := state.L2PricingState().SetGasBacklog(backlog) + Require(t, err) + retrievedBacklog, err := arbGasInfo.GetGasBacklog(callCtx, evm) + Require(t, err) + if retrievedBacklog != backlog { + t.Fatal("expected backlog to be", backlog, "but got", retrievedBacklog) + } +} + +func TestGetL1PricingUpdateTime(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + lastUpdateTime := uint64(1001) + err := state.L1PricingState().SetLastUpdateTime(lastUpdateTime) + Require(t, err) + retrievedLastUpdateTime, err := arbGasInfo.GetLastL1PricingUpdateTime(callCtx, evm) + Require(t, err) + if retrievedLastUpdateTime != lastUpdateTime { + t.Fatal("expected last update time to be", lastUpdateTime, "but got", retrievedLastUpdateTime) + } +} + +func TestGetL1PricingFundsDueForRewards(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + fundsDueForRewards := big.NewInt(1002) + err := state.L1PricingState().SetFundsDueForRewards(fundsDueForRewards) + Require(t, err) + retrievedFundsDueForRewards, err := arbGasInfo.GetL1PricingFundsDueForRewards(callCtx, evm) + Require(t, err) + if retrievedFundsDueForRewards.Cmp(fundsDueForRewards) != 0 { + t.Fatal("expected funds due for rewards to be", fundsDueForRewards, "but got", retrievedFundsDueForRewards) + } +} + +func TestGetL1PricingUnitsSinceUpdate(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + pricingUnitsSinceUpdate := uint64(1003) + err := state.L1PricingState().SetUnitsSinceUpdate(pricingUnitsSinceUpdate) + Require(t, err) + retrievedPricingUnitsSinceUpdate, err := arbGasInfo.GetL1PricingUnitsSinceUpdate(callCtx, evm) + Require(t, err) + if retrievedPricingUnitsSinceUpdate != pricingUnitsSinceUpdate { + t.Fatal("expected pricing units since update to be", pricingUnitsSinceUpdate, "but got", retrievedPricingUnitsSinceUpdate) + } +} + +func TestGetLastL1PricingSurplus(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo := setupArbGasInfo(t) + + lastSurplus := big.NewInt(1004) + err := state.L1PricingState().SetLastSurplus(lastSurplus, params.ArbosVersion_Stylus) + Require(t, err) + retrievedLastSurplus, err := arbGasInfo.GetLastL1PricingSurplus(callCtx, evm) + Require(t, err) + if retrievedLastSurplus.Cmp(lastSurplus) != 0 { + t.Fatal("expected last surplus to be", lastSurplus, "but got", retrievedLastSurplus) + } +} + +func TestGetPricesInArbGas(t *testing.T) { + t.Parallel() + + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + arbGasInfo := &ArbGasInfo{} + callCtx := testContext(caller, evm) + + evm.Context.BaseFee = big.NewInt(1005) + expectedGasPerL2Tx := big.NewInt(111442786069) + expectedGasForL1Calldata := big.NewInt(796019900) + expectedStorageArbGas := big.NewInt(int64(storage.StorageWriteCost)) + gasPerL2Tx, gasForL1Calldata, storageArbGas, err := arbGasInfo.GetPricesInArbGas(callCtx, evm) + Require(t, err) + if gasPerL2Tx.Cmp(expectedGasPerL2Tx) != 0 { + t.Fatal("expected gas per L2 tx to be", expectedGasPerL2Tx, "but got", gasPerL2Tx) + } + if gasForL1Calldata.Cmp(expectedGasForL1Calldata) != 0 { + t.Fatal("expected gas for L1 calldata to be", expectedGasForL1Calldata, "but got", gasForL1Calldata) + } + if storageArbGas.Cmp(expectedStorageArbGas) != 0 { + t.Fatal("expected storage arb gas to be", expectedStorageArbGas, "but got", storageArbGas) + } +} diff --git a/precompiles/ArbInfo.go b/precompiles/ArbInfo.go index 9f8cf34532..60e23ffb6e 100644 --- a/precompiles/ArbInfo.go +++ b/precompiles/ArbInfo.go @@ -5,6 +5,7 @@ package precompiles import ( "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/util/arbmath" ) diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 066fc0a4c4..a6df0bd0dc 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -10,13 +10,13 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/util/arbmath" am "github.com/offchainlabs/nitro/util/arbmath" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/params" ) // ArbOwner precompile provides owners with tools for managing the rollup. @@ -69,6 +69,9 @@ func (con ArbOwner) SetL2BaseFee(c ctx, evm mech, priceInWei huge) error { // SetMinimumL2BaseFee sets the minimum base fee needed for a transaction to succeed func (con ArbOwner) SetMinimumL2BaseFee(c ctx, evm mech, priceInWei huge) error { + if c.txProcessor.MsgIsNonMutating() && priceInWei.Sign() == 0 { + return errors.New("minimum base fee must be nonzero") + } return c.State.L2PricingState().SetMinBaseFeeWei(priceInWei) } @@ -117,38 +120,48 @@ func (con ArbOwner) ScheduleArbOSUpgrade(c ctx, evm mech, newVersion uint64, tim return c.State.ScheduleArbOSUpgrade(newVersion, timestamp) } +// Sets equilibration units parameter for L1 price adjustment algorithm func (con ArbOwner) SetL1PricingEquilibrationUnits(c ctx, evm mech, equilibrationUnits huge) error { return c.State.L1PricingState().SetEquilibrationUnits(equilibrationUnits) } +// Sets inertia parameter for L1 price adjustment algorithm func (con ArbOwner) SetL1PricingInertia(c ctx, evm mech, inertia uint64) error { return c.State.L1PricingState().SetInertia(inertia) } +// Sets reward recipient address for L1 price adjustment algorithm func (con ArbOwner) SetL1PricingRewardRecipient(c ctx, evm mech, recipient addr) error { return c.State.L1PricingState().SetPayRewardsTo(recipient) } +// Sets reward amount for L1 price adjustment algorithm, in wei per unit func (con ArbOwner) SetL1PricingRewardRate(c ctx, evm mech, weiPerUnit uint64) error { return c.State.L1PricingState().SetPerUnitReward(weiPerUnit) } +// Set how much ArbOS charges per L1 gas spent on transaction data. func (con ArbOwner) SetL1PricePerUnit(c ctx, evm mech, pricePerUnit *big.Int) error { return c.State.L1PricingState().SetPricePerUnit(pricePerUnit) } +// Sets the base charge (in L1 gas) attributed to each data batch in the calldata pricer func (con ArbOwner) SetPerBatchGasCharge(c ctx, evm mech, cost int64) error { return c.State.L1PricingState().SetPerBatchGasCost(cost) } +// Sets the cost amortization cap in basis points func (con ArbOwner) SetAmortizedCostCapBips(c ctx, evm mech, cap uint64) error { return c.State.L1PricingState().SetAmortizedCostCapBips(cap) } +// Sets the Brotli compression level used for fast compression +// Available in ArbOS version 12 with default level as 1 func (con ArbOwner) SetBrotliCompressionLevel(c ctx, evm mech, level uint64) error { return c.State.SetBrotliCompressionLevel(level) } +// Releases surplus funds from L1PricerFundsPoolAddress for use func (con ArbOwner) ReleaseL1PricerSurplusFunds(c ctx, evm mech, maxWeiToRelease huge) (huge, error) { balance := evm.StateDB.GetBalance(l1pricing.L1PricerFundsPoolAddress) l1p := c.State.L1PricingState() @@ -292,6 +305,7 @@ func (con ArbOwner) RemoveWasmCacheManager(c ctx, _ mech, manager addr) error { return managers.Remove(manager, c.State.ArbOSVersion()) } +// Sets serialized chain config in ArbOS state func (con ArbOwner) SetChainConfig(c ctx, evm mech, serializedChainConfig []byte) error { if c == nil { return errors.New("nil context") diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index 451e18e1cc..792b4bb59d 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -5,6 +5,7 @@ package precompiles import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" ) // ArbOwnerPublic precompile provides non-owners with info about the current chain owners. @@ -42,7 +43,7 @@ func (con ArbOwnerPublic) GetNetworkFeeAccount(c ctx, evm mech) (addr, error) { // GetInfraFeeAccount gets the infrastructure fee collector func (con ArbOwnerPublic) GetInfraFeeAccount(c ctx, evm mech) (addr, error) { - if c.State.ArbOSVersion() < 6 { + if c.State.ArbOSVersion() < params.ArbosVersion_6 { return c.State.NetworkFeeAccount() } return c.State.InfraFeeAccount() diff --git a/precompiles/ArbOwner_test.go b/precompiles/ArbOwner_test.go index 1f8c7ae4cd..74b29a79b5 100644 --- a/precompiles/ArbOwner_test.go +++ b/precompiles/ArbOwner_test.go @@ -9,17 +9,20 @@ import ( "math/big" "testing" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -114,7 +117,7 @@ func TestArbOwner(t *testing.T) { Fail(t, avail) } deposited := big.NewInt(1000000) - evm.StateDB.AddBalance(l1pricing.L1PricerFundsPoolAddress, uint256.MustFromBig(deposited)) + evm.StateDB.AddBalance(l1pricing.L1PricerFundsPoolAddress, uint256.MustFromBig(deposited), tracing.BalanceChangeUnspecified) avail, err = gasInfo.GetL1FeesAvailable(callCtx, evm) Require(t, err) if avail.Sign() != 0 { @@ -151,6 +154,23 @@ func TestArbOwner(t *testing.T) { if avail.Cmp(deposited) != 0 { Fail(t, avail, deposited) } + + err = prec.SetNetworkFeeAccount(callCtx, evm, addr1) + Require(t, err) + retrievedNetworkFeeAccount, err := prec.GetNetworkFeeAccount(callCtx, evm) + Require(t, err) + if retrievedNetworkFeeAccount.Cmp(addr1) != 0 { + Fail(t, "Expected", addr1, "got", retrievedNetworkFeeAccount) + } + + l2BaseFee := big.NewInt(123) + err = prec.SetL2BaseFee(callCtx, evm, l2BaseFee) + Require(t, err) + retrievedL2BaseFee, err := state.L2PricingState().BaseFeeWei() + Require(t, err) + if l2BaseFee.Cmp(retrievedL2BaseFee) != 0 { + Fail(t, "Expected", l2BaseFee, "got", retrievedL2BaseFee) + } } func TestArbOwnerSetChainConfig(t *testing.T) { @@ -163,7 +183,7 @@ func TestArbOwnerSetChainConfig(t *testing.T) { prec := &ArbOwner{} callCtx := testContext(caller, evm) - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() chainConfig.ArbitrumChainParams.AllowDebugPrecompiles = false serializedChainConfig, err := json.Marshal(chainConfig) Require(t, err) @@ -199,7 +219,7 @@ func TestArbInfraFeeAccount(t *testing.T) { err = prec.SetInfraFeeAccount(callCtx, evm, newAddr) // this should be a no-op (because ArbOS version 0) Require(t, err) - version5 := uint64(5) + version5 := params.ArbosVersion_5 evm = newMockEVMForTestingWithVersion(&version5) callCtx = testContext(caller, evm) prec = &ArbOwner{} diff --git a/precompiles/ArbRetryableTx.go b/precompiles/ArbRetryableTx.go index d508d75752..06e5ccd352 100644 --- a/precompiles/ArbRetryableTx.go +++ b/precompiles/ArbRetryableTx.go @@ -9,8 +9,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" @@ -39,7 +39,7 @@ type ArbRetryableTx struct { var ErrSelfModifyingRetryable = errors.New("retryable cannot modify itself") func (con ArbRetryableTx) oldNotFoundError(c ctx) error { - if c.State.ArbOSVersion() >= 3 { + if c.State.ArbOSVersion() >= params.ArbosVersion_3 { return con.NoTicketWithIDError() } return errors.New("ticketId not found") @@ -149,12 +149,11 @@ func (con ArbRetryableTx) GetTimeout(c ctx, evm mech, ticketId bytes32) (huge, e if err != nil { return nil, err } - return big.NewInt(int64(timeout)), nil + return new(big.Int).SetUint64(timeout), nil } // Keepalive adds one lifetime period to the ticket's expiry func (con ArbRetryableTx) Keepalive(c ctx, evm mech, ticketId bytes32) (huge, error) { - // charge for the expiry update retryableState := c.State.RetryableState() nbytes, err := retryableState.RetryableSizeBytes(ticketId, evm.Context.Time) @@ -176,8 +175,9 @@ func (con ArbRetryableTx) Keepalive(c ctx, evm mech, ticketId bytes32) (huge, er return big.NewInt(0), err } - err = con.LifetimeExtended(c, evm, ticketId, big.NewInt(int64(newTimeout))) - return big.NewInt(int64(newTimeout)), err + bigNewTimeout := new(big.Int).SetUint64(newTimeout) + err = con.LifetimeExtended(c, evm, ticketId, bigNewTimeout) + return bigNewTimeout, err } // GetBeneficiary gets the beneficiary of the ticket @@ -222,6 +222,9 @@ func (con ArbRetryableTx) Cancel(c ctx, evm mech, ticketId bytes32) error { return con.Canceled(c, evm, ticketId) } +// Gets the redeemer of the current retryable redeem attempt. +// Returns the zero address if the current transaction is not a retryable redeem attempt. +// If this is an auto-redeem, returns the fee refund address of the retryable. func (con ArbRetryableTx) GetCurrentRedeemer(c ctx, evm mech) (common.Address, error) { if c.txProcessor.CurrentRefundTo != nil { return *c.txProcessor.CurrentRefundTo, nil @@ -229,6 +232,7 @@ func (con ArbRetryableTx) GetCurrentRedeemer(c ctx, evm mech) (common.Address, e return common.Address{}, nil } +// Do not call. This method represents a retryable submission to aid explorers. Calling it will always revert. func (con ArbRetryableTx) SubmitRetryable( c ctx, evm mech, requestId bytes32, l1BaseFee, deposit, callvalue, gasFeeCap huge, gasLimit uint64, maxSubmissionFee huge, diff --git a/precompiles/ArbRetryableTx_test.go b/precompiles/ArbRetryableTx_test.go index 9ccb437abc..d5b93640c9 100644 --- a/precompiles/ArbRetryableTx_test.go +++ b/precompiles/ArbRetryableTx_test.go @@ -7,12 +7,37 @@ import ( "math/big" "testing" - "github.com/offchainlabs/nitro/arbos/storage" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/storage" templates "github.com/offchainlabs/nitro/solgen/go/precompilesgen" ) +func newMockEVMForTestingWithCurrentRefundTo(currentRefundTo *common.Address) *vm.EVM { + evm := newMockEVMForTesting() + txProcessor := arbos.NewTxProcessor(evm, &core.Message{}) + txProcessor.CurrentRefundTo = currentRefundTo + evm.ProcessingHook = txProcessor + return evm +} + +func TestGetCurrentRedeemer(t *testing.T) { + currentRefundTo := common.HexToAddress("0x030405") + + evm := newMockEVMForTestingWithCurrentRefundTo(¤tRefundTo) + retryableTx := ArbRetryableTx{} + context := testContext(common.Address{}, evm) + + currentRedeemer, err := retryableTx.GetCurrentRedeemer(context, evm) + Require(t, err) + if currentRefundTo.Cmp(currentRedeemer) != 0 { + t.Fatal("Expected to be ", currentRefundTo, " but got ", currentRedeemer) + } +} + func TestRetryableRedeem(t *testing.T) { evm := newMockEVMForTesting() precompileCtx := testContext(common.Address{}, evm) diff --git a/precompiles/ArbSys.go b/precompiles/ArbSys.go index 13f56d3b8e..9742ed51f4 100644 --- a/precompiles/ArbSys.go +++ b/precompiles/ArbSys.go @@ -9,6 +9,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/merkletree" @@ -36,7 +38,7 @@ func (con *ArbSys) ArbBlockNumber(c ctx, evm mech) (huge, error) { // ArbBlockHash gets the L2 block hash, if sufficiently recent func (con *ArbSys) ArbBlockHash(c ctx, evm mech, arbBlockNumber *big.Int) (bytes32, error) { if !arbBlockNumber.IsUint64() { - if c.State.ArbOSVersion() >= 11 { + if c.State.ArbOSVersion() >= params.ArbosVersion_11 { return bytes32{}, con.InvalidBlockNumberError(arbBlockNumber, evm.Context.BlockNumber) } return bytes32{}, errors.New("invalid block number") @@ -45,7 +47,7 @@ func (con *ArbSys) ArbBlockHash(c ctx, evm mech, arbBlockNumber *big.Int) (bytes currentNumber := evm.Context.BlockNumber.Uint64() if requestedBlockNum >= currentNumber || requestedBlockNum+256 < currentNumber { - if c.State.ArbOSVersion() >= 11 { + if c.State.ArbOSVersion() >= params.ArbosVersion_11 { return common.Hash{}, con.InvalidBlockNumberError(arbBlockNumber, evm.Context.BlockNumber) } return common.Hash{}, errors.New("invalid block number for ArbBlockHAsh") @@ -83,7 +85,7 @@ func (con *ArbSys) MapL1SenderContractAddressToL2Alias(c ctx, sender addr, dest // WasMyCallersAddressAliased checks if the caller's caller was aliased func (con *ArbSys) WasMyCallersAddressAliased(c ctx, evm mech) (bool, error) { topLevel := con.isTopLevel(c, evm) - if c.State.ArbOSVersion() < 6 { + if c.State.ArbOSVersion() < params.ArbosVersion_6 { topLevel = evm.Depth() == 2 } aliased := topLevel && util.DoesTxTypeAlias(c.txProcessor.TopTxType) @@ -92,7 +94,6 @@ func (con *ArbSys) WasMyCallersAddressAliased(c ctx, evm mech) (bool, error) { // MyCallersAddressWithoutAliasing gets the caller's caller without any potential aliasing func (con *ArbSys) MyCallersAddressWithoutAliasing(c ctx, evm mech) (addr, error) { - address := addr{} if evm.Depth() > 1 { @@ -162,7 +163,7 @@ func (con *ArbSys) SendTxToL1(c ctx, evm mech, value huge, destination addr, cal } } - leafNum := big.NewInt(int64(size - 1)) + leafNum := new(big.Int).SetUint64(size - 1) var blockTime big.Int blockTime.SetUint64(evm.Context.Time) @@ -180,7 +181,7 @@ func (con *ArbSys) SendTxToL1(c ctx, evm mech, value huge, destination addr, cal calldataForL1, ) - if c.State.ArbOSVersion() >= 4 { + if c.State.ArbOSVersion() >= params.ArbosVersion_4 { return leafNum, nil } return sendHash.Big(), err @@ -199,7 +200,7 @@ func (con ArbSys) SendMerkleTreeState(c ctx, evm mech) (huge, bytes32, []bytes32 for i, par := range rawPartials { partials[i] = par } - return big.NewInt(int64(size)), rootHash, partials, nil + return new(big.Int).SetUint64(size), rootHash, partials, nil } // WithdrawEth send paid eth to the destination on L1 diff --git a/precompiles/ArbWasm.go b/precompiles/ArbWasm.go index 9f42cacb5a..eecca35ce6 100644 --- a/precompiles/ArbWasm.go +++ b/precompiles/ArbWasm.go @@ -5,6 +5,9 @@ package precompiles import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + gethparams "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" @@ -32,12 +35,13 @@ func (con ArbWasm) ActivateProgram(c ctx, evm mech, value huge, program addr) (u debug := evm.ChainConfig().DebugMode() runMode := c.txProcessor.RunMode() programs := c.State.Programs() + arbosVersion := c.State.ArbOSVersion() // charge a fixed cost up front to begin activation if err := c.Burn(1659168); err != nil { return 0, nil, err } - version, codeHash, moduleHash, dataFee, takeAllGas, err := programs.ActivateProgram(evm, program, runMode, debug) + version, codeHash, moduleHash, dataFee, takeAllGas, err := programs.ActivateProgram(evm, program, arbosVersion, runMode, debug) if takeAllGas { _ = c.BurnOut() } @@ -133,6 +137,9 @@ func (con ArbWasm) MinInitGas(c ctx, _ mech) (uint64, uint64, error) { params, err := c.State.Programs().Params() init := uint64(params.MinInitGas) * programs.MinInitGasUnits cached := uint64(params.MinCachedInitGas) * programs.MinCachedGasUnits + if c.State.ArbOSVersion() < gethparams.ArbosVersion_StylusChargingFixes { + return 0, 0, vm.ErrExecutionReverted + } return init, cached, err } diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 9a6d8885ad..54d18a0cc9 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -14,13 +14,6 @@ import ( "strings" "unicode" - "github.com/offchainlabs/nitro/arbos" - "github.com/offchainlabs/nitro/arbos/arbosState" - "github.com/offchainlabs/nitro/arbos/programs" - "github.com/offchainlabs/nitro/arbos/util" - pgen "github.com/offchainlabs/nitro/solgen/go/precompilesgen" - "github.com/offchainlabs/nitro/util/arbmath" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -30,6 +23,13 @@ import ( "github.com/ethereum/go-ethereum/log" glog "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/programs" + "github.com/offchainlabs/nitro/arbos/util" + pgen "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/arbmath" ) type ArbosPrecompile interface { @@ -329,6 +329,7 @@ func MakePrecompile(metadata *bind.MetaData, implementer interface{}) (addr, *Pr gascost := func(args []reflect.Value) []reflect.Value { cost := params.LogGas + // #nosec G115 cost += params.LogTopicGas * uint64(1+len(topicInputs)) var dataValues []interface{} @@ -360,7 +361,7 @@ func MakePrecompile(metadata *bind.MetaData, implementer interface{}) (addr, *Pr args = args[2:] version := arbosState.ArbOSVersion(state) - if callerCtx.readOnly && version >= 11 { + if callerCtx.readOnly && version >= params.ArbosVersion_11 { return []reflect.Value{reflect.ValueOf(vm.ErrWriteProtection)} } @@ -530,14 +531,14 @@ func Precompiles() map[addr]ArbosPrecompile { insert(MakePrecompile(pgen.ArbFunctionTableMetaData, &ArbFunctionTable{Address: types.ArbFunctionTableAddress})) insert(MakePrecompile(pgen.ArbosTestMetaData, &ArbosTest{Address: types.ArbosTestAddress})) ArbGasInfo := insert(MakePrecompile(pgen.ArbGasInfoMetaData, &ArbGasInfo{Address: types.ArbGasInfoAddress})) - ArbGasInfo.methodsByName["GetL1FeesAvailable"].arbosVersion = 10 - ArbGasInfo.methodsByName["GetL1RewardRate"].arbosVersion = 11 - ArbGasInfo.methodsByName["GetL1RewardRecipient"].arbosVersion = 11 - ArbGasInfo.methodsByName["GetL1PricingEquilibrationUnits"].arbosVersion = 20 - ArbGasInfo.methodsByName["GetLastL1PricingUpdateTime"].arbosVersion = 20 - ArbGasInfo.methodsByName["GetL1PricingFundsDueForRewards"].arbosVersion = 20 - ArbGasInfo.methodsByName["GetL1PricingUnitsSinceUpdate"].arbosVersion = 20 - ArbGasInfo.methodsByName["GetLastL1PricingSurplus"].arbosVersion = 20 + ArbGasInfo.methodsByName["GetL1FeesAvailable"].arbosVersion = params.ArbosVersion_10 + ArbGasInfo.methodsByName["GetL1RewardRate"].arbosVersion = params.ArbosVersion_11 + ArbGasInfo.methodsByName["GetL1RewardRecipient"].arbosVersion = params.ArbosVersion_11 + ArbGasInfo.methodsByName["GetL1PricingEquilibrationUnits"].arbosVersion = params.ArbosVersion_20 + ArbGasInfo.methodsByName["GetLastL1PricingUpdateTime"].arbosVersion = params.ArbosVersion_20 + ArbGasInfo.methodsByName["GetL1PricingFundsDueForRewards"].arbosVersion = params.ArbosVersion_20 + ArbGasInfo.methodsByName["GetL1PricingUnitsSinceUpdate"].arbosVersion = params.ArbosVersion_20 + ArbGasInfo.methodsByName["GetLastL1PricingSurplus"].arbosVersion = params.ArbosVersion_20 insert(MakePrecompile(pgen.ArbAggregatorMetaData, &ArbAggregator{Address: types.ArbAggregatorAddress})) insert(MakePrecompile(pgen.ArbStatisticsMetaData, &ArbStatistics{Address: types.ArbStatisticsAddress})) @@ -553,10 +554,10 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwnerPublicImpl := &ArbOwnerPublic{Address: types.ArbOwnerPublicAddress} ArbOwnerPublic := insert(MakePrecompile(pgen.ArbOwnerPublicMetaData, ArbOwnerPublicImpl)) - ArbOwnerPublic.methodsByName["GetInfraFeeAccount"].arbosVersion = 5 - ArbOwnerPublic.methodsByName["RectifyChainOwner"].arbosVersion = 11 - ArbOwnerPublic.methodsByName["GetBrotliCompressionLevel"].arbosVersion = 20 - ArbOwnerPublic.methodsByName["GetScheduledUpgrade"].arbosVersion = 20 + ArbOwnerPublic.methodsByName["GetInfraFeeAccount"].arbosVersion = params.ArbosVersion_5 + ArbOwnerPublic.methodsByName["RectifyChainOwner"].arbosVersion = params.ArbosVersion_11 + ArbOwnerPublic.methodsByName["GetBrotliCompressionLevel"].arbosVersion = params.ArbosVersion_20 + ArbOwnerPublic.methodsByName["GetScheduledUpgrade"].arbosVersion = params.ArbosVersion_20 ArbWasmImpl := &ArbWasm{Address: types.ArbWasmAddress} ArbWasm := insert(MakePrecompile(pgen.ArbWasmMetaData, ArbWasmImpl)) @@ -610,11 +611,11 @@ func Precompiles() map[addr]ArbosPrecompile { return ArbOwnerImpl.OwnerActs(context, evm, method, owner, data) } _, ArbOwner := MakePrecompile(pgen.ArbOwnerMetaData, ArbOwnerImpl) - ArbOwner.methodsByName["GetInfraFeeAccount"].arbosVersion = 5 - ArbOwner.methodsByName["SetInfraFeeAccount"].arbosVersion = 5 - ArbOwner.methodsByName["ReleaseL1PricerSurplusFunds"].arbosVersion = 10 - ArbOwner.methodsByName["SetChainConfig"].arbosVersion = 11 - ArbOwner.methodsByName["SetBrotliCompressionLevel"].arbosVersion = 20 + ArbOwner.methodsByName["GetInfraFeeAccount"].arbosVersion = params.ArbosVersion_5 + ArbOwner.methodsByName["SetInfraFeeAccount"].arbosVersion = params.ArbosVersion_5 + ArbOwner.methodsByName["ReleaseL1PricerSurplusFunds"].arbosVersion = params.ArbosVersion_10 + ArbOwner.methodsByName["SetChainConfig"].arbosVersion = params.ArbosVersion_11 + ArbOwner.methodsByName["SetBrotliCompressionLevel"].arbosVersion = params.ArbosVersion_20 stylusMethods := []string{ "SetInkPrice", "SetWasmMaxStackDepth", "SetWasmFreePages", "SetWasmPageGas", "SetWasmPageLimit", "SetWasmMinInitGas", "SetWasmInitCostScalar", @@ -712,6 +713,8 @@ func (p *Precompile) Call( tracingInfo: util.NewTracingInfo(evm, caller, precompileAddress, util.TracingDuringEVM), } + // len(input) must be at least 4 because of the check near the start of this function + // #nosec G115 argsCost := params.CopyGas * arbmath.WordsForBytes(uint64(len(input)-4)) if err := callerCtx.Burn(argsCost); err != nil { // user cannot afford the argument data supplied @@ -795,7 +798,7 @@ func (p *Precompile) Call( ) } // nolint:errorlint - if arbosVersion >= 11 || errRet == vm.ErrExecutionReverted { + if arbosVersion >= params.ArbosVersion_11 || errRet == vm.ErrExecutionReverted { return nil, callerCtx.gasLeft, vm.ErrExecutionReverted } // Preserve behavior with old versions which would zero out gas on this type of error diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index ecce77088a..75fed711eb 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -10,12 +10,12 @@ import ( "os" "testing" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/storage" templates "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" @@ -91,6 +91,7 @@ func TestEvents(t *testing.T) { if log.Address != debugContractAddr { Fail(t, "address mismatch:", log.Address, "vs", debugContractAddr) } + // #nosec G115 if log.BlockNumber != uint64(blockNumber) { Fail(t, "block number mismatch:", log.BlockNumber, "vs", blockNumber) } @@ -170,6 +171,7 @@ func TestEventCosts(t *testing.T) { offsetBytes := 32 storeBytes := sizeBytes + offsetBytes + len(bytes) storeBytes = storeBytes + 31 - (storeBytes+31)%32 // round up to a multiple of 32 + // #nosec G115 storeCost := uint64(storeBytes) * params.LogDataGas expected[i] = baseCost + addrCost + hashCost + storeCost @@ -188,13 +190,13 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { log.SetDefault(log.NewLogger(glogger)) expectedNewMethodsPerArbosVersion := map[uint64]int{ - 0: 89, - 5: 3, - 10: 2, - 11: 4, - 20: 8, - 30: 38, - 31: 1, + 0: 89, + params.ArbosVersion_5: 3, + params.ArbosVersion_10: 2, + params.ArbosVersion_11: 4, + params.ArbosVersion_20: 8, + params.ArbosVersion_30: 38, + params.ArbosVersion_31: 1, } precompiles := Precompiles() diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index b9363c40a2..028aed755b 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -7,12 +7,13 @@ import ( "errors" "math/big" - "github.com/offchainlabs/nitro/arbos/arbosState" - "github.com/offchainlabs/nitro/arbos/util" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/util" ) // DebugPrecompile is a precompile wrapper for those not allowed in production @@ -102,7 +103,7 @@ func (wrapper *OwnerPrecompile) Call( } version := arbosState.ArbOSVersion(evm.StateDB) - if !readOnly || version < 11 { + if !readOnly || version < params.ArbosVersion_11 { // log that the owner operation succeeded if err := wrapper.emitSuccess(evm, *(*[4]byte)(input[:4]), caller, input); err != nil { log.Error("failed to emit OwnerActs event", "err", err) diff --git a/pubsub/common.go b/pubsub/common.go index d7f041af15..a4fc141bb5 100644 --- a/pubsub/common.go +++ b/pubsub/common.go @@ -2,12 +2,16 @@ package pubsub import ( "context" + "fmt" "strings" + "github.com/redis/go-redis/v9" + "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" ) +func ResultKeyFor(streamName, id string) string { return fmt.Sprintf("%s.%s", streamName, id) } + // CreateStream tries to create stream with given name, if it already exists // does not return an error. func CreateStream(ctx context.Context, streamName string, client redis.UniversalClient) error { diff --git a/pubsub/consumer.go b/pubsub/consumer.go index df3695606d..3f28749473 100644 --- a/pubsub/consumer.go +++ b/pubsub/consumer.go @@ -5,36 +5,40 @@ import ( "encoding/json" "errors" "fmt" + "math" + "math/rand" + "strconv" "time" - "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/google/uuid" - "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/redis/go-redis/v9" "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/util/stopwaiter" ) type ConsumerConfig struct { // Timeout of result entry in Redis. ResponseEntryTimeout time.Duration `koanf:"response-entry-timeout"` - // Duration after which consumer is considered to be dead if heartbeat - // is not updated. - KeepAliveTimeout time.Duration `koanf:"keepalive-timeout"` + // Minimum idle time after which messages will be autoclaimed + IdletimeToAutoclaim time.Duration `koanf:"idletime-to-autoclaim"` } var DefaultConsumerConfig = ConsumerConfig{ ResponseEntryTimeout: time.Hour, - KeepAliveTimeout: 5 * time.Minute, + IdletimeToAutoclaim: 5 * time.Minute, } var TestConsumerConfig = ConsumerConfig{ ResponseEntryTimeout: time.Minute, - KeepAliveTimeout: 30 * time.Millisecond, + IdletimeToAutoclaim: 30 * time.Millisecond, } func ConsumerConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Duration(prefix+".response-entry-timeout", DefaultConsumerConfig.ResponseEntryTimeout, "timeout for response entry") - f.Duration(prefix+".keepalive-timeout", DefaultConsumerConfig.KeepAliveTimeout, "timeout after which consumer is considered inactive if heartbeat wasn't performed") + f.Duration(prefix+".idletime-to-autoclaim", DefaultConsumerConfig.IdletimeToAutoclaim, "After a message spends this amount of time in PEL (Pending Entries List i.e claimed by another consumer but not Acknowledged) it will be allowed to be autoclaimed by other consumers") } // Consumer implements a consumer for redis stream provides heartbeat to @@ -51,6 +55,7 @@ type Consumer[Request any, Response any] struct { type Message[Request any] struct { ID string Value Request + Ack func() } func NewConsumer[Request any, Response any](client redis.UniversalClient, streamName string, cfg *ConsumerConfig) (*Consumer[Request, Response], error) { @@ -69,21 +74,14 @@ func NewConsumer[Request any, Response any](client redis.UniversalClient, stream // Start starts the consumer to iteratively perform heartbeat in configured intervals. func (c *Consumer[Request, Response]) Start(ctx context.Context) { c.StopWaiter.Start(ctx, c) - c.StopWaiter.CallIteratively( - func(ctx context.Context) time.Duration { - c.heartBeat(ctx) - return c.cfg.KeepAliveTimeout / 10 - }, - ) } -func (c *Consumer[Request, Response]) StopAndWait() { - c.StopWaiter.StopAndWait() - c.deleteHeartBeat(c.GetParentContext()) +func (c *Consumer[Request, Response]) Id() string { + return c.id } -func heartBeatKey(id string) string { - return fmt.Sprintf("consumer:%s:heartbeat", id) +func (c *Consumer[Request, Response]) StopAndWait() { + c.StopWaiter.StopAndWait() } func (c *Consumer[Request, Response]) RedisClient() redis.UniversalClient { @@ -94,68 +92,124 @@ func (c *Consumer[Request, Response]) StreamName() string { return c.redisStream } -func (c *Consumer[Request, Response]) heartBeatKey() string { - return heartBeatKey(c.id) -} - -// deleteHeartBeat deletes the heartbeat to indicate it is being shut down. -func (c *Consumer[Request, Response]) deleteHeartBeat(ctx context.Context) { - if err := c.client.Del(ctx, c.heartBeatKey()).Err(); err != nil { - l := log.Info - if ctx.Err() != nil { - l = log.Error - } - l("Deleting heardbeat", "consumer", c.id, "error", err) +func decrementMsgIdByOne(msgId string) string { + id, err := getUintParts(msgId) + if err != nil { + log.Error("Error decrementing start of XAutoClaim by one, defaulting to 0", "err", err) + return "0" } -} - -// heartBeat updates the heartBeat key indicating aliveness. -func (c *Consumer[Request, Response]) heartBeat(ctx context.Context) { - if err := c.client.Set(ctx, c.heartBeatKey(), time.Now().UnixMilli(), 2*c.cfg.KeepAliveTimeout).Err(); err != nil { - l := log.Info - if ctx.Err() != nil { - l = log.Error - } - l("Updating heardbeat", "consumer", c.id, "error", err) + if id[1] > 0 { + return strconv.FormatUint(id[0], 10) + "-" + strconv.FormatUint(id[1]-1, 10) + } else if id[0] > 0 { + return strconv.FormatUint(id[0]-1, 10) + "-" + strconv.FormatUint(math.MaxUint64, 10) } + return "0" } // Consumer first checks it there exists pending message that is claimed by // unresponsive consumer, if not then reads from the stream. func (c *Consumer[Request, Response]) Consume(ctx context.Context) (*Message[Request], error) { - res, err := c.client.XReadGroup(ctx, &redis.XReadGroupArgs{ - Group: c.redisGroup, - Consumer: c.id, - // Receive only messages that were never delivered to any other consumer, - // that is, only new messages. - Streams: []string{c.redisStream, ">"}, - Count: 1, - Block: time.Millisecond, // 0 seems to block the read instead of immediately returning - }).Result() - if errors.Is(err, redis.Nil) { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("reading message for consumer: %q: %w", c.id, err) + // First try to XAUTOCLAIM, with start as a random messageID from PEL with MinIdle as IdletimeToAutoclaim + // this prioritizes processing PEL messages that have been waiting for more than IdletimeToAutoclaim duration + var messages []redis.XMessage + if pendingMsgs, err := c.client.XPendingExt(ctx, &redis.XPendingExtArgs{ + Stream: c.redisStream, + Group: c.redisGroup, + Start: "-", + End: "+", + Count: 50, + Idle: c.cfg.IdletimeToAutoclaim, + }).Result(); err != nil { + if !errors.Is(err, redis.Nil) { + log.Error("Error from XpendingExt in getting PEL for auto claim", "err", err, "penindlen", len(pendingMsgs)) + } + } else if len(pendingMsgs) > 0 { + idx := rand.Intn(len(pendingMsgs)) + messages, _, err = c.client.XAutoClaim(ctx, &redis.XAutoClaimArgs{ + Group: c.redisGroup, + Consumer: c.id, + MinIdle: c.cfg.IdletimeToAutoclaim, // Minimum idle time for messages to claim (in milliseconds) + Stream: c.redisStream, + Start: decrementMsgIdByOne(pendingMsgs[idx].ID), + Count: 1, + }).Result() + if err != nil { + log.Info("error from xautoclaim", "err", err) + } } - if len(res) != 1 || len(res[0].Messages) != 1 { - return nil, fmt.Errorf("redis returned entries: %+v, for querying single message", res) + if len(messages) == 0 { + // If we fail to autoclaim then we do not retry but instead fallback to reading new messages + res, err := c.client.XReadGroup(ctx, &redis.XReadGroupArgs{ + Group: c.redisGroup, + Consumer: c.id, + // Receive only messages that were never delivered to any other consumer, + // that is, only new messages. + Streams: []string{c.redisStream, ">"}, + Count: 1, + Block: time.Millisecond, // 0 seems to block the read instead of immediately returning + }).Result() + if errors.Is(err, redis.Nil) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("reading message for consumer: %q: %w", c.id, err) + } + if len(res) != 1 || len(res[0].Messages) != 1 { + return nil, fmt.Errorf("redis returned entries: %+v, for querying single message", res) + } + messages = res[0].Messages } + var ( - value = res[0].Messages[0].Values[messageKey] + value = messages[0].Values[messageKey] data, ok = (value).(string) ) if !ok { - return nil, fmt.Errorf("casting request to string: %w", err) + return nil, errors.New("error casting request to string") } var req Request if err := json.Unmarshal([]byte(data), &req); err != nil { return nil, fmt.Errorf("unmarshaling value: %v, error: %w", value, err) } - log.Debug("Redis stream consuming", "consumer_id", c.id, "message_id", res[0].Messages[0].ID) + ackNotifier := make(chan struct{}) + c.StopWaiter.LaunchThread(func(ctx context.Context) { + for { + // Use XClaimJustID so that we would have clear difference between invalid requests that are claimed multiple times due to xautoclaim and + // valid requests that are just being claimed in regular intervals to indicate heartbeat + if ids, err := c.client.XClaimJustID(ctx, &redis.XClaimArgs{ + Stream: c.redisStream, + Group: c.redisGroup, + Consumer: c.id, + MinIdle: 0, + Messages: []string{messages[0].ID}, + }).Result(); err != nil { + log.Error("Error claiming message, it might be possible that other consumers might pick this request", "msgID", messages[0].ID) + } else if len(ids) == 0 { + log.Warn("XClaimJustID returned empty response when indicating hearbeat", "msgID", messages[0].ID) + } else if len(ids) > 1 { + log.Error("XClaimJustID returned response with more than entry", "msgIDs", ids) + } + select { + case <-ackNotifier: + return + case <-ctx.Done(): + log.Info("Context done while claiming message to indicate hearbeat", "messageID", messages[0].ID, "error", ctx.Err().Error()) + if c.StopWaiter.GetParentContext().Err() == nil { + // Proceeding to set the Idle time of message to IdletimeToAutoclaim to allow it to be picked by other consumers + if err := c.client.Do(c.StopWaiter.GetParentContext(), "XCLAIM", c.redisStream, c.redisGroup, c.id, 0, messages[0].ID, "IDLE", c.cfg.IdletimeToAutoclaim.Milliseconds()).Err(); err != nil { + log.Error("error when trying to set the idle time of currently worked on message to IdletimeToAutoclaim", "messageID", messages[0].ID, "err", err) + } + } + return + case <-time.After(c.cfg.IdletimeToAutoclaim / 10): + } + } + }) + log.Debug("Redis stream consuming", "consumer_id", c.id, "message_id", messages[0].ID) return &Message[Request]{ - ID: res[0].Messages[0].ID, + ID: messages[0].ID, Value: req, + Ack: func() { close(ackNotifier) }, }, nil } @@ -164,12 +218,18 @@ func (c *Consumer[Request, Response]) SetResult(ctx context.Context, messageID s if err != nil { return fmt.Errorf("marshaling result: %w", err) } - acquired, err := c.client.SetNX(ctx, messageID, resp, c.cfg.ResponseEntryTimeout).Result() + resultKey := ResultKeyFor(c.StreamName(), messageID) + log.Debug("consumer: setting result", "cid", c.id, "msgIdInStream", messageID, "resultKeyInRedis", resultKey) + acquired, err := c.client.SetNX(ctx, resultKey, resp, c.cfg.ResponseEntryTimeout).Result() if err != nil || !acquired { - return fmt.Errorf("setting result for message: %v, error: %w", messageID, err) + return fmt.Errorf("setting result for message with message-id in stream: %v, error: %w", messageID, err) } + log.Debug("consumer: xack", "cid", c.id, "messageId", messageID) if _, err := c.client.XAck(ctx, c.redisStream, c.redisGroup, messageID).Result(); err != nil { return fmt.Errorf("acking message: %v, error: %w", messageID, err) } + if _, err := c.client.XDel(ctx, c.redisStream, messageID).Result(); err != nil { + return fmt.Errorf("deleting message: %v, error: %w", messageID, err) + } return nil } diff --git a/pubsub/producer.go b/pubsub/producer.go index 2b1cdb5e3f..5aaca77aa7 100644 --- a/pubsub/producer.go +++ b/pubsub/producer.go @@ -13,18 +13,19 @@ import ( "encoding/json" "errors" "fmt" - "math" "strconv" "strings" "sync" "time" - "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/spf13/pflag" ) const ( @@ -43,50 +44,31 @@ type Producer[Request any, Response any] struct { promisesLock sync.RWMutex promises map[string]*containers.Promise[Response] - // Used for running checks for pending messages with inactive consumers - // and checking responses from consumers iteratively for the first time when - // Produce is called. + // Used for checking responses from consumers iteratively + // For the first time when Produce is called. once sync.Once } type ProducerConfig struct { - // When enabled, messages that are sent to consumers that later die before - // processing them, will be re-inserted into the stream to be proceesed by - // another consumer - EnableReproduce bool `koanf:"enable-reproduce"` - // Interval duration in which producer checks for pending messages delivered - // to the consumers that are currently inactive. - CheckPendingInterval time.Duration `koanf:"check-pending-interval"` - // Duration after which consumer is considered to be dead if heartbeat - // is not updated. - KeepAliveTimeout time.Duration `koanf:"keepalive-timeout"` // Interval duration for checking the result set by consumers. CheckResultInterval time.Duration `koanf:"check-result-interval"` - CheckPendingItems int64 `koanf:"check-pending-items"` + // RequestTimeout is a TTL for any message sent to the redis stream + RequestTimeout time.Duration `koanf:"request-timeout"` } var DefaultProducerConfig = ProducerConfig{ - EnableReproduce: true, - CheckPendingInterval: time.Second, - KeepAliveTimeout: 5 * time.Minute, - CheckResultInterval: 5 * time.Second, - CheckPendingItems: 256, + CheckResultInterval: 5 * time.Second, + RequestTimeout: 3 * time.Hour, } var TestProducerConfig = ProducerConfig{ - EnableReproduce: false, - CheckPendingInterval: 10 * time.Millisecond, - KeepAliveTimeout: 100 * time.Millisecond, - CheckResultInterval: 5 * time.Millisecond, - CheckPendingItems: 256, + CheckResultInterval: 5 * time.Millisecond, + RequestTimeout: time.Minute, } func ProducerAddConfigAddOptions(prefix string, f *pflag.FlagSet) { - f.Bool(prefix+".enable-reproduce", DefaultProducerConfig.EnableReproduce, "when enabled, messages with dead consumer will be re-inserted into the stream") - f.Duration(prefix+".check-pending-interval", DefaultProducerConfig.CheckPendingInterval, "interval in which producer checks pending messages whether consumer processing them is inactive") f.Duration(prefix+".check-result-interval", DefaultProducerConfig.CheckResultInterval, "interval in which producer checks pending messages whether consumer processing them is inactive") - f.Duration(prefix+".keepalive-timeout", DefaultProducerConfig.KeepAliveTimeout, "timeout after which consumer is considered inactive if heartbeat wasn't performed") - f.Int64(prefix+".check-pending-items", DefaultProducerConfig.CheckPendingItems, "items to screen during check-pending") + f.Duration(prefix+".request-timeout", DefaultProducerConfig.RequestTimeout, "timeout after which the message in redis stream is considered as errored, this prevents workers from working on wrong requests indefinitely") } func NewProducer[Request any, Response any](client redis.UniversalClient, streamName string, cfg *ProducerConfig) (*Producer[Request, Response], error) { @@ -106,149 +88,129 @@ func NewProducer[Request any, Response any](client redis.UniversalClient, stream }, nil } -func (p *Producer[Request, Response]) errorPromisesFor(msgIds []string) { - p.promisesLock.Lock() - defer p.promisesLock.Unlock() - for _, msg := range msgIds { - if promise, found := p.promises[msg]; found { - promise.ProduceError(fmt.Errorf("internal error, consumer died while serving the request")) - delete(p.promises, msg) - } +func getUintParts(msgId string) ([2]uint64, error) { + idParts := strings.Split(msgId, "-") + if len(idParts) != 2 { + return [2]uint64{}, fmt.Errorf("invalid i.d: %v", msgId) } -} - -// checkAndReproduce reproduce pending messages that were sent to consumers -// that are currently inactive. -func (p *Producer[Request, Response]) checkAndReproduce(ctx context.Context) time.Duration { - staleIds, err := p.checkPending(ctx) + idTimeStamp, err := strconv.ParseUint(idParts[0], 10, 64) if err != nil { - log.Error("Checking pending messages", "error", err) - return p.cfg.CheckPendingInterval - } - if len(staleIds) == 0 { - return p.cfg.CheckPendingInterval + return [2]uint64{}, fmt.Errorf("invalid i.d: %v err: %w", msgId, err) } - if p.cfg.EnableReproduce { - err = p.reproduceIds(ctx, staleIds) - if err != nil { - log.Warn("filed reproducing messages", "err", err) - } - } else { - p.errorPromisesFor(staleIds) - } - return p.cfg.CheckPendingInterval -} - -func (p *Producer[Request, Response]) reproduceIds(ctx context.Context, staleIds []string) error { - log.Info("Attempting to claim", "messages", staleIds) - claimedMsgs, err := p.client.XClaim(ctx, &redis.XClaimArgs{ - Stream: p.redisStream, - Group: p.redisGroup, - Consumer: p.id, - MinIdle: p.cfg.KeepAliveTimeout, - Messages: staleIds, - }).Result() + idSerial, err := strconv.ParseUint(idParts[1], 10, 64) if err != nil { - return fmt.Errorf("claiming ownership on messages: %v, error: %w", staleIds, err) + return [2]uint64{}, fmt.Errorf("invalid i.d serial: %v err: %w", msgId, err) } - for _, msg := range claimedMsgs { - data, ok := (msg.Values[messageKey]).(string) - if !ok { - log.Error("redis producer reproduce: message not string", "id", msg.ID, "value", msg.Values[messageKey]) - continue - } - var req Request - if err := json.Unmarshal([]byte(data), &req); err != nil { - log.Error("redis producer reproduce: message not a request", "id", msg.ID, "err", err, "value", msg.Values[messageKey]) - continue - } - if _, err := p.client.XAck(ctx, p.redisStream, p.redisGroup, msg.ID).Result(); err != nil { - log.Error("redis producer reproduce: could not ACK", "id", msg.ID, "err", err) - continue - } - // Only re-insert messages that were removed the the pending list first. - if _, err := p.reproduce(ctx, req, msg.ID); err != nil { - log.Error("redis producer reproduce: error", "err", err) - } - } - return nil + return [2]uint64{idTimeStamp, idSerial}, nil } -func setMinIdInt(min *[2]uint64, id string) error { - idParts := strings.Split(id, "-") - if len(idParts) != 2 { - return fmt.Errorf("invalid i.d: %v", id) - } - idTimeStamp, err := strconv.ParseUint(idParts[0], 10, 64) +// cmpMsgId compares two msgid's and returns (0) if equal, (-1) if msgId1 < msgId2, (1) if msgId1 > msgId2, (-2) if not comparable (or error) +func cmpMsgId(msgId1, msgId2 string) int { + id1, err := getUintParts(msgId1) if err != nil { - return fmt.Errorf("invalid i.d: %v err: %w", id, err) - } - if idTimeStamp > min[0] { - return nil + log.Trace("error comparing msgIds", "msgId1", msgId1, "msgId2", msgId2) + return -2 } - idSerial, err := strconv.ParseUint(idParts[1], 10, 64) + id2, err := getUintParts(msgId2) if err != nil { - return fmt.Errorf("invalid i.d serial: %v err: %w", id, err) - } - if idTimeStamp < min[0] { - min[0] = idTimeStamp - min[1] = idSerial - return nil + log.Trace("error comparing msgIds", "msgId1", msgId1, "msgId2", msgId2) + return -2 } - // idTimeStamp == min[0] - if idSerial < min[1] { - min[1] = idSerial + if id1[0] < id2[0] { + return -1 + } else if id1[0] > id2[0] { + return 1 + } else if id1[1] < id2[1] { + return -1 + } else if id1[1] > id2[1] { + return 1 } - return nil + return 0 } // checkResponses checks iteratively whether response for the promise is ready. func (p *Producer[Request, Response]) checkResponses(ctx context.Context) time.Duration { - minIdInt := [2]uint64{math.MaxUint64, math.MaxUint64} + log.Debug("redis producer: check responses starting") p.promisesLock.Lock() defer p.promisesLock.Unlock() responded := 0 errored := 0 + checked := 0 + allowedOldestID := fmt.Sprintf("%d-0", time.Now().Add(-p.cfg.RequestTimeout).UnixMilli()) for id, promise := range p.promises { if ctx.Err() != nil { return 0 } - res, err := p.client.Get(ctx, id).Result() + checked++ + resultKey := ResultKeyFor(p.redisStream, id) + res, err := p.client.Get(ctx, resultKey).Result() if err != nil { - errSetId := setMinIdInt(&minIdInt, id) - if errSetId != nil { - log.Error("error setting minId", "err", err) - return p.cfg.CheckResultInterval - } if !errors.Is(err, redis.Nil) { - log.Error("Error reading value in redis", "key", id, "error", err) + log.Error("Error reading value in redis", "key", resultKey, "error", err) + } else if cmpMsgId(id, allowedOldestID) == -1 { + // The request this producer is waiting for has been past its TTL or is older than current PEL's lower, + // so safe to error and stop tracking this promise + promise.ProduceError(errors.New("error getting response, request has been waiting for too long")) + log.Error("error getting response, request has been waiting past its TTL") + errored++ + delete(p.promises, id) } continue } var resp Response if err := json.Unmarshal([]byte(res), &resp); err != nil { promise.ProduceError(fmt.Errorf("error unmarshalling: %w", err)) - log.Error("Error unmarshaling", "value", res, "error", err) + log.Error("redis producer: Error unmarshaling", "value", res, "error", err) errored++ } else { promise.Produce(resp) responded++ } + p.client.Del(ctx, resultKey) delete(p.promises, id) } - var trimmed int64 - var trimErr error - minId := "+" - if minIdInt[0] < math.MaxUint64 { - minId = fmt.Sprintf("%d-%d", minIdInt[0], minIdInt[1]) - trimmed, trimErr = p.client.XTrimMinID(ctx, p.redisStream, minId).Result() - } else { - trimmed, trimErr = p.client.XTrimMaxLen(ctx, p.redisStream, 0).Result() - } - log.Trace("trimming", "id", minId, "trimmed", trimmed, "responded", responded, "errored", errored, "trim-err", trimErr) + log.Debug("checkResponses", "responded", responded, "errored", errored, "checked", checked) return p.cfg.CheckResultInterval } +func (p *Producer[Request, Response]) clearMessages(ctx context.Context) time.Duration { + pelData, err := p.client.XPending(ctx, p.redisStream, p.redisGroup).Result() + if err != nil { + log.Error("error getting PEL data from xpending, xtrimming is disabled", "err", err) + } + // XDEL on consumer side already deletes acked messages (mark as deleted) but doesnt claim the memory back, XTRIM helps in claiming this memory in normal conditions + // pelData might be outdated when we do the xtrim, but thats ok as the messages are also being trimmed by other producers + if pelData != nil && pelData.Lower != "" { + trimmed, trimErr := p.client.XTrimMinID(ctx, p.redisStream, pelData.Lower).Result() + log.Debug("trimming", "xTrimMinID", pelData.Lower, "trimmed", trimmed, "trim-err", trimErr) + // Check if pelData.Lower has been past its TTL and if it is then ack it to remove from PEL and delete it, once + // its taken out from PEL the producer that sent this request will handle the corresponding promise accordingly (as its past TTL) + allowedOldestID := fmt.Sprintf("%d-0", time.Now().Add(-p.cfg.RequestTimeout).UnixMilli()) + if cmpMsgId(pelData.Lower, allowedOldestID) == -1 { + if err := p.client.XClaim(ctx, &redis.XClaimArgs{ + Stream: p.redisStream, + Group: p.redisGroup, + Consumer: p.id, + MinIdle: 0, + Messages: []string{pelData.Lower}, + }).Err(); err != nil { + log.Error("error claiming PEL's lower message thats past its TTL", "msgID", pelData.Lower, "err", err) + return 5 * p.cfg.CheckResultInterval + } + if _, err := p.client.XAck(ctx, p.redisStream, p.redisGroup, pelData.Lower).Result(); err != nil { + log.Error("error acking PEL's lower message thats past its TTL", "msgID", pelData.Lower, "err", err) + return 5 * p.cfg.CheckResultInterval + } + if _, err := p.client.XDel(ctx, p.redisStream, pelData.Lower).Result(); err != nil { + log.Error("error deleting PEL's lower message thats past its TTL", "msgID", pelData.Lower, "err", err) + return 5 * p.cfg.CheckResultInterval + } + return 0 + } + } + return 5 * p.cfg.CheckResultInterval +} + func (p *Producer[Request, Response]) Start(ctx context.Context) { p.StopWaiter.Start(ctx, p) } @@ -259,101 +221,31 @@ func (p *Producer[Request, Response]) promisesLen() int { return len(p.promises) } -// reproduce is used when Producer claims ownership on the pending -// message that was sent to inactive consumer and reinserts it into the stream, -// so that seamlessly return the answer in the same promise. -func (p *Producer[Request, Response]) reproduce(ctx context.Context, value Request, oldKey string) (*containers.Promise[Response], error) { +func (p *Producer[Request, Response]) produce(ctx context.Context, value Request) (*containers.Promise[Response], error) { val, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("marshaling value: %w", err) } - // catching the promiseLock before we sendXadd makes sure promise ids will - // be always ascending + // catching the promiseLock before we sendXadd makes sure promise ids will be always ascending p.promisesLock.Lock() defer p.promisesLock.Unlock() - id, err := p.client.XAdd(ctx, &redis.XAddArgs{ + msgId, err := p.client.XAdd(ctx, &redis.XAddArgs{ Stream: p.redisStream, Values: map[string]any{messageKey: val}, }).Result() if err != nil { return nil, fmt.Errorf("adding values to redis: %w", err) } - promise := p.promises[oldKey] - if oldKey != "" && promise == nil { - // This will happen if the old consumer became inactive but then ack_d - // the message afterwards. - // don't error - log.Warn("tried reproducing a message but it wasn't found - probably got response", "oldKey", oldKey) - } - if oldKey == "" || promise == nil { - pr := containers.NewPromise[Response](nil) - promise = &pr - } - delete(p.promises, oldKey) - p.promises[id] = promise - return promise, nil + promise := containers.NewPromise[Response](nil) + p.promises[msgId] = &promise + return &promise, nil } func (p *Producer[Request, Response]) Produce(ctx context.Context, value Request) (*containers.Promise[Response], error) { log.Debug("Redis stream producing", "value", value) p.once.Do(func() { - p.StopWaiter.CallIteratively(p.checkAndReproduce) p.StopWaiter.CallIteratively(p.checkResponses) + p.StopWaiter.CallIteratively(p.clearMessages) }) - return p.reproduce(ctx, value, "") -} - -// Check if a consumer is with specified ID is alive. -func (p *Producer[Request, Response]) isConsumerAlive(ctx context.Context, consumerID string) bool { - if _, err := p.client.Get(ctx, heartBeatKey(consumerID)).Int64(); err != nil { - return false - } - return true -} - -func (p *Producer[Request, Response]) havePromiseFor(messageID string) bool { - p.promisesLock.Lock() - defer p.promisesLock.Unlock() - _, found := p.promises[messageID] - return found -} - -// returns ids of pending messages that's worker doesn't appear alive -func (p *Producer[Request, Response]) checkPending(ctx context.Context) ([]string, error) { - pendingMessages, err := p.client.XPendingExt(ctx, &redis.XPendingExtArgs{ - Stream: p.redisStream, - Group: p.redisGroup, - Start: "-", - End: "+", - Count: p.cfg.CheckPendingItems, - }).Result() - - if err != nil && !errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("querying pending messages: %w", err) - } - if len(pendingMessages) == 0 { - return nil, nil - } - if len(pendingMessages) >= int(p.cfg.CheckPendingItems) { - log.Warn("redis producer: many pending items found", "stream", p.redisStream, "check-pending-items", p.cfg.CheckPendingItems) - } - // IDs of the pending messages with inactive consumers. - var ids []string - active := make(map[string]bool) - for _, msg := range pendingMessages { - // Ignore messages not produced by this producer. - if !p.havePromiseFor(msg.ID) { - continue - } - alive, found := active[msg.Consumer] - if !found { - alive = p.isConsumerAlive(ctx, msg.Consumer) - active[msg.Consumer] = alive - } - if alive { - continue - } - ids = append(ids, msg.ID) - } - return ids, nil + return p.produce(ctx, value) } diff --git a/pubsub/pubsub_test.go b/pubsub/pubsub_test.go index 9f774b6372..c82a35e0b8 100644 --- a/pubsub/pubsub_test.go +++ b/pubsub/pubsub_test.go @@ -9,10 +9,12 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" "github.com/google/go-cmp/cmp" "github.com/google/uuid" + "github.com/redis/go-redis/v9" + + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/redisutil" ) @@ -23,7 +25,8 @@ var ( ) type testRequest struct { - Request string + Request string + IsInvalid bool } type testResponse struct { @@ -45,36 +48,21 @@ func destroyRedisGroup(ctx context.Context, t *testing.T, streamName string, cli } } -type configOpt interface { - apply(consCfg *ConsumerConfig, prodCfg *ProducerConfig) -} - -type withReproduce struct { - reproduce bool -} - -func (e *withReproduce) apply(_ *ConsumerConfig, prodCfg *ProducerConfig) { - prodCfg.EnableReproduce = e.reproduce -} - func producerCfg() *ProducerConfig { return &ProducerConfig{ - EnableReproduce: TestProducerConfig.EnableReproduce, - CheckPendingInterval: TestProducerConfig.CheckPendingInterval, - KeepAliveTimeout: TestProducerConfig.KeepAliveTimeout, - CheckResultInterval: TestProducerConfig.CheckResultInterval, - CheckPendingItems: TestProducerConfig.CheckPendingItems, + CheckResultInterval: TestProducerConfig.CheckResultInterval, + RequestTimeout: 2 * time.Second, } } func consumerCfg() *ConsumerConfig { return &ConsumerConfig{ ResponseEntryTimeout: TestConsumerConfig.ResponseEntryTimeout, - KeepAliveTimeout: TestConsumerConfig.KeepAliveTimeout, + IdletimeToAutoclaim: TestConsumerConfig.IdletimeToAutoclaim, } } -func newProducerConsumers(ctx context.Context, t *testing.T, opts ...configOpt) (redis.UniversalClient, string, *Producer[testRequest, testResponse], []*Consumer[testRequest, testResponse]) { +func newProducerConsumers(ctx context.Context, t *testing.T) (redis.UniversalClient, string, *Producer[testRequest, testResponse], []*Consumer[testRequest, testResponse]) { t.Helper() redisClient, err := redisutil.RedisClientFromURL(redisutil.CreateTestRedis(ctx, t)) if err != nil { @@ -82,9 +70,7 @@ func newProducerConsumers(ctx context.Context, t *testing.T, opts ...configOpt) } prodCfg, consCfg := producerCfg(), consumerCfg() streamName := fmt.Sprintf("stream:%s", uuid.NewString()) - for _, o := range opts { - o.apply(consCfg, prodCfg) - } + producer, err := NewProducer[testRequest, testResponse](redisClient, streamName, prodCfg) if err != nil { t.Fatalf("Error creating new producer: %v", err) @@ -102,13 +88,6 @@ func newProducerConsumers(ctx context.Context, t *testing.T, opts ...configOpt) t.Cleanup(func() { ctx := context.Background() destroyRedisGroup(ctx, t, streamName, producer.client) - var keys []string - for _, c := range consumers { - keys = append(keys, c.heartBeatKey()) - } - if _, err := producer.client.Del(ctx, keys...).Result(); err != nil { - log.Debug("Error deleting heartbeat keys", "error", err) - } }) return redisClient, streamName, producer, consumers } @@ -125,10 +104,10 @@ func msgForIndex(idx int) string { return fmt.Sprintf("msg: %d", idx) } -func wantMessages(n int) []string { +func wantMessages(n int, group string) []string { var ret []string for i := 0; i < n; i++ { - ret = append(ret, msgForIndex(i)) + ret = append(ret, group+msgForIndex(i)) } sort.Strings(ret) return ret @@ -143,10 +122,14 @@ func flatten(responses [][]string) []string { return ret } -func produceMessages(ctx context.Context, msgs []string, producer *Producer[testRequest, testResponse]) ([]*containers.Promise[testResponse], error) { +func produceMessages(ctx context.Context, msgs []string, producer *Producer[testRequest, testResponse], withInvalidEntries bool) ([]*containers.Promise[testResponse], error) { var promises []*containers.Promise[testResponse] - for i := 0; i < messagesCount; i++ { - promise, err := producer.Produce(ctx, testRequest{Request: msgs[i]}) + for i := 0; i < len(msgs); i++ { + req := testRequest{Request: msgs[i]} + if withInvalidEntries && i%50 == 0 { + req.IsInvalid = true + } + promise, err := producer.Produce(ctx, req) if err != nil { return nil, err } @@ -197,51 +180,97 @@ func consume(ctx context.Context, t *testing.T, consumers []*Consumer[testReques continue } gotMessages[idx][res.ID] = res.Value.Request - resp := fmt.Sprintf("result for: %v", res.ID) - if err := c.SetResult(ctx, res.ID, testResponse{Response: resp}); err != nil { - t.Errorf("Error setting a result: %v", err) + if !res.Value.IsInvalid { + resp := fmt.Sprintf("result for: %v", res.ID) + if err := c.SetResult(ctx, res.ID, testResponse{Response: resp}); err != nil { + t.Errorf("Error setting a result: %v", err) + } + wantResponses[idx] = append(wantResponses[idx], resp) } - wantResponses[idx] = append(wantResponses[idx], resp) + res.Ack() } }) } return wantResponses } -func TestRedisProduce(t *testing.T) { +func TestRedisProduceComplex(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) t.Parallel() for _, tc := range []struct { - name string - killConsumers bool - autoRecover bool + name string + entriesCount []int + numProducers int + killConsumers bool + withInvalidEntries bool // If this is set, then every 50th entry is invalid (requests that can't be solved by any consumer) }{ { - name: "all consumers are active", - killConsumers: false, - autoRecover: false, + name: "one producer, all consumers are active", + entriesCount: []int{messagesCount}, + numProducers: 1, + }, + { + name: "two producers, all consumers are active", + entriesCount: []int{20, 20}, + numProducers: 2, }, { - name: "some consumers killed, others should take over their work", + name: "one producer, some consumers killed, others should take over their work", + entriesCount: []int{messagesCount}, + numProducers: 1, killConsumers: true, - autoRecover: true, }, + { - name: "some consumers killed, should return failure", + name: "two producers, some consumers killed, others should take over their work, unequal number of requests from producers", + entriesCount: []int{messagesCount, 2 * messagesCount}, + numProducers: 2, killConsumers: true, - autoRecover: false, + }, + { + name: "two producers, some consumers killed, others should take over their work, some invalid entries, unequal number of requests from producers", + entriesCount: []int{messagesCount, 2 * messagesCount}, + numProducers: 2, + killConsumers: true, + withInvalidEntries: true, }, } { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - redisClient, streamName, producer, consumers := newProducerConsumers(ctx, t, &withReproduce{tc.autoRecover}) - producer.Start(ctx) - wantMsgs := wantMessages(messagesCount) - promises, err := produceMessages(ctx, wantMsgs, producer) - if err != nil { - t.Fatalf("Error producing messages: %v", err) + + var producers []*Producer[testRequest, testResponse] + redisClient, streamName, producer, consumers := newProducerConsumers(ctx, t) + producers = append(producers, producer) + if tc.numProducers == 2 { + producer, err := NewProducer[testRequest, testResponse](redisClient, streamName, producerCfg()) + if err != nil { + t.Fatalf("Error creating second producer: %v", err) + } + producers = append(producers, producer) + } + + for _, producer := range producers { + producer.Start(ctx) } + + var entries [][]string + if tc.numProducers == 2 { + entries = append(entries, wantMessages(tc.entriesCount[0], "1.")) + entries = append(entries, wantMessages(tc.entriesCount[1], "2.")) + } else { + entries = append(entries, wantMessages(tc.entriesCount[0], "")) + } + + var promises [][]*containers.Promise[testResponse] + for i := 0; i < tc.numProducers; i++ { + prs, err := produceMessages(ctx, entries[i], producers[i], tc.withInvalidEntries) + if err != nil { + t.Fatalf("Error producing messages from producer%d: %v", i, err) + } + promises = append(promises, prs) + } + gotMessages := messagesMaps(len(consumers)) if tc.killConsumers { // Consumer messages in every third consumer but don't ack them to check @@ -252,40 +281,79 @@ func TestRedisProduce(t *testing.T) { if err != nil { t.Errorf("Error consuming message: %v", err) } - if !tc.autoRecover { - gotMessages[i][req.ID] = req.Value.Request + if req == nil { + t.Error("Didn't consume any message") } + // Kills the actnotifier hence allowing XAUTOCLAIM consumers[i].StopAndWait() } } + time.Sleep(time.Second) wantResponses := consume(ctx, t, consumers, gotMessages) - gotResponses, errIndexes := awaitResponses(ctx, promises) - if len(errIndexes) != 0 && tc.autoRecover { - t.Fatalf("Error awaiting responses: %v", errIndexes) + + var gotResponses []string + for i := 0; i < tc.numProducers; i++ { + grs, errIndexes := awaitResponses(ctx, promises[i]) + if tc.withInvalidEntries { + if errIndexes[len(errIndexes)-1]+50 < len(entries[i]) { + t.Fatalf("Unexpected number of invalid requests while awaiting responses") + } + for j, idx := range errIndexes { + if idx != j*50 { + t.Fatalf("Invalid request' index mismatch want: %d got %d", j*50, idx) + } + } + } else if len(errIndexes) != 0 { + t.Fatalf("Error awaiting responses from promises %d: %v", i, errIndexes) + } + gotResponses = append(gotResponses, grs...) } - producer.StopAndWait() + for _, c := range consumers { c.StopAndWait() } - got, err := mergeValues(gotMessages) + + got, err := mergeValues(gotMessages, tc.withInvalidEntries) if err != nil { t.Fatalf("mergeMaps() unexpected error: %v", err) } + // Only when there are invalid entries got will have duplicates + if tc.withInvalidEntries { + got = removeDuplicates(got) + } + + var combinedEntries []string + for i := 0; i < tc.numProducers; i++ { + combinedEntries = append(combinedEntries, entries[i]...) + } + wantMsgs := combinedEntries if diff := cmp.Diff(wantMsgs, got); diff != "" { t.Errorf("Unexpected diff (-want +got):\n%s\n", diff) } - wantResp := flatten(wantResponses) + sort.Strings(gotResponses) + wantResp := flatten(wantResponses) if diff := cmp.Diff(wantResp, gotResponses); diff != "" { t.Errorf("Unexpected diff in responses:\n%s\n", diff) } - if cnt := producer.promisesLen(); cnt != 0 { - t.Errorf("Producer still has %d unfullfilled promises", cnt) + + // Check each producers all promises were responded to + for i := 0; i < tc.numProducers; i++ { + if cnt := producers[i].promisesLen(); cnt != 0 { + t.Errorf("Producer%d still has %d unfullfilled promises", i, cnt) + } } + // Trigger a trim - producer.checkResponses(ctx) + time.Sleep(time.Second) + for i := 0; i < tc.numProducers; i++ { + producers[i].checkResponses(ctx) + producers[i].StopAndWait() + } + + // Check that no messages remain in the stream msgs, err := redisClient.XRange(ctx, streamName, "-", "+").Result() if err != nil { t.Errorf("XRange failed: %v", err) @@ -297,14 +365,27 @@ func TestRedisProduce(t *testing.T) { } } +func removeDuplicates(list []string) []string { + capture := map[string]bool{} + var ret []string + for _, elem := range list { + if _, found := capture[elem]; !found { + ret = append(ret, elem) + capture[elem] = true + } + } + sort.Strings(ret) + return ret +} + // mergeValues merges maps from the slice and returns their values. // Returns and error if there exists duplicate key. -func mergeValues(messages []map[string]string) ([]string, error) { +func mergeValues(messages []map[string]string, withInvalidEntries bool) ([]string, error) { res := make(map[string]any) var ret []string for _, m := range messages { for k, v := range m { - if _, found := res[k]; found { + if _, found := res[k]; found && !withInvalidEntries { return nil, fmt.Errorf("duplicate key: %v", k) } res[k] = v diff --git a/relay/relay_stress_test.go b/relay/relay_stress_test.go index 9a8875a429..93ba510193 100644 --- a/relay/relay_stress_test.go +++ b/relay/relay_stress_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/broadcastclient" @@ -47,6 +48,7 @@ func (r *DummyUpStream) PopulateFeedBacklogByNumber(ctx context.Context, backlog was := r.broadcaster.GetCachedMessageCount() var seqNums []arbutil.MessageIndex for i := was; i < was+backlogSize; i++ { + // #nosec G115 seqNums = append(seqNums, arbutil.MessageIndex(i)) } @@ -160,7 +162,7 @@ func largeBacklogRelayTestImpl(t *testing.T, numClients, backlogSize, l2MsgSize connected++ } } - if int32(connected) != int32(numClients) { + if connected != numClients { t.Fail() } log.Info("number of clients connected", "expected", numClients, "got", connected) diff --git a/scripts/build-brotli.sh b/scripts/build-brotli.sh index 7160936baa..1a23a88ae0 100755 --- a/scripts/build-brotli.sh +++ b/scripts/build-brotli.sh @@ -2,7 +2,7 @@ set -e -mydir=`dirname $0` +mydir=$(dirname "$0") cd "$mydir" BUILD_WASM=false @@ -35,7 +35,7 @@ usage(){ echo "all relative paths are relative to script location" } -while getopts "s:t:c:D:wldhf" option; do +while getopts "n:s:t:c:D:wldhf" option; do case $option in h) usage @@ -62,6 +62,9 @@ while getopts "s:t:c:D:wldhf" option; do s) SOURCE_DIR="$OPTARG" ;; + *) + usage + ;; esac done @@ -74,7 +77,7 @@ if [ ! -d "$TARGET_DIR" ]; then mkdir -p "${TARGET_DIR}lib" ln -s "lib" "${TARGET_DIR}lib64" # Fedora build fi -TARGET_DIR_ABS=`cd -P "$TARGET_DIR"; pwd` +TARGET_DIR_ABS=$(cd -P "$TARGET_DIR"; pwd) if $USE_DOCKER; then @@ -94,9 +97,9 @@ cd "$SOURCE_DIR" if $BUILD_WASM; then mkdir -p buildfiles/build-wasm mkdir -p buildfiles/install-wasm - TEMP_INSTALL_DIR_ABS=`cd -P buildfiles/install-wasm; pwd` + TEMP_INSTALL_DIR_ABS=$(cd -P buildfiles/install-wasm; pwd) cd buildfiles/build-wasm - cmake ../../ -DCMAKE_C_COMPILER=emcc -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-fPIC -DCMAKE_INSTALL_PREFIX="$TEMP_INSTALL_DIR_ABS" -DCMAKE_AR=`which emar` -DCMAKE_RANLIB=`which touch` + cmake ../../ -DCMAKE_C_COMPILER=emcc -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-fPIC -DCMAKE_INSTALL_PREFIX="$TEMP_INSTALL_DIR_ABS" -DCMAKE_AR="$(which emar)" -DCMAKE_RANLIB="$(which touch)" make -j make install cp -rv "$TEMP_INSTALL_DIR_ABS/lib" "$TARGET_DIR_ABS/lib-wasm" diff --git a/scripts/check-build.sh b/scripts/check-build.sh new file mode 100755 index 0000000000..6084900f96 --- /dev/null +++ b/scripts/check-build.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# This script checks the prerequisites for building Arbitrum Nitro locally. + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Documentation link for installation instructions +INSTALLATION_DOCS_URL="Refer to https://docs.arbitrum.io/run-arbitrum-node/nitro/build-nitro-locally for installation." + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +EXIT_CODE=0 + +# Detect operating system +OS=$(uname -s) +echo -e "${BLUE}Detected OS: $OS${NC}" + +# Step 1: Check Docker Installation +if command_exists docker; then + echo -e "${GREEN}Docker is installed.${NC}" +else + echo -e "${RED}Docker is not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 2: Check if Docker service is running +if [[ "$OS" == "Linux" ]] && ! pidof dockerd >/dev/null; then + echo -e "${YELLOW}Docker service is not running on Linux. Start it with: sudo service docker start${NC}" + EXIT_CODE=1 +elif [[ "$OS" == "Darwin" ]] && ! docker info >/dev/null 2>&1; then + echo -e "${YELLOW}Docker service is not running on macOS. Ensure Docker Desktop is started.${NC}" + EXIT_CODE=1 +else + echo -e "${GREEN}Docker service is running.${NC}" +fi + +# Step 3: Check the version tag +VERSION_TAG=$(git tag --points-at HEAD | sed '/-/!s/$/_/' | sort -rV | sed 's/_$//' | head -n 1 | grep ^ || git show -s --pretty=%D | sed 's/, /\n/g' | grep -v '^origin/' | grep -v '^grafted\|HEAD\|master\|main$' || echo "") +if [[ -z "${VERSION_TAG}" ]]; then + echo -e "${YELLOW}Untagged version of Nitro checked out, may not be fully tested.${NC}" +else + echo -e "${GREEN}You are on Nitro version tag: $VERSION_TAG${NC}" +fi + +# Check if submodules are properly initialized and updated +if git submodule status | grep -qE '^-|\+'; then + echo -e "${YELLOW}Submodules are not properly initialized or updated. Run: git submodule update --init --recursive --force${NC}" + EXIT_CODE=1 +else + echo -e "${GREEN}All submodules are properly initialized and up to date.${NC}" +fi + +# Step 4: Check if Nitro Docker Image is built +if docker images | grep -q "nitro-node"; then + echo -e "${GREEN}Nitro Docker image is built.${NC}" +else + echo -e "${YELLOW}Nitro Docker image is not built. Build it using: docker build . --tag nitro-node${NC}" +fi + +# Step 5: Check prerequisites for building binaries +if [[ "$OS" == "Linux" ]]; then + prerequisites=(git curl make cmake npm golang clang make gotestsum wasm2wat wasm-ld python3 yarn) +else + prerequisites=(git curl make cmake npm go golangci-lint wasm2wat clang wasm-ld gotestsum yarn) +fi + +for pkg in "${prerequisites[@]}"; do + EXISTS=$(command_exists "$pkg") + [[ "$pkg" == "make" ]] && pkg="build-essential" + [[ "$pkg" == "wasm2wat" ]] && pkg="wabt" + [[ "$pkg" == "clang" ]] && pkg="llvm" + [[ "$pkg" == "wasm-ld" ]] && pkg="lld" + if $EXISTS; then + # There is no way to check for wabt / llvm directly, since they install multiple tools + # So instead, we check for wasm2wat and clang, which are part of wabt and llvm respectively + # and if they are installed, we assume wabt / llvm is installed else we ask the user to install wabt / llvm + + echo -e "${GREEN}$pkg is installed.${NC}" + else + echo -e "${RED}$pkg is not installed. Please install $pkg.${NC}" + EXIT_CODE=1 + fi +done + +# Step 6: Check Node.js version +if command_exists node && node -v | grep -q "v18"; then + echo -e "${GREEN}Node.js version 18 is installed.${NC}" +else + echo -e "${RED}Node.js version 18 not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 7a: Check Rust version +if command_exists rustc && rustc --version | grep -q "1.80.1"; then + echo -e "${GREEN}Rust version 1.80.1 is installed.${NC}" +else + echo -e "${RED}Rust version 1.80.1 not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 7b: Check Rust nightly toolchain +if rustup toolchain list | grep -q "nightly"; then + echo -e "${GREEN}Rust nightly toolchain is installed.${NC}" +else + echo -e "${RED}Rust nightly toolchain is not installed. Install it using: rustup toolchain install nightly${NC}" + EXIT_CODE=1 +fi + +# Step 8: Check Go version +go_version_needed=$(grep "^go " go.mod | awk '{print $2}') +if command_exists go && go version | grep -q "$go_version_needed"; then + echo -e "${GREEN}Go version $go_version_needed is installed.${NC}" +else + echo -e "${RED}Go version $go_version_needed not installed.${NC}" + EXIT_CODE=1 +fi + +# Step 9: Check Foundry installation +if command_exists foundryup; then + echo -e "${GREEN}Foundry is installed.${NC}" +else + echo -e "${RED}Foundry is not installed.${NC}" + EXIT_CODE=1 +fi + + +if [ $EXIT_CODE != 0 ]; then + echo -e "${RED}One or more dependencies missing. $INSTALLATION_DOCS_URL${NC}" +else + echo -e "${BLUE}Build readiness check passed.${NC}" +fi + +exit $EXIT_CODE + diff --git a/scripts/convert-databases.bash b/scripts/convert-databases.bash index 3020b389b4..baddcdcacd 100755 --- a/scripts/convert-databases.bash +++ b/scripts/convert-databases.bash @@ -33,7 +33,7 @@ printStatus() { } printUsage() { -echo Usage: $0 \[OPTIONS..\] +echo Usage: "$0" \[OPTIONS..\] echo echo OPTIONS: echo "--dbconv dbconv binary path (default: \"$DEFAULT_DBCONV\")" @@ -42,15 +42,15 @@ echo Usage: $0 \[OPTIONS..\] echo "--force remove destination directory if it exists" echo "--skip-existing skip convertion of databases which directories already exist in the destination directory" echo "--clean sets what should be removed in case of error, possible values:" - echo " \"failed\" - remove database which conversion failed (default)" + echo " \"failed\" - remove database which conversion failed (default)" echo " \"none\" - remove nothing, leave unfinished and potentially corrupted databases" echo " \"all\" - remove whole destination directory" } removeDir() { cmd="rm -r \"$1\"" - echo $cmd - eval $cmd + echo "$cmd" + eval "$cmd" return $? } @@ -62,7 +62,7 @@ cleanup() { ;; failed) echo "== Note: removing only failed destination directory" - dstdir=$(echo $dst/$1 | tr -s /) + dstdir=$(echo "$dst"/"$1" | tr -s /) removeDir "$dstdir" ;; none) @@ -127,8 +127,8 @@ if $force && $skip_existing; then exit 1 fi -if [ $clean != "all" ] && [ $clean != "failed" ] && [ $clean != "none" ] ; then - echo Error: Invalid --clean value: $clean +if [ "$clean" != "all" ] && [ "$clean" != "failed" ] && [ "$clean" != "none" ] ; then + echo Error: Invalid --clean value: "$clean" printUsage exit 1 fi @@ -138,8 +138,8 @@ if ! [ -e "$dbconv" ]; then exit 1 fi -if ! [ -n "$dst" ]; then - echo Error: Missing destination directory \(\-\-dst\) +if [ -z "$dst" ]; then + echo "Error: Missing destination directory (--dst)" printUsage exit 1 fi @@ -168,9 +168,8 @@ fi if [ -e "$dst" ] && ! $skip_existing; then if $force; then - echo == Warning! Destination already exists, --force is set, removing all files under path: "$dst" - removeDir "$dst" - if [ $? -ne 0 ]; then + echo "== Warning! Destination already exists, --force is set, removing all files under path: $dst" + if ! removeDir "$dst"; then echo Error: failed to remove "$dst" exit 1 fi @@ -183,14 +182,13 @@ fi convert_result= convert () { srcdir="$src"/$1 - dstdir=$(echo $dst/$1 | tr -s /) - if ! [ -e $dstdir ]; then + dstdir=$(echo "$dst"/"$1" | tr -s /) + if ! [ -e "$dstdir" ]; then echo "== Converting $1 db" cmd="$dbconv --src.db-engine=leveldb --src.data \"$srcdir\" --dst.db-engine=pebble --dst.data \"$dstdir\" --convert --compact" - echo $cmd - eval $cmd - if [ $? -ne 0 ]; then - cleanup $1 + echo "$cmd" + if ! eval "$cmd"; then + cleanup "$1" convert_result="FAILED" return 1 fi @@ -221,9 +219,8 @@ if ! [ -e "$dst"/l2chaindata/ancient ]; then ancient_dst=$(echo "$dst"/l2chaindata/ | tr -s /) echo "== Copying l2chaindata ancients" cmd="cp -r \"$ancient_src\" \"$ancient_dst\"" - echo $cmd - eval $cmd - if [ $? -ne 0 ]; then + echo "$cmd" + if ! eval "$cmd"; then l2chaindata_ancient_status="FAILED (failed to copy)" cleanup "l2chaindata" printStatus @@ -249,7 +246,7 @@ if [ $res -ne 0 ]; then exit 1 fi -if [ -e $src/wasm ]; then +if [ -e "$src"/wasm ]; then convert "wasm" res=$? wasm_status=$convert_result @@ -262,7 +259,7 @@ else wasm_status="not found in source directory" fi -if [ -e $src/classic-msg ]; then +if [ -e "$src"/classic-msg ]; then convert "classic-msg" res=$? classicmsg_status=$convert_result diff --git a/scripts/fuzz.bash b/scripts/fuzz.bash index 6271b917b6..a73c208e88 100755 --- a/scripts/fuzz.bash +++ b/scripts/fuzz.bash @@ -2,12 +2,12 @@ set -e -mydir=`dirname $0` +mydir=$(dirname "$0") cd "$mydir" function printusage { - echo Usage: $0 --build \[--binary-path PATH\] - echo " " $0 \ \[--binary-path PATH\] \[--fuzzcache-path PATH\] \[--nitro-path PATH\] \[--duration DURATION\] + echo Usage: "$0" --build \[--binary-path PATH\] + echo " " "$0" \ \[--binary-path PATH\] \[--fuzzcache-path PATH\] \[--nitro-path PATH\] \[--duration DURATION\] echo echo fuzzer names: echo " " FuzzPrecompiles @@ -22,7 +22,6 @@ if [[ $# -eq 0 ]]; then exit fi -fuzz_executable=../target/bin/system_test.fuzz binpath=../target/bin/ fuzzcachepath=../target/var/fuzz-cache nitropath=../ @@ -72,7 +71,7 @@ while [[ $# -gt 0 ]]; do shift ;; FuzzPrecompiles | FuzzStateTransition) - if [[ ! -z "$test_name" ]]; then + if [[ -n "$test_name" ]]; then echo can only run one fuzzer at a time exit 1 fi @@ -81,7 +80,7 @@ while [[ $# -gt 0 ]]; do shift ;; FuzzInboxMultiplexer) - if [[ ! -z "$test_name" ]]; then + if [[ -n "$test_name" ]]; then echo can only run one fuzzer at a time exit 1 fi @@ -102,17 +101,17 @@ fi if $run_build; then for build_group in system_tests arbstate; do - go test -c ${nitropath}/${build_group} -fuzz Fuzz -o "$binpath"/${build_group}.fuzz + go test -c "${nitropath}"/${build_group} -fuzz Fuzz -o "$binpath"/${build_group}.fuzz done fi -if [[ ! -z $test_group ]]; then - timeout "$((60 * duration))" "$binpath"/${test_group}.fuzz -test.run "^$" -test.fuzzcachedir "$fuzzcachepath" -test.fuzz $test_name || exit_status=$? +if [[ -n $test_group ]]; then + timeout "$((60 * duration))" "$binpath"/${test_group}.fuzz -test.run "^$" -test.fuzzcachedir "$fuzzcachepath" -test.fuzz "$test_name" || exit_status=$? fi -if [ -n "$exit_status" ] && [ $exit_status -ne 0 ] && [ $exit_status -ne 124 ]; then +if [ -n "$exit_status" ] && [ "$exit_status" -ne 0 ] && [ "$exit_status" -ne 124 ]; then echo "Fuzzing failed." - exit $exit_status + exit "$exit_status" fi echo "Fuzzing succeeded." diff --git a/scripts/split-val-entry.sh b/scripts/split-val-entry.sh index 42e0c5fe08..ab8c520918 100755 --- a/scripts/split-val-entry.sh +++ b/scripts/split-val-entry.sh @@ -39,4 +39,4 @@ for port in 52000 52001; do done done echo launching nitro-node -/usr/local/bin/nitro --validation.wasm.allowed-wasm-module-roots /home/user/nitro-legacy/machines,/home/user/target/machines --node.block-validator.validation-server-configs-list='[{"jwtsecret":"/tmp/nitro-val.jwt","url":"ws://127.0.0.10:52000"}, {"jwtsecret":"/tmp/nitro-val.jwt","url":"ws://127.0.0.10:52001"}]' "$@" +exec /usr/local/bin/nitro --validation.wasm.allowed-wasm-module-roots /home/user/nitro-legacy/machines,/home/user/target/machines --node.block-validator.validation-server-configs-list='[{"jwtsecret":"/tmp/nitro-val.jwt","url":"ws://127.0.0.10:52000"}, {"jwtsecret":"/tmp/nitro-val.jwt","url":"ws://127.0.0.10:52001"}]' "$@" diff --git a/scripts/startup-testnode.bash b/scripts/startup-testnode.bash index 701e7ff59a..5313a9ec5d 100755 --- a/scripts/startup-testnode.bash +++ b/scripts/startup-testnode.bash @@ -5,9 +5,9 @@ timeout 60 ./nitro-testnode/test-node.bash --init --dev || exit_status=$? -if [ -n "$exit_status" ] && [ $exit_status -ne 0 ] && [ $exit_status -ne 124 ]; then +if [ -n "$exit_status" ] && [ "$exit_status" -ne 0 ] && [ "$exit_status" -ne 124 ]; then echo "Startup failed." - exit $exit_status + exit "$exit_status" fi echo "Startup succeeded." diff --git a/staker/block_validator.go b/staker/block_validator.go index 8f5724beac..43e5c7d28f 100644 --- a/staker/block_validator.go +++ b/staker/block_validator.go @@ -16,12 +16,15 @@ import ( "testing" "time" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rlp" + "github.com/offchainlabs/nitro/arbnode/resourcemanager" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/containers" @@ -29,7 +32,8 @@ import ( "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/client/redis" - "github.com/spf13/pflag" + "github.com/offchainlabs/nitro/validator/inputs" + "github.com/offchainlabs/nitro/validator/server_api" ) var ( @@ -56,12 +60,12 @@ type BlockValidator struct { chainCaughtUp bool // can only be accessed from creation thread or if holding reorg-write - nextCreateBatch []byte - nextCreateBatchBlockHash common.Hash - nextCreateBatchMsgCount arbutil.MessageIndex - nextCreateBatchReread bool - nextCreateStartGS validator.GoGlobalState - nextCreatePrevDelayed uint64 + nextCreateBatch *FullBatchInfo + nextCreateBatchReread bool + prevBatchCache map[uint64][]byte + + nextCreateStartGS validator.GoGlobalState + nextCreatePrevDelayed uint64 // can only be accessed from from validation thread or if holding reorg-write lastValidGS validator.GoGlobalState @@ -94,6 +98,9 @@ type BlockValidator struct { // for testing only testingProgressMadeChan chan struct{} + // For troubleshooting failed validations + validationInputsWriter *inputs.Writer + fatalErr chan<- error MemoryFreeLimitChecker resourcemanager.LimitChecker @@ -106,13 +113,18 @@ type BlockValidatorConfig struct { ValidationServerConfigs []rpcclient.ClientConfig `koanf:"validation-server-configs"` ValidationPoll time.Duration `koanf:"validation-poll" reload:"hot"` PrerecordedBlocks uint64 `koanf:"prerecorded-blocks" reload:"hot"` + RecordingIterLimit uint64 `koanf:"recording-iter-limit"` ForwardBlocks uint64 `koanf:"forward-blocks" reload:"hot"` + BatchCacheLimit uint32 `koanf:"batch-cache-limit"` CurrentModuleRoot string `koanf:"current-module-root"` // TODO(magic) requires reinitialization on hot reload PendingUpgradeModuleRoot string `koanf:"pending-upgrade-module-root"` // TODO(magic) requires StatelessBlockValidator recreation on hot reload FailureIsFatal bool `koanf:"failure-is-fatal" reload:"hot"` Dangerous BlockValidatorDangerousConfig `koanf:"dangerous"` MemoryFreeLimit string `koanf:"memory-free-limit" reload:"hot"` ValidationServerConfigsList string `koanf:"validation-server-configs-list"` + // The directory to which the BlockValidator will write the + // block_inputs_.json files when WriteToFile() is called. + BlockInputsFilePath string `koanf:"block-inputs-file-path"` memoryFreeLimit int } @@ -171,13 +183,16 @@ func BlockValidatorConfigAddOptions(prefix string, f *pflag.FlagSet) { redis.ValidationClientConfigAddOptions(prefix+".redis-validation-client-config", f) f.String(prefix+".validation-server-configs-list", DefaultBlockValidatorConfig.ValidationServerConfigsList, "array of execution rpc configs given as a json string. time duration should be supplied in number indicating nanoseconds") f.Duration(prefix+".validation-poll", DefaultBlockValidatorConfig.ValidationPoll, "poll time to check validations") - f.Uint64(prefix+".forward-blocks", DefaultBlockValidatorConfig.ForwardBlocks, "prepare entries for up to that many blocks ahead of validation (small footprint)") + f.Uint64(prefix+".forward-blocks", DefaultBlockValidatorConfig.ForwardBlocks, "prepare entries for up to that many blocks ahead of validation (stores batch-copy per block)") f.Uint64(prefix+".prerecorded-blocks", DefaultBlockValidatorConfig.PrerecordedBlocks, "record that many blocks ahead of validation (larger footprint)") + f.Uint32(prefix+".batch-cache-limit", DefaultBlockValidatorConfig.BatchCacheLimit, "limit number of old batches to keep in block-validator") f.String(prefix+".current-module-root", DefaultBlockValidatorConfig.CurrentModuleRoot, "current wasm module root ('current' read from chain, 'latest' from machines/latest dir, or provide hash)") + f.Uint64(prefix+".recording-iter-limit", DefaultBlockValidatorConfig.RecordingIterLimit, "limit on block recordings sent per iteration") f.String(prefix+".pending-upgrade-module-root", DefaultBlockValidatorConfig.PendingUpgradeModuleRoot, "pending upgrade wasm module root to additionally validate (hash, 'latest' or empty)") f.Bool(prefix+".failure-is-fatal", DefaultBlockValidatorConfig.FailureIsFatal, "failing a validation is treated as a fatal error") BlockValidatorDangerousConfigAddOptions(prefix+".dangerous", f) f.String(prefix+".memory-free-limit", DefaultBlockValidatorConfig.MemoryFreeLimit, "minimum free-memory limit after reaching which the blockvalidator pauses validation. Enabled by default as 1GB, to disable provide empty string") + f.String(prefix+".block-inputs-file-path", DefaultBlockValidatorConfig.BlockInputsFilePath, "directory to write block validation inputs files") } func BlockValidatorDangerousConfigAddOptions(prefix string, f *pflag.FlagSet) { @@ -190,13 +205,16 @@ var DefaultBlockValidatorConfig = BlockValidatorConfig{ ValidationServer: rpcclient.DefaultClientConfig, RedisValidationClientConfig: redis.DefaultValidationClientConfig, ValidationPoll: time.Second, - ForwardBlocks: 1024, + ForwardBlocks: 128, PrerecordedBlocks: uint64(2 * runtime.NumCPU()), + BatchCacheLimit: 20, CurrentModuleRoot: "current", PendingUpgradeModuleRoot: "latest", FailureIsFatal: true, Dangerous: DefaultBlockValidatorDangerousConfig, + BlockInputsFilePath: "./target/validation_inputs", MemoryFreeLimit: "default", + RecordingIterLimit: 20, } var TestBlockValidatorConfig = BlockValidatorConfig{ @@ -206,11 +224,14 @@ var TestBlockValidatorConfig = BlockValidatorConfig{ RedisValidationClientConfig: redis.TestValidationClientConfig, ValidationPoll: 100 * time.Millisecond, ForwardBlocks: 128, + BatchCacheLimit: 20, PrerecordedBlocks: uint64(2 * runtime.NumCPU()), + RecordingIterLimit: 20, CurrentModuleRoot: "latest", PendingUpgradeModuleRoot: "latest", FailureIsFatal: true, Dangerous: DefaultBlockValidatorDangerousConfig, + BlockInputsFilePath: "./target/validation_inputs", MemoryFreeLimit: "default", } @@ -267,7 +288,15 @@ func NewBlockValidator( progressValidationsChan: make(chan struct{}, 1), config: config, fatalErr: fatalErr, + prevBatchCache: make(map[uint64][]byte), } + valInputsWriter, err := inputs.NewWriter( + inputs.WithBaseDir(ret.stack.InstanceDir()), + inputs.WithSlug("BlockValidator")) + if err != nil { + return nil, err + } + ret.validationInputsWriter = valInputsWriter if !config().Dangerous.ResetBlockValidation { validated, err := ret.ReadLastValidatedInfo() if err != nil { @@ -315,6 +344,7 @@ func NewBlockValidator( func atomicStorePos(addr *atomic.Uint64, val arbutil.MessageIndex, metr metrics.Gauge) { addr.Store(uint64(val)) + // #nosec G115 metr.Update(int64(val)) } @@ -498,18 +528,16 @@ func (v *BlockValidator) sendRecord(s *validationStatus) error { } //nolint:gosec -func (v *BlockValidator) writeToFile(validationEntry *validationEntry, moduleRoot common.Hash) error { - input, err := validationEntry.ToInput([]rawdb.Target{rawdb.TargetWavm}) +func (v *BlockValidator) writeToFile(validationEntry *validationEntry) error { + input, err := validationEntry.ToInput([]ethdb.WasmTarget{rawdb.TargetWavm}) if err != nil { return err } - for _, spawner := range v.execSpawners { - if validator.SpawnerSupportsModule(spawner, moduleRoot) { - _, err = spawner.WriteToFile(input, validationEntry.End, moduleRoot).Await(v.GetContext()) - return err - } + inputJson := server_api.ValidationInputToJson(input) + if err := v.validationInputsWriter.Write(inputJson); err != nil { + return err } - return errors.New("did not find exec spawner for wasmModuleRoot") + return nil } func (v *BlockValidator) SetCurrentWasmModuleRoot(hash common.Hash) error { @@ -566,32 +594,63 @@ func (v *BlockValidator) createNextValidationEntry(ctx context.Context) (bool, e } if v.nextCreateStartGS.PosInBatch == 0 || v.nextCreateBatchReread { // new batch - found, batch, batchBlockHash, count, err := v.readBatch(ctx, v.nextCreateStartGS.Batch) + found, fullBatchInfo, err := v.readFullBatch(ctx, v.nextCreateStartGS.Batch) if !found { return false, err } - v.nextCreateBatch = batch - v.nextCreateBatchBlockHash = batchBlockHash - v.nextCreateBatchMsgCount = count - validatorMsgCountCurrentBatch.Update(int64(count)) + if v.nextCreateBatch != nil { + v.prevBatchCache[v.nextCreateBatch.Number] = v.nextCreateBatch.PostedData + } + v.nextCreateBatch = fullBatchInfo + // #nosec G115 + validatorMsgCountCurrentBatch.Update(int64(fullBatchInfo.MsgCount)) + batchCacheLimit := v.config().BatchCacheLimit + if len(v.prevBatchCache) > int(batchCacheLimit) { + for num := range v.prevBatchCache { + if num+uint64(batchCacheLimit) < v.nextCreateStartGS.Batch { + delete(v.prevBatchCache, num) + } + } + } v.nextCreateBatchReread = false } endGS := validator.GoGlobalState{ BlockHash: endRes.BlockHash, SendRoot: endRes.SendRoot, } - if pos+1 < v.nextCreateBatchMsgCount { + if pos+1 < v.nextCreateBatch.MsgCount { endGS.Batch = v.nextCreateStartGS.Batch endGS.PosInBatch = v.nextCreateStartGS.PosInBatch + 1 - } else if pos+1 == v.nextCreateBatchMsgCount { + } else if pos+1 == v.nextCreateBatch.MsgCount { endGS.Batch = v.nextCreateStartGS.Batch + 1 endGS.PosInBatch = 0 } else { - return false, fmt.Errorf("illegal batch msg count %d pos %d batch %d", v.nextCreateBatchMsgCount, pos, endGS.Batch) + return false, fmt.Errorf("illegal batch msg count %d pos %d batch %d", v.nextCreateBatch.MsgCount, pos, endGS.Batch) } chainConfig := v.streamer.ChainConfig() + prevBatchNums, err := msg.Message.PastBatchesRequired() + if err != nil { + return false, err + } + prevBatches := make([]validator.BatchInfo, 0, len(prevBatchNums)) + // prevBatchNums are only used for batch reports, each is only used once + for _, batchNum := range prevBatchNums { + data, found := v.prevBatchCache[batchNum] + if found { + delete(v.prevBatchCache, batchNum) + } else { + data, err = v.readPostedBatch(ctx, batchNum) + if err != nil { + return false, err + } + } + prevBatches = append(prevBatches, validator.BatchInfo{ + Number: batchNum, + Data: data, + }) + } entry, err := newValidationEntry( - pos, v.nextCreateStartGS, endGS, msg, v.nextCreateBatch, v.nextCreateBatchBlockHash, v.nextCreatePrevDelayed, chainConfig, + pos, v.nextCreateStartGS, endGS, msg, v.nextCreateBatch, prevBatches, v.nextCreatePrevDelayed, chainConfig, ) if err != nil { return false, err @@ -650,6 +709,10 @@ func (v *BlockValidator) sendNextRecordRequests(ctx context.Context) (bool, erro if recordUntil < pos { return false, nil } + recordUntilLimit := pos + arbutil.MessageIndex(v.config().RecordingIterLimit) + if recordUntil > recordUntilLimit { + recordUntil = recordUntilLimit + } log.Trace("preparing to record", "pos", pos, "until", recordUntil) // prepare could take a long time so we do it without a lock err := v.recorder.PrepareForRecord(ctx, pos, recordUntil) @@ -723,6 +786,7 @@ func (v *BlockValidator) iterativeValidationPrint(ctx context.Context) time.Dura if err != nil { printedCount = -1 } else { + // #nosec G115 printedCount = int64(batchMsgs) + int64(validated.GlobalState.PosInBatch) } log.Info("validated execution", "messageCount", printedCount, "globalstate", validated.GlobalState, "WasmRoots", validated.WasmRoots) @@ -777,7 +841,7 @@ validationsLoop: runEnd, err := run.Current() if err == nil && runEnd != validationStatus.Entry.End { err = fmt.Errorf("validation failed: expected %v got %v", validationStatus.Entry.End, runEnd) - writeErr := v.writeToFile(validationStatus.Entry, run.WasmModuleRoot()) + writeErr := v.writeToFile(validationStatus.Entry) if writeErr != nil { log.Warn("failed to write debug results file", "err", writeErr) } @@ -986,14 +1050,19 @@ func (v *BlockValidator) UpdateLatestStaked(count arbutil.MessageIndex, globalSt v.nextCreateStartGS = globalState v.nextCreatePrevDelayed = msg.DelayedMessagesRead v.nextCreateBatchReread = true + if v.nextCreateBatch != nil { + v.prevBatchCache[v.nextCreateBatch.Number] = v.nextCreateBatch.PostedData + } v.createdA.Store(countUint64) } // under the reorg mutex we don't need atomic access if v.recordSentA.Load() < countUint64 { v.recordSentA.Store(countUint64) } + // #nosec G115 v.validatedA.Store(countUint64) v.valLoopPos = count + // #nosec G115 validatorMsgCountValidatedGauge.Update(int64(countUint64)) err = v.writeLastValidated(globalState, nil) // we don't know which wasm roots were validated if err != nil { @@ -1010,6 +1079,7 @@ func (v *BlockValidator) ReorgToBatchCount(count uint64) { defer v.reorgMutex.Unlock() if v.nextCreateStartGS.Batch >= count { v.nextCreateBatchReread = true + v.prevBatchCache = make(map[uint64][]byte) } } @@ -1047,9 +1117,10 @@ func (v *BlockValidator) Reorg(ctx context.Context, count arbutil.MessageIndex) } v.validations.Delete(iPos) } - v.nextCreateStartGS = buildGlobalState(*res, endPosition) + v.nextCreateStartGS = BuildGlobalState(*res, endPosition) v.nextCreatePrevDelayed = msg.DelayedMessagesRead v.nextCreateBatchReread = true + v.prevBatchCache = make(map[uint64][]byte) countUint64 := uint64(count) v.createdA.Store(countUint64) // under the reorg mutex we don't need atomic access @@ -1058,6 +1129,7 @@ func (v *BlockValidator) Reorg(ctx context.Context, count arbutil.MessageIndex) } if v.validatedA.Load() > countUint64 { v.validatedA.Store(countUint64) + // #nosec G115 validatorMsgCountValidatedGauge.Update(int64(countUint64)) err := v.writeLastValidated(v.nextCreateStartGS, nil) // we don't know which wasm roots were validated if err != nil { @@ -1249,6 +1321,7 @@ func (v *BlockValidator) checkValidatedGSCaughtUp() (bool, error) { atomicStorePos(&v.createdA, count, validatorMsgCountCreatedGauge) atomicStorePos(&v.recordSentA, count, validatorMsgCountRecordSentGauge) atomicStorePos(&v.validatedA, count, validatorMsgCountValidatedGauge) + // #nosec G115 validatorMsgCountValidatedGauge.Update(int64(count)) v.chainCaughtUp = true return true, nil diff --git a/staker/block_validator_schema.go b/staker/block_validator_schema.go index f6eb39f015..330116dda0 100644 --- a/staker/block_validator_schema.go +++ b/staker/block_validator_schema.go @@ -5,6 +5,7 @@ package staker import ( "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator" ) diff --git a/staker/bold/bold_staker.go b/staker/bold/bold_staker.go new file mode 100644 index 0000000000..1a8eed80fa --- /dev/null +++ b/staker/bold/bold_staker.go @@ -0,0 +1,536 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/main/LICENSE +package bold + +import ( + "context" + "errors" + "fmt" + "math/big" + "time" + + flag "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" + + protocol "github.com/offchainlabs/bold/chain-abstraction" + solimpl "github.com/offchainlabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/offchainlabs/bold/challenge-manager" + boldtypes "github.com/offchainlabs/bold/challenge-manager/types" + l2stateprovider "github.com/offchainlabs/bold/layer2-state-provider" + "github.com/offchainlabs/bold/solgen/go/challengeV2gen" + boldrollup "github.com/offchainlabs/bold/solgen/go/rollupgen" + "github.com/offchainlabs/bold/util" + "github.com/offchainlabs/nitro/arbnode/dataposter" + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/staker" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" + "github.com/offchainlabs/nitro/util/headerreader" + "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/offchainlabs/nitro/validator" +) + +var assertionCreatedId common.Hash + +func init() { + rollupAbi, err := boldrollup.RollupCoreMetaData.GetAbi() + if err != nil { + panic(err) + } + assertionCreatedEvent, ok := rollupAbi.Events["AssertionCreated"] + if !ok { + panic("RollupCore ABI missing AssertionCreated event") + } + assertionCreatedId = assertionCreatedEvent.ID +} + +type BoldConfig struct { + Enable bool `koanf:"enable"` + Strategy string `koanf:"strategy"` + // How often to post assertions onchain. + AssertionPostingInterval time.Duration `koanf:"assertion-posting-interval"` + // How often to scan for newly created assertions onchain. + AssertionScanningInterval time.Duration `koanf:"assertion-scanning-interval"` + // How often to confirm assertions onchain. + AssertionConfirmingInterval time.Duration `koanf:"assertion-confirming-interval"` + API bool `koanf:"api"` + APIHost string `koanf:"api-host"` + APIPort uint16 `koanf:"api-port"` + APIDBPath string `koanf:"api-db-path"` + TrackChallengeParentAssertionHashes []string `koanf:"track-challenge-parent-assertion-hashes"` + CheckStakerSwitchInterval time.Duration `koanf:"check-staker-switch-interval"` + StateProviderConfig StateProviderConfig `koanf:"state-provider-config"` + StartValidationFromStaked bool `koanf:"start-validation-from-staked"` + strategy legacystaker.StakerStrategy +} + +func (c *BoldConfig) Validate() error { + strategy, err := legacystaker.ParseStrategy(c.Strategy) + if err != nil { + return err + } + c.strategy = strategy + return nil +} + +type StateProviderConfig struct { + // A name identifier for the validator for cosmetic purposes. + ValidatorName string `koanf:"validator-name"` + CheckBatchFinality bool `koanf:"check-batch-finality"` + // Path to a filesystem directory that will cache machine hashes for BOLD. + MachineLeavesCachePath string `koanf:"machine-leaves-cache-path"` +} + +var DefaultStateProviderConfig = StateProviderConfig{ + ValidatorName: "default-validator", + CheckBatchFinality: true, + MachineLeavesCachePath: "machine-hashes-cache", +} + +var DefaultBoldConfig = BoldConfig{ + Enable: false, + Strategy: "Watchtower", + AssertionPostingInterval: time.Minute * 15, + AssertionScanningInterval: time.Minute, + AssertionConfirmingInterval: time.Minute, + API: false, + APIHost: "127.0.0.1", + APIPort: 9393, + APIDBPath: "bold-api-db", + TrackChallengeParentAssertionHashes: []string{}, + CheckStakerSwitchInterval: time.Minute, // Every minute, check if the Nitro node staker should switch to using BOLD. + StateProviderConfig: DefaultStateProviderConfig, + StartValidationFromStaked: true, +} + +var BoldModes = map[legacystaker.StakerStrategy]boldtypes.Mode{ + legacystaker.WatchtowerStrategy: boldtypes.WatchTowerMode, + legacystaker.DefensiveStrategy: boldtypes.DefensiveMode, + legacystaker.ResolveNodesStrategy: boldtypes.ResolveMode, + legacystaker.MakeNodesStrategy: boldtypes.MakeMode, +} + +func BoldConfigAddOptions(prefix string, f *flag.FlagSet) { + f.Bool(prefix+".enable", DefaultBoldConfig.Enable, "enable bold challenge protocol") + f.String(prefix+".strategy", DefaultBoldConfig.Strategy, "define the bold validator staker strategy, either watchtower, defensive, stakeLatest, or makeNodes") + f.Duration(prefix+".assertion-posting-interval", DefaultBoldConfig.AssertionPostingInterval, "assertion posting interval") + f.Duration(prefix+".assertion-scanning-interval", DefaultBoldConfig.AssertionScanningInterval, "scan assertion interval") + f.Duration(prefix+".assertion-confirming-interval", DefaultBoldConfig.AssertionConfirmingInterval, "confirm assertion interval") + f.Duration(prefix+".check-staker-switch-interval", DefaultBoldConfig.CheckStakerSwitchInterval, "how often to check if staker can switch to bold") + f.Bool(prefix+".api", DefaultBoldConfig.API, "enable api") + f.String(prefix+".api-host", DefaultBoldConfig.APIHost, "bold api host") + f.Uint16(prefix+".api-port", DefaultBoldConfig.APIPort, "bold api port") + f.String(prefix+".api-db-path", DefaultBoldConfig.APIDBPath, "bold api db path") + f.StringSlice(prefix+".track-challenge-parent-assertion-hashes", DefaultBoldConfig.TrackChallengeParentAssertionHashes, "only track challenges/edges with these parent assertion hashes") + StateProviderConfigAddOptions(prefix+".state-provider-config", f) + f.Bool(prefix+".start-validation-from-staked", DefaultBoldConfig.StartValidationFromStaked, "assume staked nodes are valid") +} + +func StateProviderConfigAddOptions(prefix string, f *flag.FlagSet) { + f.String(prefix+".validator-name", DefaultStateProviderConfig.ValidatorName, "name identifier for cosmetic purposes") + f.Bool(prefix+".check-batch-finality", DefaultStateProviderConfig.CheckBatchFinality, "check batch finality") + f.String(prefix+".machine-leaves-cache-path", DefaultStateProviderConfig.MachineLeavesCachePath, "path to machine cache") +} + +type BOLDStaker struct { + stopwaiter.StopWaiter + config *BoldConfig + chalManager *challengemanager.Manager + blockValidator *staker.BlockValidator + statelessBlockValidator *staker.StatelessBlockValidator + rollupAddress common.Address + l1Reader *headerreader.HeaderReader + client protocol.ChainBackend + callOpts bind.CallOpts + wallet legacystaker.ValidatorWalletInterface + stakedNotifiers []legacystaker.LatestStakedNotifier + confirmedNotifiers []legacystaker.LatestConfirmedNotifier +} + +func NewBOLDStaker( + ctx context.Context, + stack *node.Node, + rollupAddress common.Address, + callOpts bind.CallOpts, + txOpts *bind.TransactOpts, + l1Reader *headerreader.HeaderReader, + blockValidator *staker.BlockValidator, + statelessBlockValidator *staker.StatelessBlockValidator, + config *BoldConfig, + dataPoster *dataposter.DataPoster, + wallet legacystaker.ValidatorWalletInterface, + stakedNotifiers []legacystaker.LatestStakedNotifier, + confirmedNotifiers []legacystaker.LatestConfirmedNotifier, +) (*BOLDStaker, error) { + if err := config.Validate(); err != nil { + return nil, err + } + wrappedClient := util.NewBackendWrapper(l1Reader.Client(), rpc.LatestBlockNumber) + manager, err := newBOLDChallengeManager(ctx, stack, rollupAddress, txOpts, l1Reader, wrappedClient, blockValidator, statelessBlockValidator, config, dataPoster) + if err != nil { + return nil, err + } + return &BOLDStaker{ + config: config, + chalManager: manager, + blockValidator: blockValidator, + statelessBlockValidator: statelessBlockValidator, + rollupAddress: rollupAddress, + l1Reader: l1Reader, + client: wrappedClient, + callOpts: callOpts, + wallet: wallet, + stakedNotifiers: stakedNotifiers, + confirmedNotifiers: confirmedNotifiers, + }, nil +} + +// Initialize Updates the block validator module root. +// And updates the init state of the block validator if block validator has not started yet. +func (b *BOLDStaker) Initialize(ctx context.Context) error { + err := b.updateBlockValidatorModuleRoot(ctx) + if err != nil { + log.Warn("error updating latest wasm module root", "err", err) + } + walletAddressOrZero := b.wallet.AddressOrZero() + var stakerAddr common.Address + if b.wallet.DataPoster() != nil { + stakerAddr = b.wallet.DataPoster().Sender() + } + log.Info("running as validator", "txSender", stakerAddr, "actingAsWallet", walletAddressOrZero, "strategy", b.config.Strategy) + + if b.blockValidator != nil && b.config.StartValidationFromStaked && !b.blockValidator.Started() { + rollupUserLogic, err := boldrollup.NewRollupUserLogic(b.rollupAddress, b.client) + if err != nil { + return err + } + latestStaked, err := rollupUserLogic.LatestStakedAssertion(b.getCallOpts(ctx), walletAddressOrZero) + if err != nil { + return err + } + if latestStaked == [32]byte{} { + latestConfirmed, err := rollupUserLogic.LatestConfirmed(&bind.CallOpts{Context: ctx}) + if err != nil { + return err + } + latestStaked = latestConfirmed + } + assertion, err := readBoldAssertionCreationInfo( + ctx, + rollupUserLogic, + b.client, + b.rollupAddress, + latestStaked, + ) + if err != nil { + return err + } + afterState := protocol.GoGlobalStateFromSolidity(assertion.AfterState.GlobalState) + return b.blockValidator.InitAssumeValid(validator.GoGlobalState(afterState)) + } + return nil +} + +func (b *BOLDStaker) Start(ctxIn context.Context) { + b.StopWaiter.Start(ctxIn, b) + b.chalManager.Start(ctxIn) + b.CallIteratively(func(ctx context.Context) time.Duration { + err := b.updateBlockValidatorModuleRoot(ctx) + if err != nil { + log.Warn("error updating latest wasm module root", "err", err) + } + agreedMsgCount, agreedGlobalState, err := b.getLatestState(ctx, false) + if err != nil { + log.Error("staker: error checking latest agreed", "err", err) + } + + if agreedGlobalState != nil { + for _, notifier := range b.stakedNotifiers { + notifier.UpdateLatestStaked(agreedMsgCount, *agreedGlobalState) + } + } + confirmedMsgCount, confirmedGlobalState, err := b.getLatestState(ctx, true) + if err != nil { + log.Error("staker: error checking latest confirmed", "err", err) + } + + if confirmedGlobalState != nil { + for _, notifier := range b.confirmedNotifiers { + notifier.UpdateLatestConfirmed(confirmedMsgCount, *confirmedGlobalState) + } + } + return b.config.AssertionPostingInterval + }) +} + +func (b *BOLDStaker) getLatestState(ctx context.Context, confirmed bool) (arbutil.MessageIndex, *validator.GoGlobalState, error) { + var globalState protocol.GoGlobalState + var err error + if confirmed { + globalState, err = b.chalManager.LatestConfirmedState(ctx) + } else { + globalState, err = b.chalManager.LatestAgreedState(ctx) + } + var assertionType string + if confirmed { + assertionType = "confirmed" + } else { + assertionType = "agreed" + } + if err != nil { + return 0, nil, fmt.Errorf("error getting latest %s: %w", assertionType, err) + } + caughtUp, count, err := staker.GlobalStateToMsgCount(b.statelessBlockValidator.InboxTracker(), b.statelessBlockValidator.InboxStreamer(), validator.GoGlobalState(globalState)) + if err != nil { + if errors.Is(err, staker.ErrGlobalStateNotInChain) { + return 0, nil, fmt.Errorf("latest %s assertion of %v not yet in our node: %w", assertionType, globalState, err) + } + return 0, nil, fmt.Errorf("error getting message count: %w", err) + } + + if !caughtUp { + log.Info(fmt.Sprintf("latest %s assertion not yet in our node", assertionType), "state", globalState) + return 0, nil, nil + } + + processedCount, err := b.statelessBlockValidator.InboxStreamer().GetProcessedMessageCount() + if err != nil { + return 0, nil, err + } + + if processedCount < count { + log.Info("execution catching up to rollup", "rollupCount", count, "processedCount", processedCount) + return 0, nil, nil + } + + return count, (*validator.GoGlobalState)(&globalState), nil +} + +func (b *BOLDStaker) StopAndWait() { + b.chalManager.StopAndWait() + b.StopWaiter.StopAndWait() +} + +func (b *BOLDStaker) updateBlockValidatorModuleRoot(ctx context.Context) error { + if b.blockValidator == nil { + return nil + } + boldRollup, err := boldrollup.NewRollupUserLogic(b.rollupAddress, b.client) + if err != nil { + return err + } + moduleRoot, err := boldRollup.WasmModuleRoot(b.getCallOpts(ctx)) + if err != nil { + return err + } + return b.blockValidator.SetCurrentWasmModuleRoot(moduleRoot) +} + +func (b *BOLDStaker) getCallOpts(ctx context.Context) *bind.CallOpts { + opts := b.callOpts + opts.Context = ctx + return &opts +} + +// Sets up a BOLD challenge manager implementation by providing it with +// its necessary dependencies and configuration. The challenge manager can then be started, as it +// implements the StopWaiter pattern as part of the Nitro validator. +func newBOLDChallengeManager( + ctx context.Context, + stack *node.Node, + rollupAddress common.Address, + txOpts *bind.TransactOpts, + l1Reader *headerreader.HeaderReader, + client protocol.ChainBackend, + blockValidator *staker.BlockValidator, + statelessBlockValidator *staker.StatelessBlockValidator, + config *BoldConfig, + dataPoster *dataposter.DataPoster, +) (*challengemanager.Manager, error) { + // Initializes the BOLD contract bindings and the assertion chain abstraction. + rollupBindings, err := boldrollup.NewRollupUserLogic(rollupAddress, client) + if err != nil { + return nil, fmt.Errorf("could not create rollup bindings: %w", err) + } + chalManager, err := rollupBindings.ChallengeManager(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("could not get challenge manager: %w", err) + } + chalManagerBindings, err := challengeV2gen.NewEdgeChallengeManager(chalManager, client) + if err != nil { + return nil, fmt.Errorf("could not create challenge manager bindings: %w", err) + } + assertionChain, err := solimpl.NewAssertionChain(ctx, rollupAddress, chalManager, txOpts, client, NewDataPosterTransactor(dataPoster)) + if err != nil { + return nil, fmt.Errorf("could not create assertion chain: %w", err) + } + + blockChallengeHeightBig, err := chalManagerBindings.LAYERZEROBLOCKEDGEHEIGHT(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("could not get block challenge height: %w", err) + } + if !blockChallengeHeightBig.IsUint64() { + return nil, errors.New("block challenge height was not a uint64") + } + bigStepHeightBig, err := chalManagerBindings.LAYERZEROBIGSTEPEDGEHEIGHT(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("could not get big step challenge height: %w", err) + } + if !bigStepHeightBig.IsUint64() { + return nil, errors.New("big step challenge height was not a uint64") + } + smallStepHeightBig, err := chalManagerBindings.LAYERZEROSMALLSTEPEDGEHEIGHT(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("could not get small step challenge height: %w", err) + } + if !smallStepHeightBig.IsUint64() { + return nil, errors.New("small step challenge height was not a uint64") + } + numBigSteps, err := chalManagerBindings.NUMBIGSTEPLEVEL(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("could not get number of big steps: %w", err) + } + blockChallengeLeafHeight := l2stateprovider.Height(blockChallengeHeightBig.Uint64()) + bigStepHeight := l2stateprovider.Height(bigStepHeightBig.Uint64()) + smallStepHeight := l2stateprovider.Height(smallStepHeightBig.Uint64()) + + apiDBPath := config.APIDBPath + if apiDBPath != "" { + apiDBPath = stack.ResolvePath(apiDBPath) + } + machineHashesPath := config.StateProviderConfig.MachineLeavesCachePath + if machineHashesPath != "" { + machineHashesPath = stack.ResolvePath(machineHashesPath) + } + + // Sets up the state provider interface that BOLD will use to request data such as + // execution states for assertions, history commitments for machine execution, and one step proofs. + stateProvider, err := NewBOLDStateProvider( + blockValidator, + statelessBlockValidator, + // Specify the height constants needed for the state provider. + // TODO: Fetch these from the smart contract instead. + blockChallengeLeafHeight, + &config.StateProviderConfig, + machineHashesPath, + ) + if err != nil { + return nil, fmt.Errorf("could not create state manager: %w", err) + } + providerHeights := []l2stateprovider.Height{blockChallengeLeafHeight} + for i := uint8(0); i < numBigSteps; i++ { + providerHeights = append(providerHeights, bigStepHeight) + } + providerHeights = append(providerHeights, smallStepHeight) + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateProvider, + stateProvider, + stateProvider, + providerHeights, + stateProvider, + nil, // Nil API database for the history commitment provider, as it will be provided later. TODO: Improve this dependency injection. + ) + // The interval at which the challenge manager will attempt to post assertions. + postingInterval := config.AssertionPostingInterval + // The interval at which the manager will scan for newly created assertions onchain. + scanningInterval := config.AssertionScanningInterval + // The interval at which the manager will attempt to confirm assertions. + confirmingInterval := config.AssertionConfirmingInterval + + stackOpts := []challengemanager.StackOpt{ + challengemanager.StackWithName(config.StateProviderConfig.ValidatorName), + challengemanager.StackWithMode(BoldModes[config.strategy]), + challengemanager.StackWithPollingInterval(scanningInterval), + challengemanager.StackWithPostingInterval(postingInterval), + challengemanager.StackWithConfirmationInterval(confirmingInterval), + challengemanager.StackWithTrackChallengeParentAssertionHashes(config.TrackChallengeParentAssertionHashes), + challengemanager.StackWithHeaderProvider(l1Reader), + } + if config.API { + apiAddr := fmt.Sprintf("%s:%d", config.APIHost, config.APIPort) + stackOpts = append(stackOpts, challengemanager.StackWithAPIEnabled(apiAddr, apiDBPath)) + } + + manager, err := challengemanager.NewChallengeStack( + assertionChain, + provider, + stackOpts..., + ) + if err != nil { + return nil, fmt.Errorf("could not create challenge manager: %w", err) + } + return manager, nil +} + +// Read the creation info for an assertion by looking up its creation +// event from the rollup contracts. +func readBoldAssertionCreationInfo( + ctx context.Context, + rollup *boldrollup.RollupUserLogic, + client bind.ContractFilterer, + rollupAddress common.Address, + assertionHash common.Hash, +) (*protocol.AssertionCreatedInfo, error) { + var creationBlock uint64 + var topics [][]common.Hash + if assertionHash == (common.Hash{}) { + rollupDeploymentBlock, err := rollup.RollupDeploymentBlock(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, err + } + if !rollupDeploymentBlock.IsUint64() { + return nil, errors.New("rollup deployment block was not a uint64") + } + creationBlock = rollupDeploymentBlock.Uint64() + } else { + var b [32]byte + copy(b[:], assertionHash[:]) + node, err := rollup.GetAssertion(&bind.CallOpts{Context: ctx}, b) + if err != nil { + return nil, err + } + creationBlock = node.CreatedAtBlock + } + topics = [][]common.Hash{{assertionCreatedId}, {assertionHash}} + var query = ethereum.FilterQuery{ + FromBlock: new(big.Int).SetUint64(creationBlock), + ToBlock: new(big.Int).SetUint64(creationBlock), + Addresses: []common.Address{rollupAddress}, + Topics: topics, + } + logs, err := client.FilterLogs(ctx, query) + if err != nil { + return nil, err + } + if len(logs) == 0 { + return nil, errors.New("no assertion creation logs found") + } + if len(logs) > 1 { + return nil, errors.New("found multiple instances of requested node") + } + ethLog := logs[0] + parsedLog, err := rollup.ParseAssertionCreated(ethLog) + if err != nil { + return nil, err + } + afterState := parsedLog.Assertion.AfterState + return &protocol.AssertionCreatedInfo{ + ConfirmPeriodBlocks: parsedLog.ConfirmPeriodBlocks, + RequiredStake: parsedLog.RequiredStake, + ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, + BeforeState: parsedLog.Assertion.BeforeState, + AfterState: afterState, + InboxMaxCount: parsedLog.InboxMaxCount, + AfterInboxBatchAcc: parsedLog.AfterInboxBatchAcc, + AssertionHash: protocol.AssertionHash{Hash: parsedLog.AssertionHash}, + WasmModuleRoot: parsedLog.WasmModuleRoot, + ChallengeManager: parsedLog.ChallengeManager, + TransactionHash: ethLog.TxHash, + CreationBlock: ethLog.BlockNumber, + }, nil +} diff --git a/staker/bold/bold_state_provider.go b/staker/bold/bold_state_provider.go new file mode 100644 index 0000000000..48b7cbd91e --- /dev/null +++ b/staker/bold/bold_state_provider.go @@ -0,0 +1,542 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see +// https://github.com/offchainlabs/bold/blob/main/LICENSE +package bold + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + + protocol "github.com/offchainlabs/bold/chain-abstraction" + "github.com/offchainlabs/bold/containers/option" + l2stateprovider "github.com/offchainlabs/bold/layer2-state-provider" + "github.com/offchainlabs/bold/state-commitments/history" + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/staker" + challengecache "github.com/offchainlabs/nitro/staker/challenge-cache" + "github.com/offchainlabs/nitro/validator" + "github.com/offchainlabs/nitro/validator/server_arb" +) + +var ( + _ l2stateprovider.ProofCollector = (*BOLDStateProvider)(nil) + _ l2stateprovider.L2MessageStateCollector = (*BOLDStateProvider)(nil) + _ l2stateprovider.MachineHashCollector = (*BOLDStateProvider)(nil) + _ l2stateprovider.ExecutionProvider = (*BOLDStateProvider)(nil) +) + +var executionNodeOfflineGauge = metrics.NewRegisteredGauge("arb/state_provider/execution_node_offline", nil) + +var ( + ErrChainCatchingUp = errors.New("chain catching up") +) + +type BOLDStateProvider struct { + validator *staker.BlockValidator + statelessValidator *staker.StatelessBlockValidator + historyCache challengecache.HistoryCommitmentCacher + blockChallengeLeafHeight l2stateprovider.Height + stateProviderConfig *StateProviderConfig + sync.RWMutex +} + +func NewBOLDStateProvider( + blockValidator *staker.BlockValidator, + statelessValidator *staker.StatelessBlockValidator, + blockChallengeLeafHeight l2stateprovider.Height, + stateProviderConfig *StateProviderConfig, + machineHashesCachePath string, +) (*BOLDStateProvider, error) { + historyCache, err := challengecache.New(machineHashesCachePath) + if err != nil { + return nil, err + } + sp := &BOLDStateProvider{ + validator: blockValidator, + statelessValidator: statelessValidator, + historyCache: historyCache, + blockChallengeLeafHeight: blockChallengeLeafHeight, + stateProviderConfig: stateProviderConfig, + } + return sp, nil +} + +// ExecutionStateAfterPreviousState Produces the L2 execution state for the next +// assertion. Returns the state at maxSeqInboxCount or blockChallengeLeafHeight +// after the previous state, whichever is earlier. If previousGlobalState is +// nil, defaults to returning the state at maxSeqInboxCount. +func (s *BOLDStateProvider) ExecutionStateAfterPreviousState( + ctx context.Context, + maxSeqInboxCount uint64, + previousGlobalState protocol.GoGlobalState, +) (*protocol.ExecutionState, error) { + if maxSeqInboxCount == 0 { + return nil, errors.New("max inbox count cannot be zero") + } + batchIndex := maxSeqInboxCount + maxNumberOfBlocks := uint64(s.blockChallengeLeafHeight) + messageCount, err := s.statelessValidator.InboxTracker().GetBatchMessageCount(batchIndex - 1) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, fmt.Errorf("%w: batch count %d", l2stateprovider.ErrChainCatchingUp, maxSeqInboxCount) + } + return nil, err + } + var previousMessageCount arbutil.MessageIndex + if previousGlobalState.Batch > 0 { + previousMessageCount, err = s.statelessValidator.InboxTracker().GetBatchMessageCount(previousGlobalState.Batch - 1) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, fmt.Errorf("%w: batch count %d", l2stateprovider.ErrChainCatchingUp, maxSeqInboxCount) + } + return nil, err + } + } + previousMessageCount += arbutil.MessageIndex(previousGlobalState.PosInBatch) + messageDiffBetweenBatches := messageCount - previousMessageCount + maxMessageCount := previousMessageCount + arbutil.MessageIndex(maxNumberOfBlocks) + if messageDiffBetweenBatches > maxMessageCount { + messageCount = maxMessageCount + batchIndex, _, err = s.statelessValidator.InboxTracker().FindInboxBatchContainingMessage(messageCount) + if err != nil { + return nil, err + } + } + globalState, err := s.findGlobalStateFromMessageCountAndBatch(messageCount, l2stateprovider.Batch(batchIndex)) + if err != nil { + return nil, err + } + // If the state we are requested to produce is neither validated nor past + // threshold, we return ErrChainCatchingUp as an error. + stateValidatedAndMessageCountPastThreshold, err := s.isStateValidatedAndMessageCountPastThreshold(ctx, globalState, messageCount) + if err != nil { + return nil, err + } + if !stateValidatedAndMessageCountPastThreshold { + return nil, fmt.Errorf("%w: batch count %d", l2stateprovider.ErrChainCatchingUp, maxSeqInboxCount) + } + + executionState := &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState(globalState), + MachineStatus: protocol.MachineStatusFinished, + } + toBatch := executionState.GlobalState.Batch + historyCommitStates, _, err := s.StatesInBatchRange( + ctx, + previousGlobalState, + toBatch, + l2stateprovider.Height(maxNumberOfBlocks), + ) + if err != nil { + return nil, err + } + historyCommit, err := history.NewCommitment(historyCommitStates, maxNumberOfBlocks+1) + if err != nil { + return nil, err + } + executionState.EndHistoryRoot = historyCommit.Merkle + return executionState, nil +} + +func (s *BOLDStateProvider) isStateValidatedAndMessageCountPastThreshold( + ctx context.Context, gs validator.GoGlobalState, messageCount arbutil.MessageIndex, +) (bool, error) { + if s.stateProviderConfig.CheckBatchFinality { + finalizedMessageCount, err := s.statelessValidator.InboxReader().GetFinalizedMsgCount(ctx) + if err != nil { + return false, err + } + if messageCount > finalizedMessageCount { + return false, nil + } + } + if s.validator == nil { + // If we do not have a validator, we cannot check if the state is validated. + // So we assume it is validated and return true. + return true, nil + } + lastValidatedGs, err := s.validator.ReadLastValidatedInfo() + if err != nil { + return false, err + } + if lastValidatedGs == nil { + return false, ErrChainCatchingUp + } + stateValidated := gs.Batch < lastValidatedGs.GlobalState.Batch || (gs.Batch == lastValidatedGs.GlobalState.Batch && gs.PosInBatch <= lastValidatedGs.GlobalState.PosInBatch) + return stateValidated, nil +} + +func (s *BOLDStateProvider) StatesInBatchRange( + ctx context.Context, + fromState protocol.GoGlobalState, + batchLimit uint64, + toHeight l2stateprovider.Height, +) ([]common.Hash, []validator.GoGlobalState, error) { + // Check the integrity of the arguments. + if batchLimit < fromState.Batch || (batchLimit == fromState.Batch && fromState.PosInBatch > 0) { + return nil, nil, fmt.Errorf("batch limit %v cannot be less than from batch %v", batchLimit, fromState.Batch) + } + // Compute the total desired hashes from this request. + totalDesiredHashes := uint64(toHeight + 1) + machineHashes := make([]common.Hash, 0) + states := make([]validator.GoGlobalState, 0) + + var prevBatchMsgCount arbutil.MessageIndex + var err error + if fromState.Batch > 0 { + prevBatchMsgCount, err = s.statelessValidator.InboxTracker().GetBatchMessageCount(uint64(fromState.Batch) - 1) + if err != nil { + return nil, nil, err + } + } + + batchNum := fromState.Batch + currBatchMsgCount, err := s.statelessValidator.InboxTracker().GetBatchMessageCount(batchNum) + if err != nil { + return nil, nil, err + } + posInBatch := fromState.PosInBatch + initialPos := prevBatchMsgCount + arbutil.MessageIndex(posInBatch) + if initialPos >= currBatchMsgCount { + return nil, nil, fmt.Errorf("initial position %v is past end of from batch %v message count %v", initialPos, batchNum, currBatchMsgCount) + } + for pos := initialPos; uint64(len(states)) < totalDesiredHashes; pos++ { + if ctx.Err() != nil { + return nil, nil, ctx.Err() + } + executionResult, err := s.statelessValidator.InboxStreamer().ResultAtCount(arbutil.MessageIndex(pos)) + if err != nil { + return nil, nil, err + } + state := validator.GoGlobalState{ + BlockHash: executionResult.BlockHash, + SendRoot: executionResult.SendRoot, + Batch: batchNum, + PosInBatch: posInBatch, + } + states = append(states, state) + machineHashes = append(machineHashes, machineHash(state)) + if batchNum >= batchLimit { + break + } + // Check if the next message is in the next batch. + if uint64(pos+1) == uint64(currBatchMsgCount) { + posInBatch = 0 + batchNum++ + // Only get the next batch metadata if it'll be needed. + // Otherwise, we might try to read too many batches, and hit an error that + // the next batch isn't found. + if uint64(len(states)) < totalDesiredHashes && batchNum < batchLimit { + currBatchMsgCount, err = s.statelessValidator.InboxTracker().GetBatchMessageCount(batchNum) + if err != nil { + return nil, nil, err + } + } + } else { + posInBatch++ + } + } + return machineHashes, states, nil +} + +func machineHash(gs validator.GoGlobalState) common.Hash { + return crypto.Keccak256Hash([]byte("Machine finished:"), gs.Hash().Bytes()) +} + +func (s *BOLDStateProvider) findGlobalStateFromMessageCountAndBatch(count arbutil.MessageIndex, batchIndex l2stateprovider.Batch) (validator.GoGlobalState, error) { + var prevBatchMsgCount arbutil.MessageIndex + var err error + if batchIndex > 0 { + prevBatchMsgCount, err = s.statelessValidator.InboxTracker().GetBatchMessageCount(uint64(batchIndex) - 1) + if err != nil { + return validator.GoGlobalState{}, err + } + if prevBatchMsgCount > count { + return validator.GoGlobalState{}, fmt.Errorf("bad batch %v provided for message count %v as previous batch ended at message count %v", batchIndex, count, prevBatchMsgCount) + } + } + if count != prevBatchMsgCount { + batchMsgCount, err := s.statelessValidator.InboxTracker().GetBatchMessageCount(uint64(batchIndex)) + if err != nil { + return validator.GoGlobalState{}, err + } + if count > batchMsgCount { + return validator.GoGlobalState{}, fmt.Errorf("message count %v is past end of batch %v message count %v", count, batchIndex, batchMsgCount) + } + } + res, err := s.statelessValidator.InboxStreamer().ResultAtCount(count) + if err != nil { + return validator.GoGlobalState{}, fmt.Errorf("%s: could not check if we have result at count %d: %w", s.stateProviderConfig.ValidatorName, count, err) + } + return validator.GoGlobalState{ + BlockHash: res.BlockHash, + SendRoot: res.SendRoot, + Batch: uint64(batchIndex), + PosInBatch: uint64(count - prevBatchMsgCount), + }, nil +} + +// L2MessageStatesUpTo Computes a block history commitment from a start L2 +// message to an end L2 message index and up to a required batch index. The +// hashes used for this commitment are the machine hashes at each message +// number. +func (s *BOLDStateProvider) L2MessageStatesUpTo( + ctx context.Context, + fromState protocol.GoGlobalState, + batchLimit l2stateprovider.Batch, + toHeight option.Option[l2stateprovider.Height], +) ([]common.Hash, error) { + var to l2stateprovider.Height + if !toHeight.IsNone() { + to = toHeight.Unwrap() + } else { + to = s.blockChallengeLeafHeight + } + items, _, err := s.StatesInBatchRange(ctx, fromState, uint64(batchLimit), to) + if err != nil { + return nil, err + } + return items, nil +} + +// CollectMachineHashes Collects a list of machine hashes at a message number +// based on some configuration parameters. +func (s *BOLDStateProvider) CollectMachineHashes( + ctx context.Context, cfg *l2stateprovider.HashCollectorConfig, +) ([]common.Hash, error) { + s.Lock() + defer s.Unlock() + batchLimit := cfg.AssertionMetadata.BatchLimit + messageNum, err := s.messageNum(cfg.AssertionMetadata, cfg.BlockChallengeHeight) + if err != nil { + return nil, err + } + // Check if we have a virtual global state. + vs, err := s.virtualState(messageNum, batchLimit) + if err != nil { + return nil, err + } + if vs.IsSome() { + m := server_arb.NewFinishedMachine(vs.Unwrap()) + defer m.Destroy() + return []common.Hash{m.Hash()}, nil + } + stepHeights := make([]uint64, len(cfg.StepHeights)) + for i, h := range cfg.StepHeights { + stepHeights[i] = uint64(h) + } + messageResult, err := s.statelessValidator.InboxStreamer().ResultAtCount(arbutil.MessageIndex(messageNum + 1)) + if err != nil { + return nil, err + } + cacheKey := &challengecache.Key{ + RollupBlockHash: messageResult.BlockHash, + WavmModuleRoot: cfg.AssertionMetadata.WasmModuleRoot, + MessageHeight: uint64(messageNum), + StepHeights: stepHeights, + } + if s.historyCache != nil { + cachedRoots, err := s.historyCache.Get(cacheKey, cfg.NumDesiredHashes) + switch { + case err == nil: + log.Info( + "In collect machine hashes", + "cfg", fmt.Sprintf("%+v", cfg), + "firstHash", fmt.Sprintf("%#x", cachedRoots[0]), + "lastHash", fmt.Sprintf("%#x", cachedRoots[len(cachedRoots)-1]), + ) + return cachedRoots, nil + case !errors.Is(err, challengecache.ErrNotFoundInCache): + return nil, err + } + } + entry, err := s.statelessValidator.CreateReadyValidationEntry(ctx, messageNum) + if err != nil { + return nil, err + } + input, err := entry.ToInput([]ethdb.WasmTarget{rawdb.TargetWavm}) + if err != nil { + return nil, err + } + // TODO: Enable Redis streams. + wasmModRoot := cfg.AssertionMetadata.WasmModuleRoot + execRun, err := s.statelessValidator.ExecutionSpawners()[0].CreateExecutionRun(wasmModRoot, input, true).Await(ctx) + if err != nil { + return nil, err + } + defer execRun.Close() + ctxCheckAlive, cancelCheckAlive := ctxWithCheckAlive(ctx, execRun) + defer cancelCheckAlive() + stepLeaves := execRun.GetMachineHashesWithStepSize(uint64(cfg.MachineStartIndex), uint64(cfg.StepSize), cfg.NumDesiredHashes) + result, err := stepLeaves.Await(ctxCheckAlive) + if err != nil { + return nil, err + } + log.Info(fmt.Sprintf("Finished gathering machine hashes for request %+v", cfg)) + // Do not save a history commitment of length 1 to the cache. + if len(result) > 1 && s.historyCache != nil { + if err := s.historyCache.Put(cacheKey, result); err != nil { + if !errors.Is(err, challengecache.ErrFileAlreadyExists) { + return nil, err + } + } + } + return result, nil +} + +// messageNum returns the message number at which the BoLD protocol should +// process machine hashes based on the AssociatedAssertionMetadata and +// chalHeight. +func (s *BOLDStateProvider) messageNum(md *l2stateprovider.AssociatedAssertionMetadata, chalHeight l2stateprovider.Height) (arbutil.MessageIndex, error) { + var prevBatchMsgCount arbutil.MessageIndex + bNum := md.FromState.Batch + posInBatch := md.FromState.PosInBatch + if bNum > 0 { + var err error + prevBatchMsgCount, err = s.statelessValidator.InboxTracker().GetBatchMessageCount(uint64(bNum - 1)) + if err != nil { + return 0, fmt.Errorf("could not get prevBatchMsgCount at %d: %w", bNum-1, err) + } + } + return prevBatchMsgCount + arbutil.MessageIndex(posInBatch) + arbutil.MessageIndex(chalHeight), nil +} + +// virtualState returns an optional global state. +// +// If messageNum is a virtual block or the last real block to which this +// validator's assertion committed, then this function retuns a global state +// representing that virtual block's finished machine. Otherwise, it returns +// an Option.None. +// +// This can happen in the BoLD protocol when the rival block-level challenge +// edge has committed to more blocks that this validator expected for the +// current batch. In that case, the chalHeight will be a block in the virtual +// padding of the history commitment of this validator. +// +// If there is an Option.Some() retrun value, it means that callers don't need +// to actually step through a machine to produce a series of hashes, because all +// of the hashes can just be "virtual" copies of a single machine in the +// FINISHED state's hash. +func (s *BOLDStateProvider) virtualState(msgNum arbutil.MessageIndex, limit l2stateprovider.Batch) (option.Option[validator.GoGlobalState], error) { + gs := option.None[validator.GoGlobalState]() + limitMsgCount, err := s.statelessValidator.InboxTracker().GetBatchMessageCount(uint64(limit) - 1) + if err != nil { + return gs, fmt.Errorf("could not get limitMsgCount at %d: %w", limit, err) + } + if msgNum >= limitMsgCount { + result, err := s.statelessValidator.InboxStreamer().ResultAtCount(arbutil.MessageIndex(limitMsgCount)) + if err != nil { + return gs, fmt.Errorf("could not get global state at limitMsgCount %d: %w", limitMsgCount, err) + } + gs = option.Some(validator.GoGlobalState{ + BlockHash: result.BlockHash, + SendRoot: result.SendRoot, + Batch: uint64(limit), + PosInBatch: 0, + }) + } + return gs, nil +} + +// CtxWithCheckAlive Creates a context with a check alive routine that will +// cancel the context if the check alive routine fails. +func ctxWithCheckAlive(ctxIn context.Context, execRun validator.ExecutionRun) (context.Context, context.CancelFunc) { + // Create a context that will cancel if the check alive routine fails. + // This is to ensure that we do not have the validator froze indefinitely if + // the execution run is no longer alive. + ctx, cancel := context.WithCancel(ctxIn) + go func() { + // Call cancel so that the calling function is canceled if the check alive + // routine fails/returns. + defer cancel() + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + // Create a context with a timeout, so that the check alive routine does + // not run indefinitely. + ctxCheckAliveWithTimeout, cancelCheckAliveWithTimeout := context.WithTimeout(ctx, 5*time.Second) + err := execRun.CheckAlive(ctxCheckAliveWithTimeout) + if err != nil { + executionNodeOfflineGauge.Inc(1) + cancelCheckAliveWithTimeout() + return + } + cancelCheckAliveWithTimeout() + } + } + }() + return ctx, cancel +} + +// CollectProof collects a one-step proof at a message number and OpcodeIndex. +func (s *BOLDStateProvider) CollectProof( + ctx context.Context, + assertionMetadata *l2stateprovider.AssociatedAssertionMetadata, + blockChallengeHeight l2stateprovider.Height, + machineIndex l2stateprovider.OpcodeIndex, +) ([]byte, error) { + messageNum, err := s.messageNum(assertionMetadata, blockChallengeHeight) + if err != nil { + return nil, err + } + // Check if we have a virtual global state. + vs, err := s.virtualState(messageNum, assertionMetadata.BatchLimit) + if err != nil { + return nil, err + } + if vs.IsSome() { + m := server_arb.NewFinishedMachine(vs.Unwrap()) + defer m.Destroy() + log.Info( + "Getting machine OSP from virtual state", + "fromBatch", assertionMetadata.FromState.Batch, + "fromPosInBatch", assertionMetadata.FromState.PosInBatch, + "blockChallengeHeight", blockChallengeHeight, + "messageNum", messageNum, + "machineIndex", machineIndex, + ) + return m.ProveNextStep(), nil + } + entry, err := s.statelessValidator.CreateReadyValidationEntry(ctx, messageNum) + if err != nil { + return nil, err + } + input, err := entry.ToInput([]ethdb.WasmTarget{rawdb.TargetWavm}) + if err != nil { + return nil, err + } + log.Info( + "Getting machine OSP", + "fromBatch", assertionMetadata.FromState.Batch, + "fromPosInBatch", assertionMetadata.FromState.PosInBatch, + "blockChallengeHeight", blockChallengeHeight, + "messageNum", messageNum, + "machineIndex", machineIndex, + "startState", fmt.Sprintf("%+v", input.StartState), + ) + wasmModRoot := assertionMetadata.WasmModuleRoot + execRun, err := s.statelessValidator.ExecutionSpawners()[0].CreateExecutionRun(wasmModRoot, input, true).Await(ctx) + if err != nil { + return nil, err + } + defer execRun.Close() + ctxCheckAlive, cancelCheckAlive := ctxWithCheckAlive(ctx, execRun) + defer cancelCheckAlive() + oneStepProofPromise := execRun.GetProofAt(uint64(machineIndex)) + return oneStepProofPromise.Await(ctxCheckAlive) +} diff --git a/staker/bold/data_poster_transactor.go b/staker/bold/data_poster_transactor.go new file mode 100644 index 0000000000..aa5f8d9768 --- /dev/null +++ b/staker/bold/data_poster_transactor.go @@ -0,0 +1,44 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/main/LICENSE +package bold + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + + solimpl "github.com/offchainlabs/bold/chain-abstraction/sol-implementation" + "github.com/offchainlabs/nitro/arbnode/dataposter" +) + +// DataPosterTransactor is a wrapper around a DataPoster that implements the Transactor interface. +type DataPosterTransactor struct { + fifo *solimpl.FIFO + *dataposter.DataPoster +} + +func NewDataPosterTransactor(dataPoster *dataposter.DataPoster) *DataPosterTransactor { + return &DataPosterTransactor{ + fifo: solimpl.NewFIFO(1000), + DataPoster: dataPoster, + } +} + +func (d *DataPosterTransactor) SendTransaction(ctx context.Context, fn func(opts *bind.TransactOpts) (*types.Transaction, error), opts *bind.TransactOpts, gas uint64) (*types.Transaction, error) { + // Try to acquire lock and if it fails, wait for a bit and try again. + for !d.fifo.Lock() { + select { + case <-time.After(100 * time.Millisecond): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + defer d.fifo.Unlock() + tx, err := fn(opts) + if err != nil { + return nil, err + } + return d.PostSimpleTransaction(ctx, *tx.To(), tx.Data(), gas, tx.Value()) +} diff --git a/staker/challenge-cache/cache.go b/staker/challenge-cache/cache.go index ed4fad6450..98310c742a 100644 --- a/staker/challenge-cache/cache.go +++ b/staker/challenge-cache/cache.go @@ -79,29 +79,21 @@ type Cache struct { // New cache from a base directory path. func New(baseDir string) (*Cache, error) { - return &Cache{ - baseDir: baseDir, - tempWritesDir: "", - }, nil -} - -// Init a cache by verifying its base directory exists. -func (c *Cache) Init(_ context.Context) error { - if _, err := os.Stat(c.baseDir); err != nil { - if err := os.MkdirAll(c.baseDir, os.ModePerm); err != nil { - return fmt.Errorf("could not make initialize challenge cache directory %s: %w", c.baseDir, err) - } + if err := os.MkdirAll(baseDir, os.ModePerm); err != nil { + return nil, err } // We create a temp directory to write our hashes to first when putting to the cache. // Once writing succeeds, we rename in an atomic operation to the correct file name // in the cache directory hierarchy in the `Put` function. All of these temporary writes // will occur in a subdir of the base directory called temp. - tempWritesDir, err := os.MkdirTemp(c.baseDir, "temp") + tempWritesDir, err := os.MkdirTemp(baseDir, "temp") if err != nil { - return err + return nil, err } - c.tempWritesDir = tempWritesDir - return nil + return &Cache{ + baseDir: baseDir, + tempWritesDir: tempWritesDir, + }, nil } // Get a list of hashes from the cache from index 0 up to a certain index. Hashes are saved as files in the directory @@ -187,12 +179,12 @@ func (c *Cache) Prune(ctx context.Context, messageNumber uint64) error { if info.IsDir() { matches := pattern.FindStringSubmatch(info.Name()) if len(matches) > 1 { - dirNameMessageNum, err := strconv.Atoi(matches[1]) + dirNameMessageNum, err := strconv.ParseUint(matches[1], 10, 64) if err != nil { return err } // Collect the directory path if the message number is <= the specified value. - if dirNameMessageNum <= int(messageNumber) { + if dirNameMessageNum <= messageNumber { pathsToDelete = append(pathsToDelete, path) } } @@ -217,11 +209,11 @@ func (c *Cache) Prune(ctx context.Context, messageNumber uint64) error { } // Reads 32 bytes at a time from a reader up to a specified height. If none, then read all. -func readHashes(r io.Reader, numToRead uint64) ([]common.Hash, error) { +func readHashes(r io.Reader, toReadLimit uint64) ([]common.Hash, error) { br := bufio.NewReader(r) hashes := make([]common.Hash, 0) buf := make([]byte, 0, common.HashLength) - for totalRead := uint64(0); totalRead < numToRead; totalRead++ { + for totalRead := uint64(0); totalRead < toReadLimit; totalRead++ { n, err := br.Read(buf[:cap(buf)]) if err != nil { // If we try to read but reach EOF, we break out of the loop. @@ -236,13 +228,6 @@ func readHashes(r io.Reader, numToRead uint64) ([]common.Hash, error) { } hashes = append(hashes, common.BytesToHash(buf)) } - if numToRead > uint64(len(hashes)) { - return nil, fmt.Errorf( - "wanted to read %d hashes, but only read %d hashes", - numToRead, - len(hashes), - ) - } return hashes, nil } diff --git a/staker/challenge-cache/cache_test.go b/staker/challenge-cache/cache_test.go index af0a058f78..4328ceee12 100644 --- a/staker/challenge-cache/cache_test.go +++ b/staker/challenge-cache/cache_test.go @@ -18,8 +18,6 @@ import ( var _ HistoryCommitmentCacher = (*Cache)(nil) func TestCache(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() basePath := t.TempDir() if err := os.MkdirAll(basePath, os.ModePerm); err != nil { t.Fatal(err) @@ -28,9 +26,6 @@ func TestCache(t *testing.T) { if err != nil { t.Fatal(err) } - if err = cache.Init(ctx); err != nil { - t.Fatal(err) - } key := &Key{ WavmModuleRoot: common.BytesToHash([]byte("foo")), MessageHeight: 0, @@ -81,9 +76,6 @@ func TestPrune(t *testing.T) { if err != nil { t.Fatal(err) } - if err = cache.Init(ctx); err != nil { - t.Fatal(err) - } key := &Key{ WavmModuleRoot: common.BytesToHash([]byte("foo")), MessageHeight: 20, @@ -166,8 +158,9 @@ func TestPrune(t *testing.T) { } key = &Key{ WavmModuleRoot: root, - MessageHeight: uint64(i), - StepHeights: []uint64{0}, + // #nosec G115 + MessageHeight: uint64(i), + StepHeights: []uint64{0}, } if err = cache.Put(key, hashes); err != nil { t.Fatal(err) @@ -182,8 +175,9 @@ func TestPrune(t *testing.T) { for i := 0; i <= 5; i++ { key = &Key{ WavmModuleRoot: root, - MessageHeight: uint64(i), - StepHeights: []uint64{0}, + // #nosec G115 + MessageHeight: uint64(i), + StepHeights: []uint64{0}, } if _, err = cache.Get(key, 3); !errors.Is(err, ErrNotFoundInCache) { t.Error(err) @@ -193,8 +187,9 @@ func TestPrune(t *testing.T) { for i := 6; i < totalMessages; i++ { key = &Key{ WavmModuleRoot: root, - MessageHeight: uint64(i), - StepHeights: []uint64{0}, + // #nosec G115 + MessageHeight: uint64(i), + StepHeights: []uint64{0}, } items, err := cache.Get(key, 3) if err != nil { @@ -209,16 +204,6 @@ func TestPrune(t *testing.T) { } func TestReadWriteStatehashes(t *testing.T) { - t.Run("read up to, but had empty reader", func(t *testing.T) { - b := bytes.NewBuffer([]byte{}) - _, err := readHashes(b, 100) - if err == nil { - t.Fatal("Wanted error") - } - if !strings.Contains(err.Error(), "only read 0 hashes") { - t.Fatal("Unexpected error") - } - }) t.Run("read single root", func(t *testing.T) { b := bytes.NewBuffer([]byte{}) want := common.BytesToHash([]byte("foo")) @@ -321,20 +306,20 @@ func Test_readHashes(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } }) - t.Run("EOF, but did not read as much as was expected", func(t *testing.T) { + t.Run("EOF, but did not read as much as was possible", func(t *testing.T) { want := []common.Hash{ common.BytesToHash([]byte("foo")), common.BytesToHash([]byte("bar")), common.BytesToHash([]byte("baz")), } - m := &mockReader{wantErr: true, hashes: want, err: io.EOF} - _, err := readHashes(m, 100) - if err == nil { - t.Fatal(err) - } - if !strings.Contains(err.Error(), "wanted to read 100") { + m := &mockReader{wantErr: false, hashes: want, bytesRead: 32} + hashes, err := readHashes(m, 100) + if err != nil { t.Fatalf("Unexpected error: %v", err) } + if len(hashes) != len(want) { + t.Fatalf("Wrong number of hashes. Expected %d, got %d", len(want), len(hashes)) + } }) t.Run("Reads wrong number of bytes", func(t *testing.T) { want := []common.Hash{ @@ -421,8 +406,6 @@ func Test_determineFilePath(t *testing.T) { } func BenchmarkCache_Read_32Mb(b *testing.B) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() b.StopTimer() basePath := os.TempDir() if err := os.MkdirAll(basePath, os.ModePerm); err != nil { @@ -432,9 +415,6 @@ func BenchmarkCache_Read_32Mb(b *testing.B) { if err != nil { b.Fatal(err) } - if err = cache.Init(ctx); err != nil { - b.Fatal(err) - } key := &Key{ WavmModuleRoot: common.BytesToHash([]byte("foo")), MessageHeight: 0, diff --git a/staker/execution_challenge_bakend.go b/staker/execution_challenge_bakend.go index 8ab60efced..6616d8f8c1 100644 --- a/staker/execution_challenge_bakend.go +++ b/staker/execution_challenge_bakend.go @@ -7,6 +7,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator" ) diff --git a/staker/block_challenge_backend.go b/staker/legacy/block_challenge_backend.go similarity index 95% rename from staker/block_challenge_backend.go rename to staker/legacy/block_challenge_backend.go index 42351789ba..969c482586 100644 --- a/staker/block_challenge_backend.go +++ b/staker/legacy/block_challenge_backend.go @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "context" @@ -13,19 +13,21 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/challengegen" + "github.com/offchainlabs/nitro/staker" "github.com/offchainlabs/nitro/validator" ) type BlockChallengeBackend struct { - streamer TransactionStreamerInterface + streamer staker.TransactionStreamerInterface startMsgCount arbutil.MessageIndex startPosition uint64 endPosition uint64 startGs validator.GoGlobalState endGs validator.GoGlobalState - inboxTracker InboxTrackerInterface + inboxTracker staker.InboxTrackerInterface tooFarStartsAtPosition uint64 } @@ -35,8 +37,8 @@ var _ ChallengeBackend = (*BlockChallengeBackend)(nil) func NewBlockChallengeBackend( initialState *challengegen.ChallengeManagerInitiatedChallenge, maxBatchesRead uint64, - streamer TransactionStreamerInterface, - inboxTracker InboxTrackerInterface, + streamer staker.TransactionStreamerInterface, + inboxTracker staker.InboxTrackerInterface, ) (*BlockChallengeBackend, error) { startGs := validator.GoGlobalStateFromSolidity(initialState.StartState) @@ -219,6 +221,6 @@ func (b *BlockChallengeBackend) IssueExecChallenge( }, machineStatuses, globalStateHashes, - big.NewInt(int64(numsteps)), + new(big.Int).SetUint64(numsteps), ) } diff --git a/staker/challenge_manager.go b/staker/legacy/challenge_manager.go similarity index 96% rename from staker/challenge_manager.go rename to staker/legacy/challenge_manager.go index b1421d7e41..1aa13a9e05 100644 --- a/staker/challenge_manager.go +++ b/staker/legacy/challenge_manager.go @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "context" @@ -10,16 +10,19 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/challengegen" + "github.com/offchainlabs/nitro/staker" "github.com/offchainlabs/nitro/validator" ) @@ -47,7 +50,7 @@ type ChallengeBackend interface { } // Assert that ExecutionChallengeBackend implements ChallengeBackend -var _ ChallengeBackend = (*ExecutionChallengeBackend)(nil) +var _ ChallengeBackend = (*staker.ExecutionChallengeBackend)(nil) type challengeCore struct { con *challengegen.ChallengeManager @@ -68,13 +71,13 @@ type ChallengeManager struct { blockChallengeBackend *BlockChallengeBackend // fields below are only used to create execution challenge from block challenge - validator *StatelessBlockValidator + validator *staker.StatelessBlockValidator maxBatchesRead uint64 wasmModuleRoot common.Hash // these fields are empty until working on execution challenge initialMachineMessageCount arbutil.MessageIndex - executionChallengeBackend *ExecutionChallengeBackend + executionChallengeBackend *staker.ExecutionChallengeBackend machineFinalStepCount uint64 } @@ -87,7 +90,7 @@ func NewChallengeManager( fromAddr common.Address, challengeManagerAddr common.Address, challengeIndex uint64, - val *StatelessBlockValidator, + val *staker.StatelessBlockValidator, startL1Block uint64, confirmationBlocks int64, ) (*ChallengeManager, error) { @@ -127,8 +130,8 @@ func NewChallengeManager( backend, err := NewBlockChallengeBackend( parsedLog, challengeInfo.MaxInboxMessages, - val.streamer, - val.inboxTracker, + val.InboxStreamer(), + val.InboxTracker(), ) if err != nil { return nil, fmt.Errorf("error creating block challenge backend for challenge %v: %w", challengeIndex, err) @@ -165,7 +168,7 @@ func NewExecutionChallengeManager( if err != nil { return nil, err } - backend, err := NewExecutionChallengeBackend(exec) + backend, err := staker.NewExecutionChallengeBackend(exec) if err != nil { return nil, err } @@ -294,7 +297,7 @@ func (m *ChallengeManager) bisect(ctx context.Context, backend ChallengeBackend, if newChallengeLength < bisectionDegree { bisectionDegree = newChallengeLength } - newSegments := make([][32]byte, int(bisectionDegree+1)) + newSegments := make([][32]byte, bisectionDegree+1) position := startSegmentPosition normalSegmentLength := newChallengeLength / bisectionDegree for i := range newSegments { @@ -468,7 +471,7 @@ func (m *ChallengeManager) createExecutionBackend(ctx context.Context, step uint if err != nil { return fmt.Errorf("error creating validation entry for challenge %v msg %v for execution challenge: %w", m.challengeIndex, initialCount, err) } - input, err := entry.ToInput([]rawdb.Target{rawdb.TargetWavm}) + input, err := entry.ToInput([]ethdb.WasmTarget{rawdb.TargetWavm}) if err != nil { return fmt.Errorf("error getting validation entry input of challenge %v msg %v: %w", m.challengeIndex, initialCount, err) } @@ -480,9 +483,9 @@ func (m *ChallengeManager) createExecutionBackend(ctx context.Context, step uint } input.BatchInfo = prunedBatches var execRun validator.ExecutionRun - for _, spawner := range m.validator.execSpawners { + for _, spawner := range m.validator.ExecutionSpawners() { if validator.SpawnerSupportsModule(spawner, m.wasmModuleRoot) { - execRun, err = spawner.CreateExecutionRun(m.wasmModuleRoot, input).Await(ctx) + execRun, err = spawner.CreateExecutionRun(m.wasmModuleRoot, input, false).Await(ctx) if err != nil { return fmt.Errorf("error creating execution backend for msg %v: %w", initialCount, err) } @@ -492,7 +495,7 @@ func (m *ChallengeManager) createExecutionBackend(ctx context.Context, step uint if execRun == nil { return fmt.Errorf("did not find valid execution backend") } - backend, err := NewExecutionChallengeBackend(execRun) + backend, err := staker.NewExecutionChallengeBackend(execRun) if err != nil { return err } @@ -565,6 +568,7 @@ func (m *ChallengeManager) Act(ctx context.Context) (*types.Transaction, error) nextMovePos, ) } + // #nosec G115 err = m.createExecutionBackend(ctx, uint64(nextMovePos)) if err != nil { return nil, fmt.Errorf("error creating execution backend: %w", err) diff --git a/staker/challenge_test.go b/staker/legacy/challenge_test.go similarity index 92% rename from staker/challenge_test.go rename to staker/legacy/challenge_test.go index 4534b04a25..a34e4e885d 100644 --- a/staker/challenge_test.go +++ b/staker/legacy/challenge_test.go @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "context" @@ -17,9 +17,10 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/ospgen" "github.com/offchainlabs/nitro/validator" @@ -77,7 +78,7 @@ func CreateChallenge( resultReceiverAddr, maxInboxMessage, [2][32]byte{startHashBytes, endHashBytes}, - big.NewInt(int64(endMachineSteps)), + new(big.Int).SetUint64(endMachineSteps), asserter, challenger, big.NewInt(100), @@ -97,12 +98,12 @@ func createTransactOpts(t *testing.T) *bind.TransactOpts { return opts } -func createGenesisAlloc(accts ...*bind.TransactOpts) core.GenesisAlloc { - alloc := make(core.GenesisAlloc) +func createGenesisAlloc(accts ...*bind.TransactOpts) types.GenesisAlloc { + alloc := make(types.GenesisAlloc) amount := big.NewInt(10) amount.Exp(amount, big.NewInt(20), nil) for _, opts := range accts { - alloc[opts.From] = core.GenesisAccount{ + alloc[opts.From] = types.Account{ Balance: new(big.Int).Set(amount), } } @@ -241,7 +242,7 @@ func runChallengeTest( func createBaseMachine(t *testing.T, wasmname string, wasmModules []string) *server_arb.ArbitratorMachine { _, filename, _, _ := runtime.Caller(0) - wasmDir := path.Join(path.Dir(filename), "../arbitrator/prover/test-cases/") + wasmDir := path.Join(path.Dir(filename), "../../arbitrator/prover/test-cases/") wasmPath := path.Join(wasmDir, wasmname) @@ -258,31 +259,31 @@ func createBaseMachine(t *testing.T, wasmname string, wasmModules []string) *ser func TestChallengeToOSP(t *testing.T) { machine := createBaseMachine(t, "global-state.wasm", []string{"global-state-wrapper.wasm"}) - IncorrectMachine := server_arb.NewIncorrectMachine(machine, 200) + IncorrectMachine := NewIncorrectMachine(machine, 200) runChallengeTest(t, machine, IncorrectMachine, false, false, 0) } func TestChallengeToFailedOSP(t *testing.T) { machine := createBaseMachine(t, "global-state.wasm", []string{"global-state-wrapper.wasm"}) - IncorrectMachine := server_arb.NewIncorrectMachine(machine, 200) + IncorrectMachine := NewIncorrectMachine(machine, 200) runChallengeTest(t, machine, IncorrectMachine, true, false, 0) } func TestChallengeToErroredOSP(t *testing.T) { machine := createBaseMachine(t, "const.wasm", nil) - IncorrectMachine := server_arb.NewIncorrectMachine(machine, 10) + IncorrectMachine := NewIncorrectMachine(machine, 10) runChallengeTest(t, machine, IncorrectMachine, false, false, 0) } func TestChallengeToFailedErroredOSP(t *testing.T) { machine := createBaseMachine(t, "const.wasm", nil) - IncorrectMachine := server_arb.NewIncorrectMachine(machine, 10) + IncorrectMachine := NewIncorrectMachine(machine, 10) runChallengeTest(t, machine, IncorrectMachine, true, false, 0) } func TestChallengeToTimeout(t *testing.T) { machine := createBaseMachine(t, "global-state.wasm", []string{"global-state-wrapper.wasm"}) - IncorrectMachine := server_arb.NewIncorrectMachine(machine, 200) + IncorrectMachine := NewIncorrectMachine(machine, 200) runChallengeTest(t, machine, IncorrectMachine, false, true, 0) } diff --git a/staker/common_test.go b/staker/legacy/common_test.go similarity index 95% rename from staker/common_test.go rename to staker/legacy/common_test.go index eec6882fde..06ebeeffa3 100644 --- a/staker/common_test.go +++ b/staker/legacy/common_test.go @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "testing" diff --git a/staker/fast_confirm.go b/staker/legacy/fast_confirm.go similarity index 90% rename from staker/fast_confirm.go rename to staker/legacy/fast_confirm.go index 88f457f528..13ce32b849 100644 --- a/staker/fast_confirm.go +++ b/staker/legacy/fast_confirm.go @@ -1,7 +1,7 @@ // Copyright 2023-2024, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "context" @@ -46,7 +46,7 @@ func NewFastConfirmSafe( gasRefunder: gasRefunder, l1Reader: l1Reader, } - safe, err := contractsgen.NewSafe(fastConfirmSafeAddress, builder) + safe, err := contractsgen.NewSafe(fastConfirmSafeAddress, wallet.L1Client()) if err != nil { return nil, err } @@ -121,15 +121,13 @@ func (f *FastConfirmSafe) tryFastConfirmation(ctx context.Context, blockHash com return err } if alreadyApproved.Cmp(common.Big1) == 0 { + log.Info("Already approved Safe tx hash for fast confirmation, checking if we can execute the Safe tx", "safeHash", safeTxHash, "nodeHash", nodeHash) _, err = f.checkApprovedHashAndExecTransaction(ctx, fastConfirmCallData, safeTxHash) return err } - auth, err := f.builder.Auth(ctx) - if err != nil { - return err - } - _, err = f.safe.ApproveHash(auth, safeTxHash) + log.Info("Approving Safe tx hash to fast confirm", "safeHash", safeTxHash, "nodeHash", nodeHash) + _, err = f.safe.ApproveHash(f.builder.Auth(ctx), safeTxHash) if err != nil { return err } @@ -158,7 +156,7 @@ func (f *FastConfirmSafe) tryFastConfirmation(ctx context.Context, blockHash com } func (f *FastConfirmSafe) flushTransactions(ctx context.Context) error { - arbTx, err := f.wallet.ExecuteTransactions(ctx, f.builder, f.gasRefunder) + arbTx, err := f.builder.ExecuteTransactions(ctx) if err != nil { return err } @@ -170,7 +168,6 @@ func (f *FastConfirmSafe) flushTransactions(ctx context.Context) error { return fmt.Errorf("error waiting for tx receipt: %w", err) } } - f.builder.ClearTransactions() return nil } @@ -227,12 +224,9 @@ func (f *FastConfirmSafe) checkApprovedHashAndExecTransaction(ctx context.Contex } } if approvedHashCount >= f.threshold { - auth, err := f.builder.Auth(ctx) - if err != nil { - return false, err - } - _, err = f.safe.ExecTransaction( - auth, + log.Info("Executing Safe tx to fast confirm", "safeHash", safeTxHash) + _, err := f.safe.ExecTransaction( + f.builder.Auth(ctx), f.wallet.RollupAddress(), big.NewInt(0), fastConfirmCallData, @@ -249,5 +243,6 @@ func (f *FastConfirmSafe) checkApprovedHashAndExecTransaction(ctx context.Contex } return true, nil } + log.Info("Not enough Safe tx approvals yet to fast confirm", "safeHash", safeTxHash) return false, nil } diff --git a/staker/l1_validator.go b/staker/legacy/l1_validator.go similarity index 89% rename from staker/l1_validator.go rename to staker/legacy/l1_validator.go index dd9673ee0b..f88ab93d0e 100644 --- a/staker/l1_validator.go +++ b/staker/legacy/l1_validator.go @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "context" @@ -10,18 +10,20 @@ import ( "math/big" "time" - "github.com/offchainlabs/nitro/staker/txbuilder" - "github.com/offchainlabs/nitro/util/arbmath" - "github.com/offchainlabs/nitro/util/headerreader" - "github.com/offchainlabs/nitro/validator" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/rollupgen" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/staker/txbuilder" + "github.com/offchainlabs/nitro/util/arbmath" + "github.com/offchainlabs/nitro/util/headerreader" + "github.com/offchainlabs/nitro/validator" ) type ConfirmType uint8 @@ -42,34 +44,35 @@ const ( ) type L1Validator struct { - rollup *RollupWatcher + rollup *staker.RollupWatcher rollupAddress common.Address validatorUtils *rollupgen.ValidatorUtils - client arbutil.L1Interface + client *ethclient.Client builder *txbuilder.Builder wallet ValidatorWalletInterface callOpts bind.CallOpts - inboxTracker InboxTrackerInterface - txStreamer TransactionStreamerInterface - blockValidator *BlockValidator + inboxTracker staker.InboxTrackerInterface + txStreamer staker.TransactionStreamerInterface + blockValidator *staker.BlockValidator lastWasmModuleRoot common.Hash } func NewL1Validator( - client arbutil.L1Interface, + client *ethclient.Client, wallet ValidatorWalletInterface, validatorUtilsAddress common.Address, + gasRefunder common.Address, callOpts bind.CallOpts, - inboxTracker InboxTrackerInterface, - txStreamer TransactionStreamerInterface, - blockValidator *BlockValidator, + inboxTracker staker.InboxTrackerInterface, + txStreamer staker.TransactionStreamerInterface, + blockValidator *staker.BlockValidator, ) (*L1Validator, error) { - builder, err := txbuilder.NewBuilder(wallet) + builder, err := txbuilder.NewBuilder(wallet, gasRefunder) if err != nil { return nil, err } - rollup, err := NewRollupWatcher(wallet.RollupAddress(), builder, callOpts) + rollup, err := staker.NewRollupWatcher(wallet.RollupAddress(), wallet.L1Client(), callOpts) if err != nil { return nil, err } @@ -101,8 +104,7 @@ func (v *L1Validator) getCallOpts(ctx context.Context) *bind.CallOpts { } func (v *L1Validator) Initialize(ctx context.Context) error { - err := v.rollup.Initialize(ctx) - if err != nil { + if err := v.rollup.Initialize(ctx); err != nil { return err } return v.updateBlockValidatorModuleRoot(ctx) @@ -140,7 +142,7 @@ func (v *L1Validator) resolveTimedOutChallenges(ctx context.Context) (*types.Tra return v.wallet.TimeoutChallenges(ctx, challengesToEliminate) } -func (v *L1Validator) resolveNextNode(ctx context.Context, info *StakerInfo, latestConfirmedNode *uint64) (bool, error) { +func (v *L1Validator) resolveNextNode(ctx context.Context, info *staker.StakerInfo, latestConfirmedNode *uint64) (bool, error) { callOpts := v.getCallOpts(ctx) confirmType, err := v.validatorUtils.CheckDecidableNextNode(callOpts, v.rollupAddress) if err != nil { @@ -158,11 +160,7 @@ func (v *L1Validator) resolveNextNode(ctx context.Context, info *StakerInfo, lat return false, nil } log.Warn("rejecting node", "node", unresolvedNodeIndex) - auth, err := v.builder.Auth(ctx) - if err != nil { - return false, err - } - _, err = v.rollup.RejectNextNode(auth, *addr) + _, err = v.rollup.RejectNextNode(v.builder.Auth(ctx), *addr) return true, err case CONFIRM_TYPE_VALID: nodeInfo, err := v.rollup.LookupNode(ctx, unresolvedNodeIndex) @@ -171,11 +169,7 @@ func (v *L1Validator) resolveNextNode(ctx context.Context, info *StakerInfo, lat } afterGs := nodeInfo.AfterState().GlobalState log.Info("confirming node", "node", unresolvedNodeIndex) - auth, err := v.builder.Auth(ctx) - if err != nil { - return false, err - } - _, err = v.rollup.ConfirmNextNode(auth, afterGs.BlockHash, afterGs.SendRoot) + _, err = v.rollup.ConfirmNextNode(v.builder.Auth(ctx), afterGs.BlockHash, afterGs.SendRoot) if err != nil { return false, err } @@ -204,7 +198,7 @@ func (v *L1Validator) isRequiredStakeElevated(ctx context.Context) (bool, error) } type createNodeAction struct { - assertion *Assertion + assertion *staker.Assertion prevInboxMaxCount *big.Int hash common.Hash } @@ -221,7 +215,7 @@ type OurStakerInfo struct { LatestStakedNodeHash common.Hash CanProgress bool StakeExists bool - *StakerInfo + *staker.StakerInfo } func (v *L1Validator) generateNodeAction( @@ -247,6 +241,7 @@ func (v *L1Validator) generateNodeAction( startStateProposedParentChain, err, ) } + // #nosec G115 startStateProposedTime := time.Unix(int64(startStateProposedHeader.Time), 0) v.txStreamer.PauseReorgs() @@ -264,16 +259,16 @@ func (v *L1Validator) generateNodeAction( return nil, false, nil } - caughtUp, startCount, err := GlobalStateToMsgCount(v.inboxTracker, v.txStreamer, startState.GlobalState) + caughtUp, startCount, err := staker.GlobalStateToMsgCount(v.inboxTracker, v.txStreamer, startState.GlobalState) if err != nil { return nil, false, fmt.Errorf("start state not in chain: %w", err) } if !caughtUp { - target := GlobalStatePosition{ + target := staker.GlobalStatePosition{ BatchNumber: startState.GlobalState.Batch, PosInBatch: startState.GlobalState.PosInBatch, } - var current GlobalStatePosition + var current staker.GlobalStatePosition head, err := v.txStreamer.GetProcessedMessageCount() if err != nil { _, current, err = v.blockValidator.GlobalStatePositionsAtCount(head) @@ -294,7 +289,7 @@ func (v *L1Validator) generateNodeAction( return nil, false, err } validatedGlobalState = valInfo.GlobalState - caughtUp, validatedCount, err = GlobalStateToMsgCount( + caughtUp, validatedCount, err = staker.GlobalStateToMsgCount( v.inboxTracker, v.txStreamer, valInfo.GlobalState, ) if err != nil { @@ -353,11 +348,11 @@ func (v *L1Validator) generateNodeAction( if err != nil { return nil, false, err } - _, gsPos, err := GlobalStatePositionsAtCount(v.inboxTracker, validatedCount, batchNum) + _, gsPos, err := staker.GlobalStatePositionsAtCount(v.inboxTracker, validatedCount, batchNum) if err != nil { return nil, false, fmt.Errorf("%w: failed calculating GSposition for count %d", err, validatedCount) } - validatedGlobalState = buildGlobalState(*execResult, gsPos) + validatedGlobalState = staker.BuildGlobalState(*execResult, gsPos) } currentL1BlockNum, err := v.client.BlockNumber(ctx) @@ -375,6 +370,7 @@ func (v *L1Validator) generateNodeAction( return nil, false, fmt.Errorf("error getting rollup minimum assertion period: %w", err) } + // #nosec G115 timeSinceProposed := big.NewInt(int64(l1BlockNumber) - int64(startStateProposedL1)) if timeSinceProposed.Cmp(minAssertionPeriod) < 0 { // Too soon to assert @@ -423,8 +419,8 @@ func (v *L1Validator) generateNodeAction( log.Error("Found incorrect assertion: Machine status not finished", "node", nd.NodeNum, "machineStatus", nd.Assertion.AfterState.MachineStatus) continue } - caughtUp, nodeMsgCount, err := GlobalStateToMsgCount(v.inboxTracker, v.txStreamer, afterGS) - if errors.Is(err, ErrGlobalStateNotInChain) { + caughtUp, nodeMsgCount, err := staker.GlobalStateToMsgCount(v.inboxTracker, v.txStreamer, afterGS) + if errors.Is(err, staker.ErrGlobalStateNotInChain) { wrongNodesExist = true log.Error("Found incorrect assertion", "node", nd.NodeNum, "afterGS", afterGS, "err", err) continue @@ -507,7 +503,7 @@ func (v *L1Validator) createNewNodeAction( hasSiblingByte[0] = 1 } assertionNumBlocks := uint64(validatedCount - startCount) - assertion := &Assertion{ + assertion := &staker.Assertion{ BeforeState: startState, AfterState: &validator.ExecutionState{ GlobalState: validatedGS, @@ -537,13 +533,13 @@ func (v *L1Validator) createNewNodeAction( } // Returns (execution state, inbox max count, L1 block proposed, parent chain block proposed, error) -func lookupNodeStartState(ctx context.Context, rollup *RollupWatcher, nodeNum uint64, nodeHash common.Hash) (*validator.ExecutionState, *big.Int, uint64, uint64, error) { +func lookupNodeStartState(ctx context.Context, rollup *staker.RollupWatcher, nodeNum uint64, nodeHash common.Hash) (*validator.ExecutionState, *big.Int, uint64, uint64, error) { if nodeNum == 0 { creationEvent, err := rollup.LookupCreation(ctx) if err != nil { return nil, nil, 0, 0, fmt.Errorf("error looking up rollup creation event: %w", err) } - l1BlockNumber, err := arbutil.CorrespondingL1BlockNumber(ctx, rollup.client, creationEvent.Raw.BlockNumber) + l1BlockNumber, err := arbutil.CorrespondingL1BlockNumber(ctx, rollup.Client(), creationEvent.Raw.BlockNumber) if err != nil { return nil, nil, 0, 0, err } diff --git a/validator/server_arb/mock_machine.go b/staker/legacy/mock_machine_test.go similarity index 78% rename from validator/server_arb/mock_machine.go rename to staker/legacy/mock_machine_test.go index 3cf0f9f771..a5dc5400a7 100644 --- a/validator/server_arb/mock_machine.go +++ b/staker/legacy/mock_machine_test.go @@ -1,33 +1,37 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package server_arb +package legacystaker import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator" + "github.com/offchainlabs/nitro/validator/server_arb" ) +// IncorrectMachine will report a bad global state after the incorrectStep onwards. +// It'll also extend the step count to incorrectStep if necessary. type IncorrectMachine struct { - inner *ArbitratorMachine + inner *server_arb.ArbitratorMachine incorrectStep uint64 stepCount uint64 } var badGlobalState = validator.GoGlobalState{Batch: 0xbadbadbadbad, PosInBatch: 0xbadbadbadbad} -var _ MachineInterface = (*IncorrectMachine)(nil) +var _ server_arb.MachineInterface = (*IncorrectMachine)(nil) -func NewIncorrectMachine(inner *ArbitratorMachine, incorrectStep uint64) *IncorrectMachine { +func NewIncorrectMachine(inner *server_arb.ArbitratorMachine, incorrectStep uint64) *IncorrectMachine { return &IncorrectMachine{ inner: inner.Clone(), incorrectStep: incorrectStep, } } -func (m *IncorrectMachine) CloneMachineInterface() MachineInterface { +func (m *IncorrectMachine) CloneMachineInterface() server_arb.MachineInterface { return &IncorrectMachine{ inner: m.inner.Clone(), incorrectStep: m.incorrectStep, @@ -57,6 +61,10 @@ func (m *IncorrectMachine) IsRunning() bool { return m.inner.IsRunning() || m.stepCount < m.incorrectStep } +func (m *IncorrectMachine) IsErrored() bool { + return !m.IsRunning() && m.inner.IsErrored() +} + func (m *IncorrectMachine) ValidForStep(step uint64) bool { return m.inner.ValidForStep(step) } diff --git a/staker/staker.go b/staker/legacy/staker.go similarity index 89% rename from staker/staker.go rename to staker/legacy/staker.go index bdfe0655b7..504e8c8421 100644 --- a/staker/staker.go +++ b/staker/legacy/staker.go @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -package staker +package legacystaker import ( "context" @@ -12,20 +12,22 @@ import ( "strings" "time" + "github.com/google/btree" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rpc" - "github.com/google/btree" - flag "github.com/spf13/pflag" "github.com/offchainlabs/nitro/arbnode/dataposter" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/solgen/go/rollupgen" - "github.com/offchainlabs/nitro/staker/txbuilder" + "github.com/offchainlabs/nitro/staker" "github.com/offchainlabs/nitro/util" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/headerreader" @@ -99,8 +101,8 @@ type L1ValidatorConfig struct { gasRefunder common.Address } -func (c *L1ValidatorConfig) ParseStrategy() (StakerStrategy, error) { - switch strings.ToLower(c.Strategy) { +func ParseStrategy(strategy string) (StakerStrategy, error) { + switch strings.ToLower(strategy) { case "watchtower": return WatchtowerStrategy, nil case "defensive": @@ -112,7 +114,7 @@ func (c *L1ValidatorConfig) ParseStrategy() (StakerStrategy, error) { case "makenodes": return MakeNodesStrategy, nil default: - return WatchtowerStrategy, fmt.Errorf("unknown staker strategy \"%v\"", c.Strategy) + return WatchtowerStrategy, fmt.Errorf("unknown staker strategy \"%v\"", strategy) } } @@ -130,7 +132,7 @@ func (c *L1ValidatorConfig) ValidatorRequired() bool { } func (c *L1ValidatorConfig) Validate() error { - strategy, err := c.ParseStrategy() + strategy, err := ParseStrategy(c.Strategy) if err != nil { return err } @@ -142,7 +144,12 @@ func (c *L1ValidatorConfig) Validate() error { return nil } -type L1ValidatorConfigFetcher func() *L1ValidatorConfig +func (c *L1ValidatorConfig) GasRefunder() common.Address { + return c.gasRefunder +} +func (c *L1ValidatorConfig) StrategyType() StakerStrategy { + return c.strategy +} var DefaultL1ValidatorConfig = L1ValidatorConfig{ Enable: true, @@ -251,6 +258,8 @@ type validatedNode struct { hash common.Hash } +type L1ValidatorConfigFetcher func() *L1ValidatorConfig + type Staker struct { *L1Validator stopwaiter.StopWaiter @@ -265,10 +274,9 @@ type Staker struct { inactiveLastCheckedNode *nodeAndHash inactiveValidatedNodes *btree.BTreeG[validatedNode] bringActiveUntilNode uint64 - inboxReader InboxReaderInterface - statelessBlockValidator *StatelessBlockValidator + inboxReader staker.InboxReaderInterface + statelessBlockValidator *staker.StatelessBlockValidator fatalErr chan<- error - enableFastConfirmation bool fastConfirmSafe *FastConfirmSafe } @@ -281,9 +289,9 @@ type ValidatorWalletInterface interface { TxSenderAddress() *common.Address RollupAddress() common.Address ChallengeManagerAddress() common.Address - L1Client() arbutil.L1Interface + L1Client() *ethclient.Client TestTransactions(context.Context, []*types.Transaction) error - ExecuteTransactions(context.Context, *txbuilder.Builder, common.Address) (*types.Transaction, error) + ExecuteTransactions(context.Context, []*types.Transaction, common.Address) (*types.Transaction, error) TimeoutChallenges(context.Context, []uint64) (*types.Transaction, error) CanBatchTxs() bool AuthIfEoa() *bind.TransactOpts @@ -298,27 +306,23 @@ func NewStaker( wallet ValidatorWalletInterface, callOpts bind.CallOpts, config L1ValidatorConfigFetcher, - blockValidator *BlockValidator, - statelessBlockValidator *StatelessBlockValidator, + blockValidator *staker.BlockValidator, + statelessBlockValidator *staker.StatelessBlockValidator, stakedNotifiers []LatestStakedNotifier, confirmedNotifiers []LatestConfirmedNotifier, validatorUtilsAddress common.Address, fatalErr chan<- error, ) (*Staker, error) { - if err := config().Validate(); err != nil { return nil, err } client := l1Reader.Client() - val, err := NewL1Validator(client, wallet, validatorUtilsAddress, callOpts, - statelessBlockValidator.inboxTracker, statelessBlockValidator.streamer, blockValidator) + val, err := NewL1Validator(client, wallet, validatorUtilsAddress, config().GasRefunder(), callOpts, + statelessBlockValidator.InboxTracker(), statelessBlockValidator.InboxStreamer(), blockValidator) if err != nil { return nil, err } stakerLastSuccessfulActionGauge.Update(time.Now().Unix()) - if config().StartValidationFromStaked && blockValidator != nil { - stakedNotifiers = append(stakedNotifiers, blockValidator) - } inactiveValidatedNodes := btree.NewG(2, func(a, b validatedNode) bool { return a.number < b.number || (a.number == b.number && a.hash.Cmp(b.hash) < 0) }) @@ -331,7 +335,7 @@ func NewStaker( config: config, highGasBlocksBuffer: big.NewInt(config().PostingStrategy.HighGasDelayBlocks), lastActCalledBlock: nil, - inboxReader: statelessBlockValidator.inboxReader, + inboxReader: statelessBlockValidator.InboxReader(), statelessBlockValidator: statelessBlockValidator, fatalErr: fatalErr, inactiveValidatedNodes: inactiveValidatedNodes, @@ -347,11 +351,27 @@ func (s *Staker) Initialize(ctx context.Context) error { if walletAddressOrZero != (common.Address{}) { s.updateStakerBalanceMetric(ctx) } + var stakerAddr common.Address + if s.L1Validator.wallet.DataPoster() != nil { + stakerAddr = s.L1Validator.wallet.DataPoster().Sender() + } + whiteListed, err := s.isWhitelisted(ctx) + if err != nil { + return fmt.Errorf("error checking if whitelisted: %w", err) + } + log.Info( + "running as validator", + "txSender", stakerAddr, + "actingAsWallet", walletAddressOrZero, + "whitelisted", whiteListed, + "strategy", s.Strategy(), + ) if s.blockValidator != nil && s.config().StartValidationFromStaked { latestStaked, _, err := s.validatorUtils.LatestStaked(&s.baseCallOpts, s.rollupAddress, walletAddressOrZero) if err != nil { return err } + // #nosec G115 stakerLatestStakedNodeGauge.Update(int64(latestStaked)) if latestStaked == 0 { return nil @@ -362,7 +382,10 @@ func (s *Staker) Initialize(ctx context.Context) error { return err } - return s.blockValidator.InitAssumeValid(stakedInfo.AfterState().GlobalState) + err = s.blockValidator.InitAssumeValid(stakedInfo.AfterState().GlobalState) + if err != nil { + return err + } } return s.setupFastConfirmation(ctx) } @@ -389,9 +412,9 @@ func (s *Staker) setupFastConfirmation(ctx context.Context) error { if err != nil { return fmt.Errorf("getting rollup fast confirmer address: %w", err) } + log.Info("Setting up fast confirmation", "wallet", walletAddress, "fastConfirmer", fastConfirmer) if fastConfirmer == walletAddress { // We can directly fast confirm nodes - s.enableFastConfirmation = true return nil } else if fastConfirmer == (common.Address{}) { // No fast confirmer enabled @@ -403,7 +426,7 @@ func (s *Staker) setupFastConfirmation(ctx context.Context) error { fastConfirmer, s.builder, s.wallet, - cfg.gasRefunder, + cfg.GasRefunder(), s.l1Reader, ) if err != nil { @@ -418,13 +441,12 @@ func (s *Staker) setupFastConfirmation(ctx context.Context) error { if !isOwner { return fmt.Errorf("staker wallet address %v is not an owner of the fast confirm safe %v", walletAddress, fastConfirmer) } - s.enableFastConfirmation = true s.fastConfirmSafe = fastConfirmSafe return nil } func (s *Staker) tryFastConfirmationNodeNumber(ctx context.Context, number uint64, hash common.Hash) error { - if !s.enableFastConfirmation { + if !s.config().EnableFastConfirmation { return nil } nodeInfo, err := s.rollup.LookupNode(ctx, number) @@ -435,28 +457,26 @@ func (s *Staker) tryFastConfirmationNodeNumber(ctx context.Context, number uint6 } func (s *Staker) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash, nodeHash common.Hash) error { - if !s.enableFastConfirmation { + if !s.config().EnableFastConfirmation { return nil } if s.fastConfirmSafe != nil { return s.fastConfirmSafe.tryFastConfirmation(ctx, blockHash, sendRoot, nodeHash) } - auth, err := s.builder.Auth(ctx) - if err != nil { - return err - } - _, err = s.rollup.FastConfirmNextNode(auth, blockHash, sendRoot) + auth := s.builder.Auth(ctx) + log.Info("Fast confirming node with wallet", "wallet", auth.From, "nodeHash", nodeHash) + _, err := s.rollup.FastConfirmNextNode(auth, blockHash, sendRoot, nodeHash) return err } -func (s *Staker) getLatestStakedState(ctx context.Context, staker common.Address) (uint64, arbutil.MessageIndex, *validator.GoGlobalState, error) { +func (s *Staker) getLatestStakedState(ctx context.Context, stakerAddress common.Address) (uint64, arbutil.MessageIndex, *validator.GoGlobalState, error) { callOpts := s.getCallOpts(ctx) if s.l1Reader.UseFinalityData() { callOpts.BlockNumber = big.NewInt(int64(rpc.FinalizedBlockNumber)) } - latestStaked, _, err := s.validatorUtils.LatestStaked(s.getCallOpts(ctx), s.rollupAddress, staker) + latestStaked, _, err := s.validatorUtils.LatestStaked(s.getCallOpts(ctx), s.rollupAddress, stakerAddress) if err != nil { - return 0, 0, nil, fmt.Errorf("couldn't get LatestStaked(%v): %w", staker, err) + return 0, 0, nil, fmt.Errorf("couldn't get LatestStaked(%v): %w", stakerAddress, err) } if latestStaked == 0 { return latestStaked, 0, nil, nil @@ -464,21 +484,21 @@ func (s *Staker) getLatestStakedState(ctx context.Context, staker common.Address stakedInfo, err := s.rollup.LookupNode(ctx, latestStaked) if err != nil { - return 0, 0, nil, fmt.Errorf("couldn't look up latest assertion of %v (%v): %w", staker, latestStaked, err) + return 0, 0, nil, fmt.Errorf("couldn't look up latest assertion of %v (%v): %w", stakerAddress, latestStaked, err) } globalState := stakedInfo.AfterState().GlobalState - caughtUp, count, err := GlobalStateToMsgCount(s.inboxTracker, s.txStreamer, globalState) + caughtUp, count, err := staker.GlobalStateToMsgCount(s.inboxTracker, s.txStreamer, globalState) if err != nil { - if errors.Is(err, ErrGlobalStateNotInChain) && s.fatalErr != nil { - fatal := fmt.Errorf("latest assertion of %v (%v) not in chain: %w", staker, latestStaked, err) + if errors.Is(err, staker.ErrGlobalStateNotInChain) && s.fatalErr != nil { + fatal := fmt.Errorf("latest assertion of %v (%v) not in chain: %w", stakerAddress, latestStaked, err) s.fatalErr <- fatal } - return 0, 0, nil, fmt.Errorf("latest assertion of %v (%v): %w", staker, latestStaked, err) + return 0, 0, nil, fmt.Errorf("latest assertion of %v (%v): %w", stakerAddress, latestStaked, err) } if !caughtUp { - log.Info("latest assertion not yet in our node", "staker", staker, "assertion", latestStaked, "state", globalState) + log.Info("latest assertion not yet in our node", "stakerAddress", stakerAddress, "assertion", latestStaked, "state", globalState) return latestStaked, 0, nil, nil } @@ -488,7 +508,7 @@ func (s *Staker) getLatestStakedState(ctx context.Context, staker common.Address } if processedCount < count { - log.Info("execution catching up to rollup", "staker", staker, "rollupCount", count, "processedCount", processedCount) + log.Info("execution catching up to rollup", "stakerAddress", stakerAddress, "rollupCount", count, "processedCount", processedCount) return latestStaked, 0, nil, nil } @@ -503,12 +523,11 @@ func (s *Staker) StopAndWait() { } func (s *Staker) Start(ctxIn context.Context) { - if s.Strategy() != WatchtowerStrategy { - s.wallet.Start(ctxIn) - } s.StopWaiter.Start(ctxIn, s) backoff := time.Second - ephemeralErrorHandler := util.NewEphemeralErrorHandler(10*time.Minute, "is ahead of on-chain nonce", 0) + isAheadOfOnChainNonceEphemeralErrorHandler := util.NewEphemeralErrorHandler(10*time.Minute, "is ahead of on-chain nonce", 0) + exceedsMaxMempoolSizeEphemeralErrorHandler := util.NewEphemeralErrorHandler(10*time.Minute, dataposter.ErrExceedsMaxMempoolSize.Error(), 0) + blockValidationPendingEphemeralErrorHandler := util.NewEphemeralErrorHandler(10*time.Minute, "block validation is still pending", 0) s.CallIteratively(func(ctx context.Context) (returningWait time.Duration) { defer func() { panicErr := recover() @@ -542,7 +561,9 @@ func (s *Staker) Start(ctxIn context.Context) { } } if err == nil { - ephemeralErrorHandler.Reset() + isAheadOfOnChainNonceEphemeralErrorHandler.Reset() + exceedsMaxMempoolSizeEphemeralErrorHandler.Reset() + blockValidationPendingEphemeralErrorHandler.Reset() backoff = time.Second stakerLastSuccessfulActionGauge.Update(time.Now().Unix()) stakerActionSuccessCounter.Inc(1) @@ -560,7 +581,9 @@ func (s *Staker) Start(ctxIn context.Context) { } else { logLevel = log.Warn } - logLevel = ephemeralErrorHandler.LogLevel(err, logLevel) + logLevel = isAheadOfOnChainNonceEphemeralErrorHandler.LogLevel(err, logLevel) + logLevel = exceedsMaxMempoolSizeEphemeralErrorHandler.LogLevel(err, logLevel) + logLevel = blockValidationPendingEphemeralErrorHandler.LogLevel(err, logLevel) logLevel("error acting as staker", "err", err) return backoff }) @@ -570,6 +593,7 @@ func (s *Staker) Start(ctxIn context.Context) { if err != nil && ctx.Err() == nil { log.Error("staker: error checking latest staked", "err", err) } + // #nosec G115 stakerLatestStakedNodeGauge.Update(int64(staked)) if stakedGlobalState != nil { for _, notifier := range s.stakedNotifiers { @@ -585,6 +609,7 @@ func (s *Staker) Start(ctxIn context.Context) { log.Error("staker: error checking latest confirmed", "err", err) } } + // #nosec G115 stakerLatestConfirmedNodeGauge.Update(int64(confirmed)) if confirmedGlobalState != nil { for _, notifier := range s.confirmedNotifiers { @@ -595,7 +620,7 @@ func (s *Staker) Start(ctxIn context.Context) { }) } -func (s *Staker) IsWhitelisted(ctx context.Context) (bool, error) { +func (s *Staker) isWhitelisted(ctx context.Context) (bool, error) { callOpts := s.getCallOpts(ctx) whitelistDisabled, err := s.rollup.ValidatorWhitelistDisabled(callOpts) if err != nil { @@ -684,12 +709,12 @@ func (s *Staker) confirmDataPosterIsReady(ctx context.Context) error { func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { cfg := s.config() - if cfg.strategy != WatchtowerStrategy { + if cfg.StrategyType() != WatchtowerStrategy { err := s.confirmDataPosterIsReady(ctx) if err != nil { return nil, err } - whitelisted, err := s.IsWhitelisted(ctx) + whitelisted, err := s.isWhitelisted(ctx) if err != nil { return nil, fmt.Errorf("error checking if whitelisted: %w", err) } @@ -703,7 +728,7 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { } callOpts := s.getCallOpts(ctx) s.builder.ClearTransactions() - var rawInfo *StakerInfo + var rawInfo *staker.StakerInfo walletAddressOrZero := s.wallet.AddressOrZero() if walletAddressOrZero != (common.Address{}) { var err error @@ -726,6 +751,7 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { if err != nil { return nil, fmt.Errorf("error getting latest staked node of own wallet %v: %w", walletAddressOrZero, err) } + // #nosec G115 stakerLatestStakedNodeGauge.Update(int64(latestStakedNodeNum)) if rawInfo != nil { rawInfo.LatestStakedNode = latestStakedNodeNum @@ -738,7 +764,7 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { StakeExists: rawInfo != nil, } - effectiveStrategy := cfg.strategy + effectiveStrategy := cfg.StrategyType() nodesLinear, err := s.validatorUtils.AreUnresolvedNodesLinear(callOpts, s.rollupAddress) if err != nil { return nil, fmt.Errorf("error checking for rollup assertion fork: %w", err) @@ -798,14 +824,14 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { confirmedCorrect = stakedOnNode } if confirmedCorrect { + log.Info("trying to fast confirm previous node", "node", firstUnresolvedNode, "nodeHash", nodeInfo.NodeHash) err = s.tryFastConfirmationNodeNumber(ctx, firstUnresolvedNode, nodeInfo.NodeHash) if err != nil { return nil, err } if s.builder.BuildingTransactionCount() > 0 { // Try to fast confirm previous nodes before working on new ones - log.Info("fast confirming previous node", "node", firstUnresolvedNode) - return s.wallet.ExecuteTransactions(ctx, s.builder, cfg.gasRefunder) + return s.builder.ExecuteTransactions(ctx) } } } @@ -875,24 +901,17 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { stakeIsUnwanted := effectiveStrategy < StakeLatestStrategy if stakeIsTooOutdated || stakeIsUnwanted { // Note: we must have an address if rawInfo != nil - auth, err := s.builder.Auth(ctx) - if err != nil { - return nil, err - } + auth := s.builder.Auth(ctx) _, err = s.rollup.ReturnOldDeposit(auth, walletAddressOrZero) if err != nil { return nil, fmt.Errorf("error returning old deposit (from our staker %v): %w", walletAddressOrZero, err) } - auth, err = s.builder.Auth(ctx) - if err != nil { - return nil, err - } _, err = s.rollup.WithdrawStakerFunds(auth) if err != nil { return nil, fmt.Errorf("error withdrawing staker funds from our staker %v: %w", walletAddressOrZero, err) } log.Info("removing old stake and withdrawing funds") - return s.wallet.ExecuteTransactions(ctx, s.builder, cfg.gasRefunder) + return s.builder.ExecuteTransactions(ctx) } } @@ -902,11 +921,7 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { return nil, fmt.Errorf("error checking withdrawable funds of our staker %v: %w", walletAddressOrZero, err) } if withdrawable.Sign() > 0 { - auth, err := s.builder.Auth(ctx) - if err != nil { - return nil, err - } - _, err = s.rollup.WithdrawStakerFunds(auth) + _, err = s.rollup.WithdrawStakerFunds(s.builder.Auth(ctx)) if err != nil { return nil, fmt.Errorf("error withdrawing our staker %v funds: %w", walletAddressOrZero, err) } @@ -946,10 +961,10 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { if info.StakerInfo == nil && info.StakeExists { log.Info("staking to execute transactions") } - return s.wallet.ExecuteTransactions(ctx, s.builder, cfg.gasRefunder) + return s.builder.ExecuteTransactions(ctx) } -func (s *Staker) handleConflict(ctx context.Context, info *StakerInfo) error { +func (s *Staker) handleConflict(ctx context.Context, info *staker.StakerInfo) error { if info.CurrentChallenge == nil { s.activeChallenge = nil return nil @@ -965,8 +980,8 @@ func (s *Staker) handleConflict(ctx context.Context, info *StakerInfo) error { newChallengeManager, err := NewChallengeManager( ctx, - s.builder, - s.builder.BuilderAuth(), + s.client, + s.builder.Auth(context.TODO()), *s.builder.WalletAddress(), s.wallet.ChallengeManagerAddress(), *info.CurrentChallenge, @@ -1024,11 +1039,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv // We'll return early if we already have a stake if info.StakeExists { - auth, err := s.builder.Auth(ctx) - if err != nil { - return err - } - _, err = s.rollup.StakeOnNewNode(auth, action.assertion.AsSolidityStruct(), action.hash, action.prevInboxMaxCount) + _, err = s.rollup.StakeOnNewNode(s.builder.Auth(ctx), action.assertion.AsSolidityStruct(), action.hash, action.prevInboxMaxCount) if err != nil { return fmt.Errorf("error staking on new node: %w", err) } @@ -1040,12 +1051,8 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv if err != nil { return fmt.Errorf("error getting current required stake: %w", err) } - auth, err := s.builder.AuthWithAmount(ctx, stakeAmount) - if err != nil { - return err - } _, err = s.rollup.NewStakeOnNewNode( - auth, + s.builder.AuthWithAmount(ctx, stakeAmount), action.assertion.AsSolidityStruct(), action.hash, action.prevInboxMaxCount, @@ -1078,11 +1085,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv log.Info("staking on existing node", "node", action.number) // We'll return early if we already havea stake if info.StakeExists { - auth, err := s.builder.Auth(ctx) - if err != nil { - return err - } - _, err = s.rollup.StakeOnExistingNode(auth, action.number, action.hash) + _, err = s.rollup.StakeOnExistingNode(s.builder.Auth(ctx), action.number, action.hash) if err != nil { return fmt.Errorf("error staking on existing node: %w", err) } @@ -1094,12 +1097,8 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv if err != nil { return fmt.Errorf("error getting current required stake: %w", err) } - auth, err := s.builder.AuthWithAmount(ctx, stakeAmount) - if err != nil { - return err - } _, err = s.rollup.NewStakeOnExistingNode( - auth, + s.builder.AuthWithAmount(ctx, stakeAmount), action.number, action.hash, ) @@ -1113,7 +1112,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv } } -func (s *Staker) createConflict(ctx context.Context, info *StakerInfo) error { +func (s *Staker) createConflict(ctx context.Context, info *staker.StakerInfo) error { if info.CurrentChallenge != nil { return nil } @@ -1175,12 +1174,8 @@ func (s *Staker) createConflict(ctx context.Context, info *StakerInfo) error { return fmt.Errorf("error looking up node %v: %w", conflictInfo.Node2, err) } log.Warn("creating challenge", "node1", conflictInfo.Node1, "node2", conflictInfo.Node2, "otherStaker", staker) - auth, err := s.builder.Auth(ctx) - if err != nil { - return err - } _, err = s.rollup.CreateChallenge( - auth, + s.builder.Auth(ctx), [2]common.Address{staker1, staker2}, [2]uint64{conflictInfo.Node1, conflictInfo.Node2}, node1Info.MachineStatuses(), @@ -1199,10 +1194,10 @@ func (s *Staker) createConflict(ctx context.Context, info *StakerInfo) error { } func (s *Staker) Strategy() StakerStrategy { - return s.config().strategy + return s.config().StrategyType() } -func (s *Staker) Rollup() *RollupWatcher { +func (s *Staker) Rollup() *staker.RollupWatcher { return s.rollup } @@ -1214,7 +1209,7 @@ func (s *Staker) updateStakerBalanceMetric(ctx context.Context) { } balance, err := s.client.BalanceAt(ctx, *txSenderAddress, nil) if err != nil { - log.Error("error getting staker balance", "txSenderAddress", *txSenderAddress, "err", err) + log.Warn("error getting staker balance", "txSenderAddress", *txSenderAddress, "err", err) return } stakerBalanceGauge.Update(arbmath.BalancePerEther(balance)) diff --git a/staker/multi_protocol/multi_protocol_staker.go b/staker/multi_protocol/multi_protocol_staker.go new file mode 100644 index 0000000000..0c104094ed --- /dev/null +++ b/staker/multi_protocol/multi_protocol_staker.go @@ -0,0 +1,249 @@ +package multiprotocolstaker + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + + "github.com/offchainlabs/bold/solgen/go/bridgegen" + boldrollup "github.com/offchainlabs/bold/solgen/go/rollupgen" + "github.com/offchainlabs/nitro/staker" + boldstaker "github.com/offchainlabs/nitro/staker/bold" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" + "github.com/offchainlabs/nitro/staker/txbuilder" + "github.com/offchainlabs/nitro/util/headerreader" + "github.com/offchainlabs/nitro/util/stopwaiter" +) + +const boldArt = ` + _______ __ _______ +/ \ / | / \ +$$$$$$$ | ______ $$ | $$$$$$$ | +$$ |__$$ | / \ $$ | $$ | $$ | +$$ $$< /$$$$$$ |$$ | $$ | $$ | +$$$$$$$ |$$ | $$ |$$ | $$ | $$ | +$$ |__$$ |$$ \__$$ |$$ |_____ $$ |__$$ | +$$ $$/ $$ $$/ $$ |$$ $$/ +$$$$$$$/ $$$$$$/ $$$$$$$$/ $$$$$$$/ +` + +type MultiProtocolStaker struct { + stopwaiter.StopWaiter + bridge *bridgegen.IBridge + oldStaker *legacystaker.Staker + boldStaker *boldstaker.BOLDStaker + legacyConfig legacystaker.L1ValidatorConfigFetcher + stakedNotifiers []legacystaker.LatestStakedNotifier + confirmedNotifiers []legacystaker.LatestConfirmedNotifier + statelessBlockValidator *staker.StatelessBlockValidator + wallet legacystaker.ValidatorWalletInterface + l1Reader *headerreader.HeaderReader + blockValidator *staker.BlockValidator + callOpts bind.CallOpts + boldConfig *boldstaker.BoldConfig + stakeTokenAddress common.Address + stack *node.Node +} + +func NewMultiProtocolStaker( + stack *node.Node, + l1Reader *headerreader.HeaderReader, + wallet legacystaker.ValidatorWalletInterface, + callOpts bind.CallOpts, + legacyConfig legacystaker.L1ValidatorConfigFetcher, + boldConfig *boldstaker.BoldConfig, + blockValidator *staker.BlockValidator, + statelessBlockValidator *staker.StatelessBlockValidator, + stakedNotifiers []legacystaker.LatestStakedNotifier, + stakeTokenAddress common.Address, + confirmedNotifiers []legacystaker.LatestConfirmedNotifier, + validatorUtilsAddress common.Address, + bridgeAddress common.Address, + fatalErr chan<- error, +) (*MultiProtocolStaker, error) { + if err := legacyConfig().Validate(); err != nil { + return nil, err + } + if legacyConfig().StartValidationFromStaked && blockValidator != nil { + stakedNotifiers = append(stakedNotifiers, blockValidator) + } + oldStaker, err := legacystaker.NewStaker( + l1Reader, + wallet, + callOpts, + legacyConfig, + blockValidator, + statelessBlockValidator, + stakedNotifiers, + confirmedNotifiers, + validatorUtilsAddress, + fatalErr, + ) + if err != nil { + return nil, err + } + bridge, err := bridgegen.NewIBridge(bridgeAddress, l1Reader.Client()) + if err != nil { + return nil, err + } + return &MultiProtocolStaker{ + oldStaker: oldStaker, + boldStaker: nil, + bridge: bridge, + legacyConfig: legacyConfig, + stakedNotifiers: stakedNotifiers, + confirmedNotifiers: confirmedNotifiers, + statelessBlockValidator: statelessBlockValidator, + wallet: wallet, + l1Reader: l1Reader, + blockValidator: blockValidator, + callOpts: callOpts, + boldConfig: boldConfig, + stakeTokenAddress: stakeTokenAddress, + stack: stack, + }, nil +} + +func (m *MultiProtocolStaker) Initialize(ctx context.Context) error { + boldActive, rollupAddress, err := m.isBoldActive(ctx) + if err != nil { + return err + } + if boldActive { + log.Info("BoLD protocol is active, initializing BoLD staker") + log.Info(boldArt) + if err := m.setupBoldStaker(ctx, rollupAddress); err != nil { + return err + } + m.oldStaker = nil + return m.boldStaker.Initialize(ctx) + } + log.Info("BoLD protocol not detected on startup, using old staker until upgrade") + return m.oldStaker.Initialize(ctx) +} + +func (m *MultiProtocolStaker) Start(ctxIn context.Context) { + m.StopWaiter.Start(ctxIn, m) + m.wallet.Start(ctxIn) + if m.boldStaker != nil { + log.Info("Starting BOLD staker") + m.boldStaker.Start(ctxIn) + } else { + log.Info("Starting pre-BOLD staker") + m.oldStaker.Start(ctxIn) + stakerSwitchInterval := m.boldConfig.CheckStakerSwitchInterval + m.CallIteratively(func(ctx context.Context) time.Duration { + switchedToBoldProtocol, err := m.checkAndSwitchToBoldStaker(ctxIn) + if err != nil { + log.Warn("staker: error in checking switch to bold staker", "err", err) + return stakerSwitchInterval + } + if switchedToBoldProtocol { + log.Info("Detected BOLD protocol upgrade, stopping old staker and starting BOLD staker") + // Ready to stop the old staker. + m.oldStaker.StopOnly() + m.StopOnly() + } + return stakerSwitchInterval + }) + } +} + +func (m *MultiProtocolStaker) StopAndWait() { + if m.boldStaker != nil { + m.boldStaker.StopAndWait() + } + if m.oldStaker != nil { + m.oldStaker.StopAndWait() + } + m.StopWaiter.StopAndWait() +} + +func (m *MultiProtocolStaker) isBoldActive(ctx context.Context) (bool, common.Address, error) { + var addr common.Address + if !m.boldConfig.Enable { + return false, addr, nil + } + callOpts := m.getCallOpts(ctx) + rollupAddress, err := m.bridge.Rollup(callOpts) + if err != nil { + return false, addr, err + } + userLogic, err := boldrollup.NewRollupUserLogic(rollupAddress, m.l1Reader.Client()) + if err != nil { + return false, addr, err + } + _, err = userLogic.ChallengeGracePeriodBlocks(callOpts) + if err != nil && !headerreader.ExecutionRevertedRegexp.MatchString(err.Error()) { + // Unexpected error, perhaps an L1 issue? + return false, addr, err + } + // ChallengeGracePeriodBlocks only exists in the BOLD rollup contracts. + return err == nil, rollupAddress, nil +} + +func (m *MultiProtocolStaker) checkAndSwitchToBoldStaker(ctx context.Context) (bool, error) { + shouldSwitch, rollupAddress, err := m.isBoldActive(ctx) + if err != nil { + return false, err + } + if !shouldSwitch { + return false, nil + } + if err := m.setupBoldStaker(ctx, rollupAddress); err != nil { + return false, err + } + if err = m.boldStaker.Initialize(ctx); err != nil { + return false, err + } + m.boldStaker.Start(ctx) + return true, nil +} + +func (m *MultiProtocolStaker) getCallOpts(ctx context.Context) *bind.CallOpts { + opts := m.callOpts + opts.Context = ctx + return &opts +} + +func (m *MultiProtocolStaker) setupBoldStaker( + ctx context.Context, + rollupAddress common.Address, +) error { + stakeTokenContract, err := m.l1Reader.Client().CodeAt(ctx, m.stakeTokenAddress, nil) + if err != nil { + return err + } + if len(stakeTokenContract) == 0 { + return fmt.Errorf("stake token address for BoLD %v does not point to a contract", m.stakeTokenAddress) + } + txBuilder, err := txbuilder.NewBuilder(m.wallet, m.legacyConfig().GasRefunder()) + if err != nil { + return err + } + boldStaker, err := boldstaker.NewBOLDStaker( + ctx, + m.stack, + rollupAddress, + m.callOpts, + txBuilder.SingleTxAuth(), + m.l1Reader, + m.blockValidator, + m.statelessBlockValidator, + m.boldConfig, + m.wallet.DataPoster(), + m.wallet, + m.stakedNotifiers, + m.confirmedNotifiers, + ) + if err != nil { + return err + } + m.boldStaker = boldStaker + return nil +} diff --git a/staker/rollup_watcher.go b/staker/rollup_watcher.go index b35bebd1c6..b117b30c2b 100644 --- a/staker/rollup_watcher.go +++ b/staker/rollup_watcher.go @@ -4,23 +4,26 @@ package staker import ( + "bytes" "context" "encoding/binary" "errors" "fmt" "math/big" + "strings" "sync/atomic" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/util/headerreader" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/core/types" ) var rollupInitializedID common.Hash @@ -48,12 +51,19 @@ type RollupWatcher struct { *rollupgen.RollupUserLogic address common.Address fromBlock *big.Int - client arbutil.L1Interface + client RollupWatcherL1Interface baseCallOpts bind.CallOpts unSupportedL3Method atomic.Bool + supportedL3Method atomic.Bool +} + +type RollupWatcherL1Interface interface { + bind.ContractBackend + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) + FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) } -func NewRollupWatcher(address common.Address, client arbutil.L1Interface, callOpts bind.CallOpts) (*RollupWatcher, error) { +func NewRollupWatcher(address common.Address, client RollupWatcherL1Interface, callOpts bind.CallOpts) (*RollupWatcher, error) { con, err := rollupgen.NewRollupUserLogic(address, client) if err != nil { return nil, err @@ -73,15 +83,41 @@ func (r *RollupWatcher) getCallOpts(ctx context.Context) *bind.CallOpts { return &opts } +const noNodeErr string = "NO_NODE" + +func looksLikeNoNodeError(err error) bool { + if err == nil { + return false + } + if strings.Contains(err.Error(), noNodeErr) { + return true + } + var errWithData rpc.DataError + ok := errors.As(err, &errWithData) + if !ok { + return false + } + dataString, ok := errWithData.ErrorData().(string) + if !ok { + return false + } + data := common.FromHex(dataString) + return bytes.Contains(data, []byte(noNodeErr)) +} + func (r *RollupWatcher) getNodeCreationBlock(ctx context.Context, nodeNum uint64) (*big.Int, error) { callOpts := r.getCallOpts(ctx) if !r.unSupportedL3Method.Load() { createdAtBlock, err := r.GetNodeCreationBlockForLogLookup(callOpts, nodeNum) if err == nil { + r.supportedL3Method.Store(true) return createdAtBlock, nil } - log.Trace("failed to call getNodeCreationBlockForLogLookup, falling back on node CreatedAtBlock field", "err", err) - if headerreader.ExecutionRevertedRegexp.MatchString(err.Error()) { + if headerreader.ExecutionRevertedRegexp.MatchString(err.Error()) && !looksLikeNoNodeError(err) { + if r.supportedL3Method.Load() { + return nil, fmt.Errorf("getNodeCreationBlockForLogLookup failed despite previously succeeding: %w", err) + } + log.Info("getNodeCreationBlockForLogLookup does not seem to exist, falling back on node CreatedAtBlock field", "err", err) r.unSupportedL3Method.Store(true) } else { return nil, err @@ -101,6 +137,10 @@ func (r *RollupWatcher) Initialize(ctx context.Context) error { return err } +func (r *RollupWatcher) Client() RollupWatcherL1Interface { + return r.client +} + func (r *RollupWatcher) LookupCreation(ctx context.Context) (*rollupgen.RollupUserLogicRollupInitialized, error) { var query = ethereum.FilterQuery{ FromBlock: r.fromBlock, @@ -196,7 +236,7 @@ func (r *RollupWatcher) LookupNodeChildren(ctx context.Context, nodeNum uint64, if logQueryRangeSize == 0 { query.ToBlock = toBlock } else { - query.ToBlock = new(big.Int).Add(fromBlock, big.NewInt(int64(logQueryRangeSize))) + query.ToBlock = new(big.Int).Add(fromBlock, new(big.Int).SetUint64(logQueryRangeSize)) } if query.ToBlock.Cmp(toBlock) > 0 { query.ToBlock = toBlock diff --git a/staker/stateless_block_validator.go b/staker/stateless_block_validator.go index d5eeb8eb69..62e772d5f8 100644 --- a/staker/stateless_block_validator.go +++ b/staker/stateless_block_validator.go @@ -9,23 +9,22 @@ import ( "fmt" "testing" - "github.com/offchainlabs/nitro/arbstate/daprovider" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/execution" "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/validator" - "github.com/offchainlabs/nitro/validator/client/redis" - validatorclient "github.com/offchainlabs/nitro/validator/client" + "github.com/offchainlabs/nitro/validator/client/redis" + "github.com/offchainlabs/nitro/validator/server_api" ) type StatelessBlockValidator struct { @@ -41,6 +40,7 @@ type StatelessBlockValidator struct { streamer TransactionStreamerInterface db ethdb.Database dapReaders []daprovider.Reader + stack *node.Node } type BlockValidatorRegistrer interface { @@ -68,6 +68,7 @@ type TransactionStreamerInterface interface { type InboxReaderInterface interface { GetSequencerMessageBytes(ctx context.Context, seqNum uint64) ([]byte, common.Hash, error) + GetFinalizedMsgCount(ctx context.Context) (arbutil.MessageIndex, error) } type GlobalStatePosition struct { @@ -115,6 +116,13 @@ const ( Ready ) +type FullBatchInfo struct { + Number uint64 + PostedData []byte + MsgCount arbutil.MessageIndex + Preimages map[arbutil.PreimageType]map[common.Hash][]byte +} + type validationEntry struct { Stage ValidationEntryStage // Valid since ReadyforRecord: @@ -134,7 +142,7 @@ type validationEntry struct { DelayedMsg []byte } -func (e *validationEntry) ToInput(stylusArchs []rawdb.Target) (*validator.ValidationInput, error) { +func (e *validationEntry) ToInput(stylusArchs []ethdb.WasmTarget) (*validator.ValidationInput, error) { if e.Stage != Ready { return nil, errors.New("cannot create input from non-ready entry") } @@ -143,7 +151,7 @@ func (e *validationEntry) ToInput(stylusArchs []rawdb.Target) (*validator.Valida HasDelayedMsg: e.HasDelayedMsg, DelayedMsgNr: e.DelayedMsgNr, Preimages: e.Preimages, - UserWasms: make(map[rawdb.Target]map[common.Hash][]byte, len(e.UserWasms)), + UserWasms: make(map[ethdb.WasmTarget]map[common.Hash][]byte, len(e.UserWasms)), BatchInfo: e.BatchInfo, DelayedMsg: e.DelayedMsg, StartState: e.Start, @@ -172,16 +180,28 @@ func newValidationEntry( start validator.GoGlobalState, end validator.GoGlobalState, msg *arbostypes.MessageWithMetadata, - batch []byte, - batchBlockHash common.Hash, + fullBatchInfo *FullBatchInfo, + prevBatches []validator.BatchInfo, prevDelayed uint64, chainConfig *params.ChainConfig, ) (*validationEntry, error) { - batchInfo := validator.BatchInfo{ - Number: start.Batch, - BlockHash: batchBlockHash, - Data: batch, + preimages := make(map[arbutil.PreimageType]map[common.Hash][]byte) + if fullBatchInfo == nil { + return nil, fmt.Errorf("fullbatchInfo cannot be nil") } + if fullBatchInfo.Number != start.Batch { + return nil, fmt.Errorf("got wrong batch expected: %d got: %d", start.Batch, fullBatchInfo.Number) + } + valBatches := []validator.BatchInfo{ + { + Number: fullBatchInfo.Number, + Data: fullBatchInfo.PostedData, + }, + } + valBatches = append(valBatches, prevBatches...) + + copyPreimagesInto(preimages, fullBatchInfo.Preimages) + hasDelayed := false var delayedNum uint64 if msg.DelayedMessagesRead == prevDelayed+1 { @@ -190,6 +210,7 @@ func newValidationEntry( } else if msg.DelayedMessagesRead != prevDelayed { return nil, fmt.Errorf("illegal validation entry delayedMessage %d, previous %d", msg.DelayedMessagesRead, prevDelayed) } + return &validationEntry{ Stage: ReadyForRecord, Pos: pos, @@ -198,8 +219,9 @@ func newValidationEntry( HasDelayedMsg: hasDelayed, DelayedMsgNr: delayedNum, msg: msg, - BatchInfo: []validator.BatchInfo{batchInfo}, + BatchInfo: valBatches, ChainConfig: chainConfig, + Preimages: preimages, }, nil } @@ -244,33 +266,104 @@ func NewStatelessBlockValidator( db: arbdb, dapReaders: dapReaders, execSpawners: executionSpawners, + stack: stack, }, nil } -func (v *StatelessBlockValidator) readBatch(ctx context.Context, batchNum uint64) (bool, []byte, common.Hash, arbutil.MessageIndex, error) { +func (v *StatelessBlockValidator) readPostedBatch(ctx context.Context, batchNum uint64) ([]byte, error) { + batchCount, err := v.inboxTracker.GetBatchCount() + if err != nil { + return nil, err + } + if batchCount <= batchNum { + return nil, fmt.Errorf("batch not found: %d", batchNum) + } + postedData, _, err := v.inboxReader.GetSequencerMessageBytes(ctx, batchNum) + return postedData, err +} + +func (v *StatelessBlockValidator) InboxTracker() InboxTrackerInterface { + return v.inboxTracker +} + +func (v *StatelessBlockValidator) InboxReader() InboxReaderInterface { + return v.inboxReader +} + +func (v *StatelessBlockValidator) InboxStreamer() TransactionStreamerInterface { + return v.streamer +} + +func (v *StatelessBlockValidator) ExecutionSpawners() []validator.ExecutionSpawner { + return v.execSpawners +} + +func (v *StatelessBlockValidator) readFullBatch(ctx context.Context, batchNum uint64) (bool, *FullBatchInfo, error) { batchCount, err := v.inboxTracker.GetBatchCount() if err != nil { - return false, nil, common.Hash{}, 0, err + return false, nil, err } if batchCount <= batchNum { - return false, nil, common.Hash{}, 0, nil + return false, nil, nil } batchMsgCount, err := v.inboxTracker.GetBatchMessageCount(batchNum) if err != nil { - return false, nil, common.Hash{}, 0, err + return false, nil, err } - batch, batchBlockHash, err := v.inboxReader.GetSequencerMessageBytes(ctx, batchNum) + postedData, batchBlockHash, err := v.inboxReader.GetSequencerMessageBytes(ctx, batchNum) if err != nil { - return false, nil, common.Hash{}, 0, err + return false, nil, err + } + preimages := make(map[arbutil.PreimageType]map[common.Hash][]byte) + if len(postedData) > 40 { + foundDA := false + for _, dapReader := range v.dapReaders { + if dapReader != nil && dapReader.IsValidHeaderByte(postedData[40]) { + preimageRecorder := daprovider.RecordPreimagesTo(preimages) + _, err := dapReader.RecoverPayloadFromBatch(ctx, batchNum, batchBlockHash, postedData, preimageRecorder, true) + if err != nil { + // Matches the way keyset validation was done inside DAS readers i.e logging the error + // But other daproviders might just want to return the error + if errors.Is(err, daprovider.ErrSeqMsgValidation) && daprovider.IsDASMessageHeaderByte(postedData[40]) { + log.Error(err.Error()) + } else { + return false, nil, err + } + } + foundDA = true + break + } + } + if !foundDA { + if daprovider.IsDASMessageHeaderByte(postedData[40]) { + log.Error("No DAS Reader configured, but sequencer message found with DAS header") + } + } + } + fullInfo := FullBatchInfo{ + Number: batchNum, + PostedData: postedData, + MsgCount: batchMsgCount, + Preimages: preimages, + } + return true, &fullInfo, nil +} + +func copyPreimagesInto(dest, source map[arbutil.PreimageType]map[common.Hash][]byte) { + for piType, piMap := range source { + if dest[piType] == nil { + dest[piType] = make(map[common.Hash][]byte, len(piMap)) + } + for hash, preimage := range piMap { + dest[piType][hash] = preimage + } } - return true, batch, batchBlockHash, batchMsgCount, nil } func (v *StatelessBlockValidator) ValidationEntryRecord(ctx context.Context, e *validationEntry) error { if e.Stage != ReadyForRecord { return fmt.Errorf("validation entry should be ReadyForRecord, is: %v", e.Stage) } - e.Preimages = make(map[arbutil.PreimageType]map[common.Hash][]byte) if e.Pos != 0 { recording, err := v.recorder.RecordBlockCreation(ctx, e.Pos, e.msg) if err != nil { @@ -279,30 +372,11 @@ func (v *StatelessBlockValidator) ValidationEntryRecord(ctx context.Context, e * if recording.BlockHash != e.End.BlockHash { return fmt.Errorf("recording failed: pos %d, hash expected %v, got %v", e.Pos, e.End.BlockHash, recording.BlockHash) } - // record any additional batch fetching - batchFetcher := func(batchNum uint64) ([]byte, error) { - found, data, hash, _, err := v.readBatch(ctx, batchNum) - if err != nil { - return nil, err - } - if !found { - return nil, errors.New("batch not found") - } - e.BatchInfo = append(e.BatchInfo, validator.BatchInfo{ - Number: batchNum, - BlockHash: hash, - Data: data, - }) - return data, nil - } - e.msg.Message.BatchGasCost = nil - err = e.msg.Message.FillInBatchGasCost(batchFetcher) - if err != nil { - return err - } - if recording.Preimages != nil { - e.Preimages[arbutil.Keccak256PreimageType] = recording.Preimages + recordingPreimages := map[arbutil.PreimageType]map[common.Hash][]byte{ + arbutil.Keccak256PreimageType: recording.Preimages, + } + copyPreimagesInto(e.Preimages, recordingPreimages) } e.UserWasms = recording.UserWasms } @@ -317,41 +391,12 @@ func (v *StatelessBlockValidator) ValidationEntryRecord(ctx context.Context, e * } e.DelayedMsg = delayedMsg } - for _, batch := range e.BatchInfo { - if len(batch.Data) <= 40 { - continue - } - foundDA := false - for _, dapReader := range v.dapReaders { - if dapReader != nil && dapReader.IsValidHeaderByte(batch.Data[40]) { - preimageRecorder := daprovider.RecordPreimagesTo(e.Preimages) - _, err := dapReader.RecoverPayloadFromBatch(ctx, batch.Number, batch.BlockHash, batch.Data, preimageRecorder, true) - if err != nil { - // Matches the way keyset validation was done inside DAS readers i.e logging the error - // But other daproviders might just want to return the error - if errors.Is(err, daprovider.ErrSeqMsgValidation) && daprovider.IsDASMessageHeaderByte(batch.Data[40]) { - log.Error(err.Error()) - } else { - return err - } - } - foundDA = true - break - } - } - if !foundDA { - if daprovider.IsDASMessageHeaderByte(batch.Data[40]) { - log.Error("No DAS Reader configured, but sequencer message found with DAS header") - } - } - } - e.msg = nil // no longer needed e.Stage = Ready return nil } -func buildGlobalState(res execution.MessageResult, pos GlobalStatePosition) validator.GoGlobalState { +func BuildGlobalState(res execution.MessageResult, pos GlobalStatePosition) validator.GoGlobalState { return validator.GoGlobalState{ BlockHash: res.BlockHash, SendRoot: res.SendRoot, @@ -403,13 +448,32 @@ func (v *StatelessBlockValidator) CreateReadyValidationEntry(ctx context.Context if err != nil { return nil, fmt.Errorf("failed calculating position for validation: %w", err) } - start := buildGlobalState(*prevResult, startPos) - end := buildGlobalState(*result, endPos) - seqMsg, batchBlockHash, err := v.inboxReader.GetSequencerMessageBytes(ctx, startPos.BatchNumber) + start := BuildGlobalState(*prevResult, startPos) + end := BuildGlobalState(*result, endPos) + found, fullBatchInfo, err := v.readFullBatch(ctx, start.Batch) if err != nil { return nil, err } - entry, err := newValidationEntry(pos, start, end, msg, seqMsg, batchBlockHash, prevDelayed, v.streamer.ChainConfig()) + if !found { + return nil, fmt.Errorf("batch %d not found", startPos.BatchNumber) + } + + prevBatchNums, err := msg.Message.PastBatchesRequired() + if err != nil { + return nil, err + } + prevBatches := make([]validator.BatchInfo, 0, len(prevBatchNums)) + for _, batchNum := range prevBatchNums { + data, err := v.readPostedBatch(ctx, batchNum) + if err != nil { + return nil, err + } + prevBatches = append(prevBatches, validator.BatchInfo{ + Number: batchNum, + Data: data, + }) + } + entry, err := newValidationEntry(pos, start, end, msg, fullBatchInfo, prevBatches, prevDelayed, v.streamer.ChainConfig()) if err != nil { return nil, err } @@ -463,6 +527,18 @@ func (v *StatelessBlockValidator) ValidateResult( return true, &entry.End, nil } +func (v *StatelessBlockValidator) ValidationInputsAt(ctx context.Context, pos arbutil.MessageIndex, targets ...ethdb.WasmTarget) (server_api.InputJSON, error) { + entry, err := v.CreateReadyValidationEntry(ctx, pos) + if err != nil { + return server_api.InputJSON{}, err + } + input, err := entry.ToInput(targets) + if err != nil { + return server_api.InputJSON{}, err + } + return *server_api.ValidationInputToJson(input), nil +} + func (v *StatelessBlockValidator) OverrideRecorder(t *testing.T, recorder execution.ExecutionRecorder) { v.recorder = recorder } diff --git a/staker/txbuilder/builder.go b/staker/txbuilder/builder.go index 9a5e9df2b5..b352036c7a 100644 --- a/staker/txbuilder/builder.go +++ b/staker/txbuilder/builder.go @@ -5,59 +5,97 @@ package txbuilder import ( "context" + "fmt" "math/big" + "sync" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/offchainlabs/nitro/arbutil" ) type ValidatorWalletInterface interface { // Address must be able to be called concurrently with other functions Address() *common.Address - L1Client() arbutil.L1Interface - TestTransactions(context.Context, []*types.Transaction) error - ExecuteTransactions(context.Context, *Builder, common.Address) (*types.Transaction, error) + TestTransactions(ctx context.Context, txs []*types.Transaction) error + ExecuteTransactions(ctx context.Context, txs []*types.Transaction, gasRefunder common.Address) (*types.Transaction, error) AuthIfEoa() *bind.TransactOpts } -// Builder combines any transactions sent to it via SendTransaction into one batch, +// Builder combines any transactions signed via it into one batch, // which is then sent to the validator wallet. // This lets the validator make multiple atomic transactions. -// This inherits from an eth client so it can be used as an L1Interface, -// where it transparently intercepts calls to SendTransaction and queues them for the next batch. type Builder struct { - arbutil.L1Interface transactions []*types.Transaction - builderAuth *bind.TransactOpts + singleTxAuth bind.TransactOpts + multiTxAuth bind.TransactOpts isAuthFake bool + authMutex sync.Mutex wallet ValidatorWalletInterface + gasRefunder common.Address } -func NewBuilder(wallet ValidatorWalletInterface) (*Builder, error) { - randKey, err := crypto.GenerateKey() - if err != nil { - return nil, err - } - builderAuth := wallet.AuthIfEoa() +func NewBuilder(wallet ValidatorWalletInterface, gasRefunder common.Address) (*Builder, error) { + var builderAuth bind.TransactOpts var isAuthFake bool - if builderAuth == nil { - // Make a fake auth so we have txs to give to the smart contract wallet - builderAuth, err = bind.NewKeyedTransactorWithChainID(randKey, big.NewInt(9999999)) + if authIfEoa := wallet.AuthIfEoa(); authIfEoa != nil { + builderAuth = *authIfEoa + } else { + isAuthFake = true + var addressOrZero common.Address + if addr := wallet.Address(); addr != nil { + addressOrZero = *addr + } + builderAuth = bind.TransactOpts{ + From: addressOrZero, + GasLimit: 123, // don't gas estimate, that's done when the real tx is created + Signer: func(_ common.Address, tx *types.Transaction) (*types.Transaction, error) { + return tx, nil + }, + } + } + builderAuth.NoSend = true + builder := &Builder{ + singleTxAuth: builderAuth, + multiTxAuth: builderAuth, + wallet: wallet, + isAuthFake: isAuthFake, + gasRefunder: gasRefunder, + } + originalSigner := builderAuth.Signer + builder.multiTxAuth.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + tx, err := originalSigner(addr, tx) if err != nil { return nil, err } - isAuthFake = true + // Append the transaction to the builder's queue of transactions + builder.transactions = append(builder.transactions, tx) + err = builder.wallet.TestTransactions(context.TODO(), builder.transactions) + if err != nil { + // Remove the bad tx + builder.transactions = builder.transactions[:len(builder.transactions)-1] + return nil, err + } + return tx, nil + } + builder.singleTxAuth.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + if !isAuthFake { + return originalSigner(addr, tx) + } + // Try to process the transaction on its own + ctx := context.TODO() + txs := []*types.Transaction{tx} + err := builder.wallet.TestTransactions(ctx, txs) + if err != nil { + return nil, fmt.Errorf("failed to test builder transaction: %w", err) + } + signedTx, err := builder.wallet.ExecuteTransactions(ctx, txs, gasRefunder) + if err != nil { + return nil, fmt.Errorf("failed to execute builder transaction: %w", err) + } + return signedTx, nil } - return &Builder{ - builderAuth: builderAuth, - wallet: wallet, - L1Interface: wallet.L1Client(), - isAuthFake: isAuthFake, - }, nil + return builder, nil } func (b *Builder) BuildingTransactionCount() int { @@ -68,59 +106,45 @@ func (b *Builder) ClearTransactions() { b.transactions = nil } -func (b *Builder) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - if len(b.transactions) == 0 && !b.isAuthFake { - return b.L1Interface.EstimateGas(ctx, call) +func (b *Builder) tryToFillAuthAddress() { + if b.multiTxAuth.From == (common.Address{}) { + if addr := b.wallet.Address(); addr != nil { + b.multiTxAuth.From = *addr + b.singleTxAuth.From = *addr + } } - return 0, nil } -func (b *Builder) SendTransaction(ctx context.Context, tx *types.Transaction) error { - b.transactions = append(b.transactions, tx) - err := b.wallet.TestTransactions(ctx, b.transactions) - if err != nil { - // Remove the bad tx - b.transactions = b.transactions[:len(b.transactions)-1] - return err - } - return nil -} - -// While this is not currently required, it's recommended not to reuse the returned auth for multiple transactions, -// as for an EOA this has the nonce in it. However, the EOA wwallet currently will only publish the first created tx, -// which is why that doesn't really matter. -func (b *Builder) AuthWithAmount(ctx context.Context, amount *big.Int) (*bind.TransactOpts, error) { - nonce, err := b.NonceAt(ctx, b.builderAuth.From, nil) - if err != nil { - return nil, err - } - return &bind.TransactOpts{ - From: b.builderAuth.From, - Nonce: new(big.Int).SetUint64(nonce), - Signer: b.builderAuth.Signer, - Value: amount, - GasPrice: b.builderAuth.GasPrice, - GasLimit: b.builderAuth.GasLimit, - Context: ctx, - }, nil +func (b *Builder) AuthWithAmount(ctx context.Context, amount *big.Int) *bind.TransactOpts { + b.authMutex.Lock() + defer b.authMutex.Unlock() + b.tryToFillAuthAddress() + auth := b.multiTxAuth + auth.Context = ctx + auth.Value = amount + return &auth } // Auth is the same as AuthWithAmount with a 0 amount specified. -// See AuthWithAmount docs for important details. -func (b *Builder) Auth(ctx context.Context) (*bind.TransactOpts, error) { +func (b *Builder) Auth(ctx context.Context) *bind.TransactOpts { return b.AuthWithAmount(ctx, common.Big0) } -func (b *Builder) Transactions() []*types.Transaction { - return b.transactions -} - -// Auth is the same as AuthWithAmount with a 0 amount specified. -// See AuthWithAmount docs for important details. -func (b *Builder) BuilderAuth() *bind.TransactOpts { - return b.builderAuth +// SingleTxAuth should be used if you need an auth without the transaction batching of the builder. +func (b *Builder) SingleTxAuth() *bind.TransactOpts { + b.authMutex.Lock() + defer b.authMutex.Unlock() + b.tryToFillAuthAddress() + auth := b.singleTxAuth + return &auth } func (b *Builder) WalletAddress() *common.Address { return b.wallet.Address() } + +func (b *Builder) ExecuteTransactions(ctx context.Context) (*types.Transaction, error) { + tx, err := b.wallet.ExecuteTransactions(ctx, b.transactions, b.gasRefunder) + b.ClearTransactions() + return tx, err +} diff --git a/staker/validatorwallet/contract.go b/staker/validatorwallet/contract.go index 77b403b669..22f579b82d 100644 --- a/staker/validatorwallet/contract.go +++ b/staker/validatorwallet/contract.go @@ -9,6 +9,7 @@ import ( "fmt" "math/big" "strings" + "sync" "sync/atomic" "github.com/ethereum/go-ethereum" @@ -16,18 +17,21 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbnode/dataposter" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/rollupgen" - "github.com/offchainlabs/nitro/staker/txbuilder" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/headerreader" ) -var validatorABI abi.ABI -var walletCreatedID common.Hash +var ( + validatorABI abi.ABI + validatorWalletCreatorABI abi.ABI + walletCreatedID common.Hash +) func init() { parsedValidator, err := abi.JSON(strings.NewReader(rollupgen.ValidatorWalletABI)) @@ -40,6 +44,7 @@ func init() { if err != nil { panic(err) } + validatorWalletCreatorABI = parsedValidatorWalletCreator walletCreatedID = parsedValidatorWalletCreator.Events["WalletCreated"].ID } @@ -56,6 +61,7 @@ type Contract struct { challengeManagerAddress common.Address dataPoster *dataposter.DataPoster getExtraGas func() uint64 + populateWalletMutex sync.Mutex } func NewContract(dp *dataposter.DataPoster, address *common.Address, walletFactoryAddr, rollupAddress common.Address, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts, rollupFromBlock int64, onWalletCreated func(common.Address), @@ -150,36 +156,51 @@ func (v *Contract) From() common.Address { return v.auth.From } -// nil value == 0 value -func (v *Contract) getAuth(ctx context.Context, value *big.Int) (*bind.TransactOpts, error) { - newAuth := *v.auth - newAuth.Context = ctx - newAuth.Value = value - nonce, err := v.L1Client().NonceAt(ctx, v.auth.From, nil) +func (v *Contract) executeTransaction(ctx context.Context, tx *types.Transaction, gasRefunder common.Address) (*types.Transaction, error) { + data, err := validatorABI.Pack("executeTransactionWithGasRefunder", gasRefunder, tx.Data(), *tx.To(), tx.Value()) if err != nil { - return nil, err + return nil, fmt.Errorf("packing arguments for executeTransactionWithGasRefunder: %w", err) } - newAuth.Nonce = new(big.Int).SetUint64(nonce) - return &newAuth, nil + gas, err := v.gasForTxData(ctx, data, tx.Value()) + if err != nil { + return nil, fmt.Errorf("getting gas for tx data: %w", err) + } + return v.dataPoster.PostSimpleTransaction(ctx, *v.Address(), data, gas, tx.Value()) } -func (v *Contract) executeTransaction(ctx context.Context, tx *types.Transaction, gasRefunder common.Address) (*types.Transaction, error) { - auth, err := v.getAuth(ctx, tx.Value()) +func createWalletContract( + ctx context.Context, + l1Reader *headerreader.HeaderReader, + from common.Address, + dataPoster *dataposter.DataPoster, + getExtraGas func() uint64, + validatorWalletFactoryAddr common.Address, +) (*types.Transaction, error) { + var initialExecutorAllowedDests []common.Address + txData, err := validatorWalletCreatorABI.Pack("createWallet", initialExecutorAllowedDests) if err != nil { return nil, err } - data, err := validatorABI.Pack("executeTransactionWithGasRefunder", gasRefunder, tx.Data(), *tx.To(), tx.Value()) - if err != nil { - return nil, fmt.Errorf("packing arguments for executeTransactionWithGasRefunder: %w", err) - } - gas, err := v.gasForTxData(ctx, auth, data) + + gas, err := gasForTxData( + ctx, + l1Reader, + from, + &validatorWalletFactoryAddr, + txData, + common.Big0, + getExtraGas, + ) if err != nil { - return nil, fmt.Errorf("getting gas for tx data: %w", err) + return nil, fmt.Errorf("getting gas for tx data when creating validator wallet, validatorWalletFactory=%v: %w", validatorWalletFactoryAddr, err) } - return v.dataPoster.PostSimpleTransaction(ctx, auth.Nonce.Uint64(), *v.Address(), data, gas, auth.Value) + + return dataPoster.PostSimpleTransaction(ctx, validatorWalletFactoryAddr, txData, gas, common.Big0) } func (v *Contract) populateWallet(ctx context.Context, createIfMissing bool) error { + v.populateWalletMutex.Lock() + defer v.populateWalletMutex.Unlock() if v.con != nil { return nil } @@ -190,11 +211,10 @@ func (v *Contract) populateWallet(ctx context.Context, createIfMissing bool) err return nil } if v.address.Load() == nil { - auth, err := v.getAuth(ctx, nil) - if err != nil { - return err - } - addr, err := GetValidatorWalletContract(ctx, v.walletFactoryAddr, v.rollupFromBlock, auth, v.l1Reader, createIfMissing) + // By passing v.dataPoster as a parameter to GetValidatorWalletContract we force to create a validator wallet through the Staker's DataPoster object. + // DataPoster keeps in its internal state information related to the transactions sent through it, which is used to infer the expected nonce in a transaction for example. + // If a transaction is sent using the Staker's DataPoster key, but not through the Staker's DataPoster object, DataPoster's internal state will be outdated, which can compromise the expected nonce inference. + addr, err := GetValidatorWalletContract(ctx, v.walletFactoryAddr, v.rollupFromBlock, v.l1Reader, createIfMissing, v.dataPoster, v.getExtraGas) if err != nil { return err } @@ -233,9 +253,7 @@ func combineTxes(txes []*types.Transaction) ([][]byte, []common.Address, []*big. return data, dest, amount, totalAmount } -// Not thread safe! Don't call this from multiple threads at the same time. -func (v *Contract) ExecuteTransactions(ctx context.Context, builder *txbuilder.Builder, gasRefunder common.Address) (*types.Transaction, error) { - txes := builder.Transactions() +func (v *Contract) ExecuteTransactions(ctx context.Context, txes []*types.Transaction, gasRefunder common.Address) (*types.Transaction, error) { if len(txes) == 0 { return nil, nil } @@ -250,7 +268,6 @@ func (v *Contract) ExecuteTransactions(ctx context.Context, builder *txbuilder.B if err != nil { return nil, err } - builder.ClearTransactions() return arbTx, nil } @@ -275,44 +292,39 @@ func (v *Contract) ExecuteTransactions(ctx context.Context, builder *txbuilder.B if callValue.Sign() < 0 { callValue.SetInt64(0) } - auth, err := v.getAuth(ctx, callValue) - if err != nil { - return nil, err - } txData, err := validatorABI.Pack("executeTransactionsWithGasRefunder", gasRefunder, data, dest, amount) if err != nil { return nil, fmt.Errorf("packing arguments for executeTransactionWithGasRefunder: %w", err) } - gas, err := v.gasForTxData(ctx, auth, txData) + gas, err := v.gasForTxData(ctx, txData, callValue) if err != nil { return nil, fmt.Errorf("getting gas for tx data: %w", err) } - arbTx, err := v.dataPoster.PostSimpleTransaction(ctx, auth.Nonce.Uint64(), *v.Address(), txData, gas, auth.Value) + arbTx, err := v.dataPoster.PostSimpleTransaction(ctx, *v.Address(), txData, gas, callValue) if err != nil { return nil, err } - builder.ClearTransactions() return arbTx, nil } -func (v *Contract) estimateGas(ctx context.Context, value *big.Int, data []byte) (uint64, error) { - h, err := v.l1Reader.LastHeader(ctx) +func gasForTxData(ctx context.Context, l1Reader *headerreader.HeaderReader, from common.Address, to *common.Address, data []byte, value *big.Int, getExtraGas func() uint64) (uint64, error) { + h, err := l1Reader.LastHeader(ctx) if err != nil { return 0, fmt.Errorf("getting the last header: %w", err) } gasFeeCap := new(big.Int).Mul(h.BaseFee, big.NewInt(2)) gasFeeCap = arbmath.BigMax(gasFeeCap, arbmath.FloatToBig(params.GWei)) - gasTipCap, err := v.l1Reader.Client().SuggestGasTipCap(ctx) + gasTipCap, err := l1Reader.Client().SuggestGasTipCap(ctx) if err != nil { return 0, fmt.Errorf("getting suggested gas tip cap: %w", err) } gasFeeCap.Add(gasFeeCap, gasTipCap) - g, err := v.l1Reader.Client().EstimateGas( + g, err := l1Reader.Client().EstimateGas( ctx, ethereum.CallMsg{ - From: v.auth.From, - To: v.Address(), + From: from, + To: to, Value: value, Data: data, GasFeeCap: gasFeeCap, @@ -322,34 +334,26 @@ func (v *Contract) estimateGas(ctx context.Context, value *big.Int, data []byte) if err != nil { return 0, fmt.Errorf("estimating gas: %w", err) } - return g + v.getExtraGas(), nil + return g + getExtraGas(), nil +} + +func (v *Contract) gasForTxData(ctx context.Context, data []byte, value *big.Int) (uint64, error) { + return gasForTxData(ctx, v.l1Reader, v.From(), v.Address(), data, value, v.getExtraGas) } func (v *Contract) TimeoutChallenges(ctx context.Context, challenges []uint64) (*types.Transaction, error) { - auth, err := v.getAuth(ctx, nil) - if err != nil { - return nil, err - } data, err := validatorABI.Pack("timeoutChallenges", v.challengeManagerAddress, challenges) if err != nil { return nil, fmt.Errorf("packing arguments for timeoutChallenges: %w", err) } - gas, err := v.gasForTxData(ctx, auth, data) + gas, err := v.gasForTxData(ctx, data, common.Big0) if err != nil { return nil, fmt.Errorf("getting gas for tx data: %w", err) } - return v.dataPoster.PostSimpleTransaction(ctx, auth.Nonce.Uint64(), *v.Address(), data, gas, auth.Value) -} - -// gasForTxData returns auth.GasLimit if it's nonzero, otherwise returns estimate. -func (v *Contract) gasForTxData(ctx context.Context, auth *bind.TransactOpts, data []byte) (uint64, error) { - if auth.GasLimit != 0 { - return auth.GasLimit, nil - } - return v.estimateGas(ctx, auth.Value, data) + return v.dataPoster.PostSimpleTransaction(ctx, *v.Address(), data, gas, common.Big0) } -func (v *Contract) L1Client() arbutil.L1Interface { +func (v *Contract) L1Client() *ethclient.Client { return v.l1Reader.Client() } @@ -400,15 +404,22 @@ func (b *Contract) DataPoster() *dataposter.DataPoster { return b.dataPoster } +// Exported for testing +func (b *Contract) GetExtraGas() func() uint64 { + return b.getExtraGas +} + func GetValidatorWalletContract( ctx context.Context, validatorWalletFactoryAddr common.Address, fromBlock int64, - transactAuth *bind.TransactOpts, l1Reader *headerreader.HeaderReader, createIfMissing bool, + dataPoster *dataposter.DataPoster, + getExtraGas func() uint64, ) (*common.Address, error) { client := l1Reader.Client() + transactAuth := dataPoster.Auth() // TODO: If we just save a mapping in the wallet creator we won't need log search walletCreator, err := rollupgen.NewValidatorWalletCreator(validatorWalletFactoryAddr, client) @@ -443,8 +454,7 @@ func GetValidatorWalletContract( return nil, nil } - var initialExecutorAllowedDests []common.Address - tx, err := walletCreator.CreateWallet(transactAuth, initialExecutorAllowedDests) + tx, err := createWalletContract(ctx, l1Reader, transactAuth.From, dataPoster, getExtraGas, validatorWalletFactoryAddr) if err != nil { return nil, err } diff --git a/staker/validatorwallet/eoa.go b/staker/validatorwallet/eoa.go index 3ae305b36c..80b805b396 100644 --- a/staker/validatorwallet/eoa.go +++ b/staker/validatorwallet/eoa.go @@ -10,16 +10,19 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/offchainlabs/nitro/arbnode/dataposter" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/challengegen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" - "github.com/offchainlabs/nitro/staker/txbuilder" ) +// EOA is a ValidatorWallet that uses an Externally Owned Account to sign transactions. +// An Ethereum Externally Owned Account is directly represented by a private key, +// as opposed to a smart contract wallet where the smart contract authorizes transactions. type EOA struct { auth *bind.TransactOpts - client arbutil.L1Interface + client *ethclient.Client rollupAddress common.Address challengeManager *challengegen.ChallengeManager challengeManagerAddress common.Address @@ -27,7 +30,7 @@ type EOA struct { getExtraGas func() uint64 } -func NewEOA(dataPoster *dataposter.DataPoster, rollupAddress common.Address, l1Client arbutil.L1Interface, getExtraGas func() uint64) (*EOA, error) { +func NewEOA(dataPoster *dataposter.DataPoster, rollupAddress common.Address, l1Client *ethclient.Client, getExtraGas func() uint64) (*EOA, error) { return &EOA{ auth: dataPoster.Auth(), client: l1Client, @@ -63,7 +66,7 @@ func (w *EOA) TxSenderAddress() *common.Address { return &w.auth.From } -func (w *EOA) L1Client() arbutil.L1Interface { +func (w *EOA) L1Client() *ethclient.Client { return w.client } @@ -80,21 +83,17 @@ func (w *EOA) TestTransactions(context.Context, []*types.Transaction) error { return nil } -func (w *EOA) ExecuteTransactions(ctx context.Context, builder *txbuilder.Builder, _ common.Address) (*types.Transaction, error) { - if len(builder.Transactions()) == 0 { +func (w *EOA) ExecuteTransactions(ctx context.Context, txes []*types.Transaction, _ common.Address) (*types.Transaction, error) { + if len(txes) == 0 { return nil, nil } - tx := builder.Transactions()[0] // we ignore future txs and only execute the first + tx := txes[0] // we ignore future txs and only execute the first return w.postTransaction(ctx, tx) } func (w *EOA) postTransaction(ctx context.Context, baseTx *types.Transaction) (*types.Transaction, error) { - nonce, err := w.L1Client().NonceAt(ctx, w.auth.From, nil) - if err != nil { - return nil, err - } gas := baseTx.Gas() + w.getExtraGas() - newTx, err := w.dataPoster.PostSimpleTransaction(ctx, nonce, *baseTx.To(), baseTx.Data(), gas, baseTx.Value()) + newTx, err := w.dataPoster.PostSimpleTransaction(ctx, *baseTx.To(), baseTx.Data(), gas, baseTx.Value()) if err != nil { return nil, fmt.Errorf("post transaction: %w", err) } diff --git a/staker/validatorwallet/noop.go b/staker/validatorwallet/noop.go index b050ebe861..b483927753 100644 --- a/staker/validatorwallet/noop.go +++ b/staker/validatorwallet/noop.go @@ -10,18 +10,18 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/offchainlabs/nitro/arbnode/dataposter" - "github.com/offchainlabs/nitro/arbutil" - "github.com/offchainlabs/nitro/staker/txbuilder" ) // NoOp validator wallet is used for watchtower mode. type NoOp struct { - l1Client arbutil.L1Interface + l1Client *ethclient.Client rollupAddress common.Address } -func NewNoOp(l1Client arbutil.L1Interface, rollupAddress common.Address) *NoOp { +func NewNoOp(l1Client *ethclient.Client, rollupAddress common.Address) *NoOp { return &NoOp{ l1Client: l1Client, rollupAddress: rollupAddress, @@ -38,7 +38,7 @@ func (*NoOp) TxSenderAddress() *common.Address { return nil } func (*NoOp) From() common.Address { return common.Address{} } -func (*NoOp) ExecuteTransactions(context.Context, *txbuilder.Builder, common.Address) (*types.Transaction, error) { +func (*NoOp) ExecuteTransactions(context.Context, []*types.Transaction, common.Address) (*types.Transaction, error) { return nil, errors.New("no op validator wallet cannot execute transactions") } @@ -46,7 +46,7 @@ func (*NoOp) TimeoutChallenges(ctx context.Context, challenges []uint64) (*types return nil, errors.New("no op validator wallet cannot timeout challenges") } -func (n *NoOp) L1Client() arbutil.L1Interface { return n.l1Client } +func (n *NoOp) L1Client() *ethclient.Client { return n.l1Client } func (n *NoOp) RollupAddress() common.Address { return n.rollupAddress } diff --git a/statetransfer/data.go b/statetransfer/data.go index df4694aa17..21268a443a 100644 --- a/statetransfer/data.go +++ b/statetransfer/data.go @@ -14,6 +14,7 @@ type ArbosInitializationInfo struct { AddressTableContents []common.Address RetryableData []InitializationDataForRetryable Accounts []AccountInitializationInfo + ChainOwner common.Address } type InitializationDataForRetryable struct { diff --git a/statetransfer/interface.go b/statetransfer/interface.go index 7d592b4430..cb70fdd14d 100644 --- a/statetransfer/interface.go +++ b/statetransfer/interface.go @@ -17,6 +17,7 @@ type InitDataReader interface { GetNextBlockNumber() (uint64, error) GetRetryableDataReader() (RetryableDataReader, error) GetAccountDataReader() (AccountDataReader, error) + GetChainOwner() (common.Address, error) } type ListReader interface { diff --git a/statetransfer/jsondatareader.go b/statetransfer/jsondatareader.go index c36061c0b0..5e992df3f0 100644 --- a/statetransfer/jsondatareader.go +++ b/statetransfer/jsondatareader.go @@ -210,3 +210,7 @@ func (r *JsonInitDataReader) GetAccountDataReader() (AccountDataReader, error) { JsonListReader: listreader, }, nil } + +func (r *JsonInitDataReader) GetChainOwner() (common.Address, error) { + return common.Address{}, nil +} diff --git a/statetransfer/memdatareader.go b/statetransfer/memdatareader.go index 1d60888937..3d6b68343c 100644 --- a/statetransfer/memdatareader.go +++ b/statetransfer/memdatareader.go @@ -99,6 +99,10 @@ func (r *MemoryInitDataReader) GetAccountDataReader() (AccountDataReader, error) }, nil } +func (r *MemoryInitDataReader) GetChainOwner() (common.Address, error) { + return r.d.ChainOwner, nil +} + func (r *MemoryInitDataReader) Close() error { return nil } diff --git a/system_tests/aliasing_test.go b/system_tests/aliasing_test.go index 60a89468a5..e6c9dab45f 100644 --- a/system_tests/aliasing_test.go +++ b/system_tests/aliasing_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" diff --git a/system_tests/arbos_upgrade_test.go b/system_tests/arbos_upgrade_test.go new file mode 100644 index 0000000000..a7103a8585 --- /dev/null +++ b/system_tests/arbos_upgrade_test.go @@ -0,0 +1,271 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package arbtest + +import ( + "context" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" +) + +func TestScheduleArbosUpgrade(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(common.HexToAddress("0x6b"), builder.L2.Client) + Require(t, err, "could not bind ArbOwner contract") + + arbOwner, err := precompilesgen.NewArbOwner(common.HexToAddress("0x70"), builder.L2.Client) + Require(t, err, "could not bind ArbOwner contract") + + callOpts := &bind.CallOpts{Context: ctx} + scheduled, err := arbOwnerPublic.GetScheduledUpgrade(callOpts) + Require(t, err, "failed to call GetScheduledUpgrade before scheduling upgrade") + if scheduled.ArbosVersion != 0 || scheduled.ScheduledForTimestamp != 0 { + t.Errorf("expected no upgrade to be scheduled, got version %v timestamp %v", scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) + } + + // Schedule a noop upgrade, which should test GetScheduledUpgrade in the same way an already completed upgrade would. + tx, err := arbOwner.ScheduleArbOSUpgrade(&auth, 1, 1) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + scheduled, err = arbOwnerPublic.GetScheduledUpgrade(callOpts) + Require(t, err, "failed to call GetScheduledUpgrade after scheduling noop upgrade") + if scheduled.ArbosVersion != 0 || scheduled.ScheduledForTimestamp != 0 { + t.Errorf("expected completed scheduled upgrade to be ignored, got version %v timestamp %v", scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) + } + + // We can't test 11 -> 20 because 11 doesn't have the GetScheduledUpgrade method we want to test + var testVersion uint64 = 100 + var testTimestamp uint64 = 1 << 62 + tx, err = arbOwner.ScheduleArbOSUpgrade(&auth, 100, 1<<62) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + scheduled, err = arbOwnerPublic.GetScheduledUpgrade(callOpts) + Require(t, err, "failed to call GetScheduledUpgrade after scheduling upgrade") + if scheduled.ArbosVersion != testVersion || scheduled.ScheduledForTimestamp != testTimestamp { + t.Errorf("expected upgrade to be scheduled for version %v timestamp %v, got version %v timestamp %v", testVersion, testTimestamp, scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) + } +} + +func checkArbOSVersion(t *testing.T, testClient *TestClient, expectedVersion uint64, scenario string) { + statedb, err := testClient.ExecNode.Backend.ArbInterface().BlockChain().State() + Require(t, err, "could not get statedb", scenario) + state, err := arbosState.OpenSystemArbosState(statedb, nil, true) + Require(t, err, "could not open ArbOS state", scenario) + if state.ArbOSVersion() != expectedVersion { + t.Errorf("%s: expected ArbOS version %v, got %v", scenario, expectedVersion, state.ArbOSVersion()) + } + +} + +func TestArbos11To32UpgradeWithMcopy(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + initialVersion := uint64(11) + finalVersion := uint64(32) + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithArbOSVersion(initialVersion) + cleanup := builder.Build(t) + defer cleanup() + seqTestClient := builder.L2 + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + auth.GasLimit = 32000000 + + // makes Owner a chain owner + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, seqTestClient.Client) + Require(t, err) + tx, err := arbDebug.BecomeChainOwner(&auth) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, seqTestClient.Client, tx) + Require(t, err) + + // deploys test contract + _, tx, contract, err := mocksgen.DeployArbOS11To32UpgradeTest(&auth, seqTestClient.Client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, seqTestClient.Client, tx) + Require(t, err) + + // build replica node + replicaConfig := arbnode.ConfigDefaultL1Test() + replicaConfig.BatchPoster.Enable = false + replicaTestClient, replicaCleanup := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: replicaConfig}) + defer replicaCleanup() + + checkArbOSVersion(t, seqTestClient, initialVersion, "initial sequencer") + checkArbOSVersion(t, replicaTestClient, initialVersion, "initial replica") + + // mcopy should fail since arbos 11 doesn't support it + tx, err = contract.Mcopy(&auth) + Require(t, err) + _, err = seqTestClient.EnsureTxSucceeded(tx) + if (err == nil) || !strings.Contains(err.Error(), "invalid opcode: MCOPY") { + t.Errorf("expected MCOPY to fail, got %v", err) + } + _, err = WaitForTx(ctx, replicaTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + + // upgrade arbos to final version + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, seqTestClient.Client) + Require(t, err) + tx, err = arbOwner.ScheduleArbOSUpgrade(&auth, finalVersion, 0) + Require(t, err) + _, err = seqTestClient.EnsureTxSucceeded(tx) + Require(t, err) + _, err = WaitForTx(ctx, replicaTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + + // checks upgrade worked + tx, err = contract.Mcopy(&auth) + Require(t, err) + _, err = seqTestClient.EnsureTxSucceeded(tx) + Require(t, err) + _, err = WaitForTx(ctx, replicaTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + + checkArbOSVersion(t, seqTestClient, finalVersion, "final sequencer") + checkArbOSVersion(t, replicaTestClient, finalVersion, "final replica") + + // generates more blocks + builder.L2Info.GenerateAccount("User2") + for i := 0; i < 3; i++ { + tx = builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, big.NewInt(1e12), nil) + err = seqTestClient.Client.SendTransaction(ctx, tx) + Require(t, err) + _, err = seqTestClient.EnsureTxSucceeded(tx) + Require(t, err) + _, err = WaitForTx(ctx, replicaTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + } + + blockNumberSeq, err := seqTestClient.Client.BlockNumber(ctx) + Require(t, err) + blockNumberReplica, err := replicaTestClient.Client.BlockNumber(ctx) + Require(t, err) + if blockNumberSeq != blockNumberReplica { + t.Errorf("expected sequencer and replica to have same block number, got %v and %v", blockNumberSeq, blockNumberReplica) + } + // #nosec G115 + blockNumber := big.NewInt(int64(blockNumberSeq)) + + blockSeq, err := seqTestClient.Client.BlockByNumber(ctx, blockNumber) + Require(t, err) + blockReplica, err := replicaTestClient.Client.BlockByNumber(ctx, blockNumber) + Require(t, err) + if blockSeq.Hash() != blockReplica.Hash() { + t.Errorf("expected sequencer and replica to have same block hash, got %v and %v", blockSeq.Hash(), blockReplica.Hash()) + } +} + +func TestArbos11To32UpgradeWithCalldata(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + initialVersion := uint64(11) + finalVersion := uint64(32) + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithArbOSVersion(initialVersion) + builder.execConfig.TxPreChecker.Strictness = gethexec.TxPreCheckerStrictnessLikelyCompatible + cleanup := builder.Build(t) + defer cleanup() + seqTestClient := builder.L2 + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + auth.GasLimit = 32000000 + + // makes Owner a chain owner + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, seqTestClient.Client) + Require(t, err) + tx, err := arbDebug.BecomeChainOwner(&auth) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, seqTestClient.Client, tx) + Require(t, err) + + // build replica node + replicaConfig := arbnode.ConfigDefaultL1Test() + replicaConfig.BatchPoster.Enable = false + replicaTestClient, replicaCleanup := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: replicaConfig}) + defer replicaCleanup() + + checkArbOSVersion(t, seqTestClient, initialVersion, "initial sequencer") + checkArbOSVersion(t, replicaTestClient, initialVersion, "initial replica") + + // upgrade arbos to final version + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, seqTestClient.Client) + Require(t, err) + tx, err = arbOwner.ScheduleArbOSUpgrade(&auth, finalVersion, 0) + Require(t, err) + _, err = seqTestClient.EnsureTxSucceeded(tx) + Require(t, err) + _, err = WaitForTx(ctx, replicaTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + + // checks upgrade worked + var data []byte + for i := range 10 { + for range 100 { + data = append(data, byte(i)) + } + } + tx = builder.L2Info.PrepareTx("Owner", "Owner", builder.L2Info.TransferGas, big.NewInt(1e12), data) + err = seqTestClient.Client.SendTransaction(ctx, tx) + Require(t, err) + _, err = seqTestClient.EnsureTxSucceeded(tx) + Require(t, err) + _, err = WaitForTx(ctx, replicaTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + + checkArbOSVersion(t, seqTestClient, finalVersion, "final sequencer") + checkArbOSVersion(t, replicaTestClient, finalVersion, "final replica") + + blockNumberSeq, err := seqTestClient.Client.BlockNumber(ctx) + Require(t, err) + blockNumberReplica, err := replicaTestClient.Client.BlockNumber(ctx) + Require(t, err) + if blockNumberSeq != blockNumberReplica { + t.Errorf("expected sequencer and replica to have same block number, got %v and %v", blockNumberSeq, blockNumberReplica) + } + // #nosec G115 + blockNumber := big.NewInt(int64(blockNumberSeq)) + + blockSeq, err := seqTestClient.Client.BlockByNumber(ctx, blockNumber) + Require(t, err) + blockReplica, err := replicaTestClient.Client.BlockByNumber(ctx, blockNumber) + Require(t, err) + if blockSeq.Hash() != blockReplica.Hash() { + t.Errorf("expected sequencer and replica to have same block hash, got %v and %v", blockSeq.Hash(), blockReplica.Hash()) + } +} diff --git a/system_tests/batch_poster_test.go b/system_tests/batch_poster_test.go index 0ec03e84c4..ee0c3b4a3a 100644 --- a/system_tests/batch_poster_test.go +++ b/system_tests/batch_poster_test.go @@ -6,6 +6,7 @@ package arbtest import ( "context" "crypto/rand" + "errors" "fmt" "math/big" "strings" @@ -13,7 +14,9 @@ import ( "time" "github.com/andybalholm/brotli" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -363,3 +366,119 @@ func TestAllowPostingFirstBatchWhenSequencerMessageCountMismatchEnabled(t *testi func TestAllowPostingFirstBatchWhenSequencerMessageCountMismatchDisabled(t *testing.T) { testAllowPostingFirstBatchWhenSequencerMessageCountMismatch(t, false) } + +func GetBatchCount(t *testing.T, builder *NodeBuilder) uint64 { + t.Helper() + sequenceInbox, err := bridgegen.NewSequencerInbox(builder.L1Info.GetAddress("SequencerInbox"), builder.L1.Client) + Require(t, err) + batchCount, err := sequenceInbox.BatchCount(&bind.CallOpts{Context: builder.ctx}) + Require(t, err) + return batchCount.Uint64() +} + +func CheckBatchCount(t *testing.T, builder *NodeBuilder, want uint64) { + if got := GetBatchCount(t, builder); got != want { + t.Fatalf("invalid batch count, want %v, got %v", want, got) + } +} + +func testBatchPosterDelayBuffer(t *testing.T, delayBufferEnabled bool) { + const messagesPerBatch = 3 + const numBatches = 3 + var threshold uint64 + if delayBufferEnabled { + threshold = 100 + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithBoldDeployment(). + WithDelayBuffer(threshold) + builder.L2Info.GenerateAccount("User2") + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour // set high max-delay so we can test the delay buffer + cleanup := builder.Build(t) + defer cleanup() + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{}) + defer cleanupB() + + initialBatchCount := GetBatchCount(t, builder) + for batch := uint64(0); batch < numBatches; batch++ { + txs := make(types.Transactions, messagesPerBatch) + for i := range txs { + txs[i] = builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, common.Big1, nil) + } + SendSignedTxesInBatchViaL1(t, ctx, builder.L1Info, builder.L1.Client, builder.L2.Client, txs) + + // Check batch wasn't sent + _, err := WaitForTx(ctx, testClientB.Client, txs[0].Hash(), 100*time.Millisecond) + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + Fatal(t, "expected context-deadline exceeded error, but got:", err) + } + CheckBatchCount(t, builder, initialBatchCount+batch) + + // Advance L1 to force a batch given the delay buffer threshold + AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, int(threshold)) // #nosec G115 + if !delayBufferEnabled { + // If the delay buffer is disabled, set max delay to zero to force it + CheckBatchCount(t, builder, initialBatchCount+batch) + builder.nodeConfig.BatchPoster.MaxDelay = 0 + } + for _, tx := range txs { + _, err := testClientB.EnsureTxSucceeded(tx) + Require(t, err, "tx not found on second node") + } + CheckBatchCount(t, builder, initialBatchCount+batch+1) + if !delayBufferEnabled { + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour + } + } +} + +func TestBatchPosterDelayBufferEnabled(t *testing.T) { + testBatchPosterDelayBuffer(t, true) +} + +func TestBatchPosterDelayBufferDisabled(t *testing.T) { + testBatchPosterDelayBuffer(t, false) +} + +func TestBatchPosterDelayBufferDontForceNonDelayedMessages(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + const threshold = 100 + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithBoldDeployment(). + WithDelayBuffer(threshold) + builder.L2Info.GenerateAccount("User2") + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour // set high max-delay so we can test the delay buffer + cleanup := builder.Build(t) + defer cleanup() + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{}) + defer cleanupB() + + // Send non-delayed message and advance L1 + initialBatchCount := GetBatchCount(t, builder) + const numTxs = 3 + txs := make(types.Transactions, numTxs) + for i := range txs { + txs[i] = builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, common.Big1, nil) + } + builder.L2.SendWaitTestTransactions(t, txs) + AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, threshold) + + // Even advancing the L1, the batch won't be posted because it doesn't contain a delayed message + CheckBatchCount(t, builder, initialBatchCount) + + // Set delay to zero to force non-delayed messages + builder.nodeConfig.BatchPoster.MaxDelay = 0 + for _, tx := range txs { + _, err := testClientB.EnsureTxSucceeded(tx) + Require(t, err, "tx not found on second node") + } + CheckBatchCount(t, builder, initialBatchCount+1) +} diff --git a/system_tests/block_hash_test.go b/system_tests/block_hash_test.go index b437f3dad9..454b4359ad 100644 --- a/system_tests/block_hash_test.go +++ b/system_tests/block_hash_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" ) diff --git a/system_tests/block_validator_test.go b/system_tests/block_validator_test.go index bd0a1f3336..d6ae4973ac 100644 --- a/system_tests/block_validator_test.go +++ b/system_tests/block_validator_test.go @@ -19,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/l2pricing" @@ -58,12 +59,11 @@ func testBlockValidatorSimple(t *testing.T, opts Options) { chainConfig, l1NodeConfigA, lifecycleManager, _, dasSignerKey := setupConfigWithDAS(t, ctx, opts.dasModeString) defer lifecycleManager.StopAndWaitUntil(time.Second) if opts.workload == upgradeArbOs { - chainConfig.ArbitrumChainParams.InitialArbOSVersion = 10 + chainConfig.ArbitrumChainParams.InitialArbOSVersion = params.ArbosVersion_10 } var delayEvery int if opts.workloadLoops > 1 { - l1NodeConfigA.BatchPoster.MaxDelay = time.Millisecond * 500 delayEvery = opts.workloadLoops / 3 } @@ -203,8 +203,6 @@ func testBlockValidatorSimple(t *testing.T, opts Options) { builder.L1.SendWaitTestTransactions(t, []*types.Transaction{ WrapL2ForDelayed(t, delayedTx, builder.L1Info, "User", 100000), }) - // give the inbox reader a bit of time to pick up the delayed message - time.Sleep(time.Millisecond * 500) // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in for i := 0; i < 30; i++ { @@ -259,6 +257,7 @@ func testBlockValidatorSimple(t *testing.T, opts Options) { Require(t, err) // up to 3 extra references: awaiting validation, recently valid, lastValidatedHeader largestRefCount := lastBlockNow.NumberU64() - lastBlock.NumberU64() + 3 + // #nosec G115 if finalRefCount < 0 || finalRefCount > int64(largestRefCount) { Fatal(t, "unexpected refcount:", finalRefCount) } @@ -284,6 +283,20 @@ func TestBlockValidatorSimpleOnchain(t *testing.T) { testBlockValidatorSimple(t, opts) } +func TestBlockValidatorSimpleJITOnchainWithPublishedMachine(t *testing.T) { + cr, err := github.LatestConsensusRelease(context.Background()) + Require(t, err) + machPath := populateMachineDir(t, cr) + opts := Options{ + dasModeString: "onchain", + workloadLoops: 1, + workload: ethSend, + arbitrator: false, + wasmRootDir: machPath, + } + testBlockValidatorSimple(t, opts) +} + func TestBlockValidatorSimpleOnchainWithPublishedMachine(t *testing.T) { cr, err := github.LatestConsensusRelease(context.Background()) Require(t, err) diff --git a/system_tests/blocks_reexecutor_test.go b/system_tests/blocks_reexecutor_test.go index c6a7181c46..e9ef5a2260 100644 --- a/system_tests/blocks_reexecutor_test.go +++ b/system_tests/blocks_reexecutor_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + blocksreexecutor "github.com/offchainlabs/nitro/blocks_reexecutor" ) @@ -13,6 +15,7 @@ func TestBlocksReExecutorModes(t *testing.T) { defer cancel() builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + builder.execConfig.Caching.StateScheme = rawdb.HashScheme cleanup := builder.Build(t) defer cleanup() @@ -37,7 +40,8 @@ func TestBlocksReExecutorModes(t *testing.T) { // Reexecute blocks at mode full success := make(chan struct{}) - executorFull := blocksreexecutor.New(&blocksreexecutor.TestConfig, blockchain, feedErrChan) + executorFull, err := blocksreexecutor.New(&blocksreexecutor.TestConfig, blockchain, builder.L2.ExecNode.ChainDB, feedErrChan) + Require(t, err) executorFull.Start(ctx, success) select { case err := <-feedErrChan: @@ -49,7 +53,8 @@ func TestBlocksReExecutorModes(t *testing.T) { success = make(chan struct{}) c := &blocksreexecutor.TestConfig c.Mode = "random" - executorRandom := blocksreexecutor.New(c, blockchain, feedErrChan) + executorRandom, err := blocksreexecutor.New(c, blockchain, builder.L2.ExecNode.ChainDB, feedErrChan) + Require(t, err) executorRandom.Start(ctx, success) select { case err := <-feedErrChan: diff --git a/system_tests/bloom_test.go b/system_tests/bloom_test.go index a3cab748e2..df6c549dda 100644 --- a/system_tests/bloom_test.go +++ b/system_tests/bloom_test.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" ) @@ -48,11 +49,13 @@ func TestBloom(t *testing.T) { nullEventCounts := make(map[uint64]struct{}) for i := 0; i < eventsNum; i++ { + // #nosec G115 count := uint64(rand.Int() % countsNum) eventCounts[count] = struct{}{} } for i := 0; i < nullEventsNum; i++ { + // #nosec G115 count := uint64(rand.Int() % countsNum) nullEventCounts[count] = struct{}{} } @@ -60,6 +63,7 @@ func TestBloom(t *testing.T) { for i := 0; i <= countsNum; i++ { var tx *types.Transaction var err error + // #nosec G115 _, sendNullEvent := nullEventCounts[uint64(i)] if sendNullEvent { tx, err = simple.EmitNullEvent(&ownerTxOpts) @@ -68,6 +72,7 @@ func TestBloom(t *testing.T) { Require(t, err) } + // #nosec G115 _, sendEvent := eventCounts[uint64(i)] if sendEvent { tx, err = simple.IncrementEmit(&ownerTxOpts) @@ -86,7 +91,9 @@ func TestBloom(t *testing.T) { if sectionSize != 256 { Fatal(t, "unexpected section size: ", sectionSize) } + // #nosec G115 t.Log("sections: ", sectionNum, "/", uint64(countsNum)/sectionSize) + // #nosec G115 if sectionSize*(sectionNum+1) > uint64(countsNum) && sectionNum > 1 { break } diff --git a/system_tests/bold_challenge_protocol_test.go b/system_tests/bold_challenge_protocol_test.go new file mode 100644 index 0000000000..777817bf3e --- /dev/null +++ b/system_tests/bold_challenge_protocol_test.go @@ -0,0 +1,922 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +//go:build challengetest && !race + +package arbtest + +import ( + "bytes" + "context" + "encoding/json" + "io" + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + protocol "github.com/offchainlabs/bold/chain-abstraction" + solimpl "github.com/offchainlabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/offchainlabs/bold/challenge-manager" + modes "github.com/offchainlabs/bold/challenge-manager/types" + l2stateprovider "github.com/offchainlabs/bold/layer2-state-provider" + "github.com/offchainlabs/bold/solgen/go/bridgegen" + "github.com/offchainlabs/bold/solgen/go/challengeV2gen" + "github.com/offchainlabs/bold/solgen/go/mocksgen" + "github.com/offchainlabs/bold/solgen/go/rollupgen" + challengetesting "github.com/offchainlabs/bold/testing" + "github.com/offchainlabs/bold/testing/setup" + butil "github.com/offchainlabs/bold/util" + "github.com/offchainlabs/nitro/arbcompress" + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbnode/dataposter/storage" + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/arbstate" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/staker/bold" + "github.com/offchainlabs/nitro/statetransfer" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/util/signature" + "github.com/offchainlabs/nitro/util/testhelpers" + "github.com/offchainlabs/nitro/validator/server_arb" + "github.com/offchainlabs/nitro/validator/server_common" + "github.com/offchainlabs/nitro/validator/valnode" +) + +func TestChallengeProtocolBOLDReadInboxChallenge(t *testing.T) { + testChallengeProtocolBOLD(t) +} + +func TestChallengeProtocolBOLDStartStepChallenge(t *testing.T) { + opts := []server_arb.SpawnerOption{ + server_arb.WithWrapper(func(inner server_arb.MachineInterface) server_arb.MachineInterface { + // This wrapper is applied after the BOLD wrapper, so step 0 is the finished machine. + // Modifying its hash results in invalid inclusion proofs for the evil validator, + // so we start modifying hashes at step 1 (the first machine step in the running state). + return NewIncorrectIntermediateMachine(inner, 1) + }), + } + testChallengeProtocolBOLD(t, opts...) +} + +func testChallengeProtocolBOLD(t *testing.T, spawnerOpts ...server_arb.SpawnerOption) { + goodDir, err := os.MkdirTemp("", "good_*") + Require(t, err) + evilDir, err := os.MkdirTemp("", "evil_*") + Require(t, err) + t.Cleanup(func() { + Require(t, os.RemoveAll(goodDir)) + Require(t, os.RemoveAll(evilDir)) + }) + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + l2chainConfig := chaininfo.ArbitrumDevTestChainConfig() + l2info := NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(l2chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + ownerBal := big.NewInt(params.Ether) + ownerBal.Mul(ownerBal, big.NewInt(1_000_000)) + l2info.GenerateGenesisAccount("Owner", ownerBal) + sconf := setup.RollupStackConfig{ + UseMockBridge: false, + UseMockOneStepProver: false, + MinimumAssertionPeriod: 0, + } + + _, l2nodeA, _, _, l1info, _, l1client, l1stack, assertionChain, stakeTokenAddr := createTestNodeOnL1ForBoldProtocol( + t, + ctx, + true, + nil, + l2chainConfig, + nil, + sconf, + l2info, + ) + defer requireClose(t, l1stack) + defer l2nodeA.StopAndWait() + + // Make sure we shut down test functionality before the rest of the node + ctx, cancelCtx = context.WithCancel(ctx) + defer cancelCtx() + + go keepChainMoving(t, ctx, l1info, l1client) + + l2nodeConfig := arbnode.ConfigDefaultL1Test() + _, l2nodeB, _ := create2ndNodeWithConfigForBoldProtocol( + t, + ctx, + l2nodeA, + l1stack, + l1info, + &l2info.ArbInitData, + l2nodeConfig, + nil, + sconf, + stakeTokenAddr, + ) + defer l2nodeB.StopAndWait() + + genesisA, err := l2nodeA.Execution.ResultAtPos(0) + Require(t, err) + genesisB, err := l2nodeB.Execution.ResultAtPos(0) + Require(t, err) + if genesisA.BlockHash != genesisB.BlockHash { + Fatal(t, "genesis blocks mismatch between nodes") + } + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + TransferBalance(t, "Faucet", "Asserter", balance, l1info, l1client, ctx) + TransferBalance(t, "Faucet", "EvilAsserter", balance, l1info, l1client, ctx) + + valCfg := valnode.TestValidationConfig + valCfg.UseJit = false + _, valStack := createTestValidationNode(t, ctx, &valCfg) + blockValidatorConfig := staker.TestBlockValidatorConfig + + statelessA, err := staker.NewStatelessBlockValidator( + l2nodeA.InboxReader, + l2nodeA.InboxTracker, + l2nodeA.TxStreamer, + l2nodeA.Execution, + l2nodeA.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = statelessA.Start(ctx) + Require(t, err) + _, valStackB := createTestValidationNode(t, ctx, &valCfg, spawnerOpts...) + + statelessB, err := staker.NewStatelessBlockValidator( + l2nodeB.InboxReader, + l2nodeB.InboxTracker, + l2nodeB.TxStreamer, + l2nodeB.Execution, + l2nodeB.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStackB, + ) + Require(t, err) + err = statelessB.Start(ctx) + Require(t, err) + + blockValidatorA, err := staker.NewBlockValidator( + statelessA, + l2nodeA.InboxTracker, + l2nodeA.TxStreamer, + StaticFetcherFrom(t, &blockValidatorConfig), + nil, + ) + Require(t, err) + Require(t, blockValidatorA.Initialize(ctx)) + Require(t, blockValidatorA.Start(ctx)) + + blockValidatorB, err := staker.NewBlockValidator( + statelessB, + l2nodeB.InboxTracker, + l2nodeB.TxStreamer, + StaticFetcherFrom(t, &blockValidatorConfig), + nil, + ) + Require(t, err) + Require(t, blockValidatorB.Initialize(ctx)) + Require(t, blockValidatorB.Start(ctx)) + + stateManager, err := bold.NewBOLDStateProvider( + blockValidatorA, + statelessA, + l2stateprovider.Height(blockChallengeLeafHeight), + &bold.StateProviderConfig{ + ValidatorName: "good", + MachineLeavesCachePath: goodDir, + CheckBatchFinality: false, + }, + goodDir, + ) + Require(t, err) + + stateManagerB, err := bold.NewBOLDStateProvider( + blockValidatorB, + statelessB, + l2stateprovider.Height(blockChallengeLeafHeight), + &bold.StateProviderConfig{ + ValidatorName: "evil", + MachineLeavesCachePath: evilDir, + CheckBatchFinality: false, + }, + evilDir, + ) + Require(t, err) + + Require(t, l2nodeA.Start(ctx)) + Require(t, l2nodeB.Start(ctx)) + + chalManagerAddr := assertionChain.SpecChallengeManager() + evilOpts := l1info.GetDefaultTransactOpts("EvilAsserter", ctx) + l1ChainId, err := l1client.ChainID(ctx) + Require(t, err) + dp, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(l2nodeB.ArbDB, storage.StakerPrefix), + l2nodeB.L1Reader, + &evilOpts, + NewFetcherFromConfig(l2nodeConfig), + l2nodeB.SyncMonitor, + l1ChainId, + ) + Require(t, err) + chainB, err := solimpl.NewAssertionChain( + ctx, + assertionChain.RollupAddress(), + chalManagerAddr.Address(), + &evilOpts, + butil.NewBackendWrapper(l1client, rpc.LatestBlockNumber), + bold.NewDataPosterTransactor(dp), + solimpl.WithRpcHeadBlockNumber(rpc.LatestBlockNumber), + ) + Require(t, err) + + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + honestSeqInbox := l1info.GetAddress("SequencerInbox") + evilSeqInbox := l1info.GetAddress("EvilSequencerInbox") + honestSeqInboxBinding, err := bridgegen.NewSequencerInbox(honestSeqInbox, l1client) + Require(t, err) + evilSeqInboxBinding, err := bridgegen.NewSequencerInbox(evilSeqInbox, l1client) + Require(t, err) + + // Post batches to the honest and evil sequencer inbox that are internally equal. + // This means the honest and evil sequencer inboxes will agree with all messages in the batch. + seqInboxABI, err := abi.JSON(strings.NewReader(bridgegen.SequencerInboxABI)) + Require(t, err) + + honestUpgradeExec, err := mocksgen.NewUpgradeExecutorMock(l1info.GetAddress("UpgradeExecutor"), l1client) + Require(t, err) + data, err := seqInboxABI.Pack( + "setIsBatchPoster", + sequencerTxOpts.From, + true, + ) + Require(t, err) + honestRollupOwnerOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + _, err = honestUpgradeExec.ExecuteCall(&honestRollupOwnerOpts, honestSeqInbox, data) + Require(t, err) + + evilUpgradeExec, err := mocksgen.NewUpgradeExecutorMock(l1info.GetAddress("EvilUpgradeExecutor"), l1client) + Require(t, err) + data, err = seqInboxABI.Pack( + "setIsBatchPoster", + sequencerTxOpts.From, + true, + ) + Require(t, err) + evilRollupOwnerOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + _, err = evilUpgradeExec.ExecuteCall(&evilRollupOwnerOpts, evilSeqInbox, data) + Require(t, err) + + totalMessagesPosted := int64(0) + numMessagesPerBatch := int64(5) + divergeAt := int64(-1) + makeBoldBatch(t, l2nodeA, l2info, l1client, &sequencerTxOpts, honestSeqInboxBinding, honestSeqInbox, numMessagesPerBatch, divergeAt) + l2info.Accounts["Owner"].Nonce.Store(0) + makeBoldBatch(t, l2nodeB, l2info, l1client, &sequencerTxOpts, evilSeqInboxBinding, evilSeqInbox, numMessagesPerBatch, divergeAt) + totalMessagesPosted += numMessagesPerBatch + + // Next, we post another batch, this time containing more messages. + // We diverge at message index 5 within the evil node's batch. + l2info.Accounts["Owner"].Nonce.Store(5) + numMessagesPerBatch = int64(10) + makeBoldBatch(t, l2nodeA, l2info, l1client, &sequencerTxOpts, honestSeqInboxBinding, honestSeqInbox, numMessagesPerBatch, divergeAt) + l2info.Accounts["Owner"].Nonce.Store(5) + divergeAt = int64(5) + makeBoldBatch(t, l2nodeB, l2info, l1client, &sequencerTxOpts, evilSeqInboxBinding, evilSeqInbox, numMessagesPerBatch, divergeAt) + totalMessagesPosted += numMessagesPerBatch + + bcA, err := l2nodeA.InboxTracker.GetBatchCount() + Require(t, err) + bcB, err := l2nodeB.InboxTracker.GetBatchCount() + Require(t, err) + msgA, err := l2nodeA.InboxTracker.GetBatchMessageCount(bcA - 1) + Require(t, err) + msgB, err := l2nodeB.InboxTracker.GetBatchMessageCount(bcB - 1) + Require(t, err) + + t.Logf("Node A batch count %d, msgs %d", bcA, msgA) + t.Logf("Node B batch count %d, msgs %d", bcB, msgB) + + // Wait for both nodes' chains to catch up. + nodeAExec, ok := l2nodeA.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + nodeBExec, ok := l2nodeB.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + for { + nodeALatest := nodeAExec.Backend.APIBackend().CurrentHeader() + nodeBLatest := nodeBExec.Backend.APIBackend().CurrentHeader() + isCaughtUp := nodeALatest.Number.Uint64() == uint64(totalMessagesPosted) + areEqual := nodeALatest.Number.Uint64() == nodeBLatest.Number.Uint64() + if isCaughtUp && areEqual { + if nodeALatest.Hash() == nodeBLatest.Hash() { + Fatal(t, "node A L2 hash", nodeALatest, "matches node B L2 hash", nodeBLatest) + } + break + } + } + + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + + // Wait until the validators have validated the batches. + for { + lastInfo, err := blockValidatorA.ReadLastValidatedInfo() + if lastInfo == nil || err != nil { + continue + } + t.Log(lastInfo.GlobalState.Batch, totalBatches-1) + if lastInfo.GlobalState.Batch >= totalBatches-1 { + break + } + time.Sleep(time.Millisecond * 200) + } + for { + lastInfo, err := blockValidatorB.ReadLastValidatedInfo() + if lastInfo == nil || err != nil { + continue + } + t.Log(lastInfo.GlobalState.Batch, totalBatches-1) + if lastInfo.GlobalState.Batch >= totalBatches-1 { + break + } + time.Sleep(time.Millisecond * 200) + } + + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + stateManager, + nil, // Api db + ) + + evilProvider := l2stateprovider.NewHistoryCommitmentProvider( + stateManagerB, + stateManagerB, + stateManagerB, + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + stateManagerB, + nil, // Api db + ) + + stackOpts := []challengemanager.StackOpt{ + challengemanager.StackWithName("honest"), + challengemanager.StackWithMode(modes.MakeMode), + challengemanager.StackWithPostingInterval(time.Second * 3), + challengemanager.StackWithPollingInterval(time.Second), + challengemanager.StackWithAverageBlockCreationTime(time.Second), + } + + manager, err := challengemanager.NewChallengeStack( + assertionChain, + provider, + stackOpts..., + ) + Require(t, err) + + evilStackOpts := append(stackOpts, challengemanager.StackWithName("evil")) + + managerB, err := challengemanager.NewChallengeStack( + chainB, + evilProvider, + evilStackOpts..., + ) + Require(t, err) + + manager.Start(ctx) + managerB.Start(ctx) + + chalManager := assertionChain.SpecChallengeManager() + filterer, err := challengeV2gen.NewEdgeChallengeManagerFilterer(chalManager.Address(), l1client) + Require(t, err) + + fromBlock := uint64(0) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + latestBlock, err := l1client.HeaderByNumber(ctx, nil) + Require(t, err) + toBlock := latestBlock.Number.Uint64() + if fromBlock == toBlock { + continue + } + filterOpts := &bind.FilterOpts{ + Start: fromBlock, + End: &toBlock, + Context: ctx, + } + it, err := filterer.FilterEdgeConfirmedByOneStepProof(filterOpts, nil, nil) + Require(t, err) + for it.Next() { + if it.Error() != nil { + t.Fatalf("Error in filter iterator: %v", it.Error()) + } + t.Log("Received event of OSP confirmation!") + tx, _, err := l1client.TransactionByHash(ctx, it.Event.Raw.TxHash) + Require(t, err) + signer := types.NewCancunSigner(tx.ChainId()) + address, err := signer.Sender(tx) + Require(t, err) + if address == l1info.GetDefaultTransactOpts("Asserter", ctx).From { + t.Log("Honest party won OSP, impossible for evil party to win if honest party continues") + Require(t, it.Close()) + return + } + } + fromBlock = toBlock + case <-ctx.Done(): + return + } + } +} + +// Every 3 seconds, send an L1 transaction to keep the chain moving. +func keepChainMoving(t *testing.T, ctx context.Context, l1Info *BlockchainTestInfo, l1Client *ethclient.Client) { + delay := time.Second * 3 + for { + select { + case <-ctx.Done(): + return + default: + time.Sleep(delay) + if ctx.Err() != nil { + break + } + TransferBalance(t, "Faucet", "Faucet", common.Big0, l1Info, l1Client, ctx) + latestBlock, err := l1Client.BlockNumber(ctx) + if ctx.Err() != nil { + break + } + Require(t, err) + if latestBlock > 150 { + delay = time.Second + } + } + } +} + +func createTestNodeOnL1ForBoldProtocol( + t *testing.T, + ctx context.Context, + isSequencer bool, + nodeConfig *arbnode.Config, + chainConfig *params.ChainConfig, + _ *node.Config, + rollupStackConf setup.RollupStackConfig, + l2infoIn info, +) ( + l2info info, currentNode *arbnode.Node, l2client *ethclient.Client, l2stack *node.Node, + l1info info, l1backend *eth.Ethereum, l1client *ethclient.Client, l1stack *node.Node, + assertionChain *solimpl.AssertionChain, stakeTokenAddr common.Address, +) { + if nodeConfig == nil { + nodeConfig = arbnode.ConfigDefaultL1Test() + } + nodeConfig.ParentChainReader.OldHeaderTimeout = time.Minute * 10 + if chainConfig == nil { + chainConfig = chaininfo.ArbitrumDevTestChainConfig() + } + nodeConfig.BatchPoster.DataPoster.MaxMempoolTransactions = 18 + fatalErrChan := make(chan error, 10) + l1info, l1client, l1backend, l1stack = createTestL1BlockChain(t, nil) + var l2chainDb ethdb.Database + var l2arbDb ethdb.Database + var l2blockchain *core.BlockChain + l2info = l2infoIn + if l2info == nil { + l2info = NewArbTestInfo(t, chainConfig.ChainID) + } + + l1info.GenerateAccount("RollupOwner") + l1info.GenerateAccount("Sequencer") + l1info.GenerateAccount("User") + l1info.GenerateAccount("Asserter") + l1info.GenerateAccount("EvilAsserter") + + SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ + l1info.PrepareTx("Faucet", "RollupOwner", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "Sequencer", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "User", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "Asserter", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "EvilAsserter", 30000, big.NewInt(9223372036854775807), nil), + }) + + l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + stakeToken, tx, tokenBindings, err := mocksgen.DeployTestWETH9( + &l1TransactionOpts, + l1client, + "Weth", + "WETH", + ) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + stakeTokenAddr = stakeToken + value, ok := new(big.Int).SetString("10000", 10) + if !ok { + t.Fatal(t, "could not set value") + } + l1TransactionOpts.Value = value + tx, err = tokenBindings.Deposit(&l1TransactionOpts) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + l1TransactionOpts.Value = nil + + addresses := deployContractsOnly(t, ctx, l1info, l1client, chainConfig.ChainID, rollupStackConf, stakeToken) + rollupUser, err := rollupgen.NewRollupUserLogic(addresses.Rollup, l1client) + Require(t, err) + chalManagerAddr, err := rollupUser.ChallengeManager(&bind.CallOpts{}) + Require(t, err) + l1info.SetContract("Bridge", addresses.Bridge) + l1info.SetContract("SequencerInbox", addresses.SequencerInbox) + l1info.SetContract("Inbox", addresses.Inbox) + l1info.SetContract("Rollup", addresses.Rollup) + l1info.SetContract("UpgradeExecutor", addresses.UpgradeExecutor) + + execConfig := ExecConfigDefaultNonSequencerTest(t) + Require(t, execConfig.Validate()) + execConfig.Caching.StateScheme = rawdb.HashScheme + useWasmCache := uint32(1) + initMessage := getInitMessage(ctx, t, l1client, addresses) + _, l2stack, l2chainDb, l2arbDb, l2blockchain = createNonL1BlockChainWithStackConfig(t, l2info, "", chainConfig, initMessage, nil, execConfig, useWasmCache) + var sequencerTxOptsPtr *bind.TransactOpts + var dataSigner signature.DataSignerFunc + if isSequencer { + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + sequencerTxOptsPtr = &sequencerTxOpts + dataSigner = signature.DataSignerFromPrivateKey(l1info.GetInfoWithPrivKey("Sequencer").PrivateKey) + } + + if !isSequencer { + nodeConfig.BatchPoster.Enable = false + nodeConfig.DelayedSequencer.Enable = false + } + + AddValNodeIfNeeded(t, ctx, nodeConfig, true, "", "") + + execConfigFetcher := func() *gethexec.Config { return execConfig } + execNode, err := gethexec.CreateExecutionNode(ctx, l2stack, l2chainDb, l2blockchain, l1client, execConfigFetcher) + Require(t, err) + + parentChainId, err := l1client.ChainID(ctx) + Require(t, err) + currentNode, err = arbnode.CreateNode( + ctx, l2stack, execNode, l2arbDb, NewFetcherFromConfig(nodeConfig), l2blockchain.Config(), l1client, + addresses, sequencerTxOptsPtr, sequencerTxOptsPtr, dataSigner, fatalErrChan, parentChainId, + nil, // Blob reader. + ) + Require(t, err) + + l2client = ClientForStack(t, l2stack) + + StartWatchChanErr(t, ctx, fatalErrChan, currentNode) + + opts := l1info.GetDefaultTransactOpts("Asserter", ctx) + dp, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(l2arbDb, storage.StakerPrefix), + currentNode.L1Reader, + &opts, + NewFetcherFromConfig(nodeConfig), + currentNode.SyncMonitor, + parentChainId, + ) + Require(t, err) + assertionChainBindings, err := solimpl.NewAssertionChain( + ctx, + addresses.Rollup, + chalManagerAddr, + &opts, + butil.NewBackendWrapper(l1client, rpc.LatestBlockNumber), + bold.NewDataPosterTransactor(dp), + solimpl.WithRpcHeadBlockNumber(rpc.LatestBlockNumber), + ) + Require(t, err) + assertionChain = assertionChainBindings + + return +} + +func deployContractsOnly( + t *testing.T, + ctx context.Context, + l1info info, + backend *ethclient.Client, + chainId *big.Int, + rollupStackConf setup.RollupStackConfig, + stakeToken common.Address, +) *chaininfo.RollupAddresses { + l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + locator, err := server_common.NewMachineLocator("") + Require(t, err) + wasmModuleRoot := locator.LatestWasmModuleRoot() + + loserStakeEscrow := l1TransactionOpts.From + genesisExecutionState := rollupgen.AssertionState{ + GlobalState: rollupgen.GlobalState{}, + MachineStatus: 1, + EndHistoryRoot: [32]byte{}, + } + genesisInboxCount := big.NewInt(0) + anyTrustFastConfirmer := common.Address{} + miniStakeValues := []*big.Int{big.NewInt(5), big.NewInt(4), big.NewInt(3), big.NewInt(2), big.NewInt(1)} + cfg := challengetesting.GenerateRollupConfig( + false, + wasmModuleRoot, + l1TransactionOpts.From, + chainId, + loserStakeEscrow, + miniStakeValues, + stakeToken, + genesisExecutionState, + genesisInboxCount, + anyTrustFastConfirmer, + challengetesting.WithLayerZeroHeights(&protocol.LayerZeroHeights{ + BlockChallengeHeight: protocol.Height(blockChallengeLeafHeight), + BigStepChallengeHeight: protocol.Height(bigStepChallengeLeafHeight), + SmallStepChallengeHeight: protocol.Height(smallStepChallengeLeafHeight), + }), + challengetesting.WithNumBigStepLevels(uint8(3)), // TODO: Hardcoded. + challengetesting.WithConfirmPeriodBlocks(uint64(120)), // TODO: Hardcoded. + ) + config, err := json.Marshal(chaininfo.ArbitrumDevTestChainConfig()) + Require(t, err) + cfg.ChainConfig = string(config) + addresses, err := setup.DeployFullRollupStack( + ctx, + butil.NewBackendWrapper(backend, rpc.LatestBlockNumber), + &l1TransactionOpts, + l1info.GetAddress("Sequencer"), + cfg, + rollupStackConf, + ) + Require(t, err) + + asserter := l1info.GetDefaultTransactOpts("Asserter", ctx) + evilAsserter := l1info.GetDefaultTransactOpts("EvilAsserter", ctx) + userLogic, err := rollupgen.NewRollupUserLogic(addresses.Rollup, backend) + Require(t, err) + chalManagerAddr, err := userLogic.ChallengeManager(&bind.CallOpts{}) + Require(t, err) + seed, ok := new(big.Int).SetString("1000", 10) + if !ok { + t.Fatal("not ok") + } + value, ok := new(big.Int).SetString("10000", 10) + if !ok { + t.Fatal(t, "could not set value") + } + tokenBindings, err := mocksgen.NewTestWETH9(stakeToken, backend) + Require(t, err) + tx, err := tokenBindings.TestWETH9Transactor.Transfer(&l1TransactionOpts, asserter.From, seed) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&asserter, addresses.Rollup, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&asserter, chalManagerAddr, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + tx, err = tokenBindings.TestWETH9Transactor.Transfer(&l1TransactionOpts, evilAsserter.From, seed) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&evilAsserter, addresses.Rollup, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&evilAsserter, chalManagerAddr, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + return &chaininfo.RollupAddresses{ + Bridge: addresses.Bridge, + Inbox: addresses.Inbox, + SequencerInbox: addresses.SequencerInbox, + Rollup: addresses.Rollup, + ValidatorUtils: addresses.ValidatorUtils, + ValidatorWalletCreator: addresses.ValidatorWalletCreator, + DeployedAt: addresses.DeployedAt, + UpgradeExecutor: addresses.UpgradeExecutor, + } +} + +func create2ndNodeWithConfigForBoldProtocol( + t *testing.T, + ctx context.Context, + first *arbnode.Node, + l1stack *node.Node, + l1info *BlockchainTestInfo, + l2InitData *statetransfer.ArbosInitializationInfo, + nodeConfig *arbnode.Config, + stackConfig *node.Config, + rollupStackConf setup.RollupStackConfig, + stakeTokenAddr common.Address, +) (*ethclient.Client, *arbnode.Node, *solimpl.AssertionChain) { + fatalErrChan := make(chan error, 10) + l1rpcClient := l1stack.Attach() + l1client := ethclient.NewClient(l1rpcClient) + firstExec, ok := first.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + chainConfig := firstExec.ArbInterface.BlockChain().Config() + addresses := deployContractsOnly(t, ctx, l1info, l1client, chainConfig.ChainID, rollupStackConf, stakeTokenAddr) + + l1info.SetContract("EvilBridge", addresses.Bridge) + l1info.SetContract("EvilSequencerInbox", addresses.SequencerInbox) + l1info.SetContract("EvilInbox", addresses.Inbox) + l1info.SetContract("EvilRollup", addresses.Rollup) + l1info.SetContract("EvilUpgradeExecutor", addresses.UpgradeExecutor) + + if nodeConfig == nil { + nodeConfig = arbnode.ConfigDefaultL1NonSequencerTest() + } + nodeConfig.ParentChainReader.OldHeaderTimeout = 10 * time.Minute + nodeConfig.BatchPoster.DataPoster.MaxMempoolTransactions = 18 + if stackConfig == nil { + stackConfig = testhelpers.CreateStackConfigForTest(t.TempDir()) + } + l2stack, err := node.New(stackConfig) + Require(t, err) + + l2chainDb, err := l2stack.OpenDatabase("chaindb", 0, 0, "", false) + Require(t, err) + l2arbDb, err := l2stack.OpenDatabase("arbdb", 0, 0, "", false) + Require(t, err) + + AddValNodeIfNeeded(t, ctx, nodeConfig, true, "", "") + + dataSigner := signature.DataSignerFromPrivateKey(l1info.GetInfoWithPrivKey("Sequencer").PrivateKey) + txOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + initReader := statetransfer.NewMemoryInitDataReader(l2InitData) + initMessage := getInitMessage(ctx, t, l1client, first.DeployInfo) + + execConfig := ExecConfigDefaultNonSequencerTest(t) + Require(t, execConfig.Validate()) + execConfig.Caching.StateScheme = rawdb.HashScheme + coreCacheConfig := gethexec.DefaultCacheConfigFor(l2stack, &execConfig.Caching) + l2blockchain, err := gethexec.WriteOrTestBlockChain(l2chainDb, coreCacheConfig, initReader, chainConfig, initMessage, execConfig.TxLookupLimit, 0) + Require(t, err) + + execConfigFetcher := func() *gethexec.Config { return execConfig } + execNode, err := gethexec.CreateExecutionNode(ctx, l2stack, l2chainDb, l2blockchain, l1client, execConfigFetcher) + Require(t, err) + l1ChainId, err := l1client.ChainID(ctx) + Require(t, err) + l2node, err := arbnode.CreateNode(ctx, l2stack, execNode, l2arbDb, NewFetcherFromConfig(nodeConfig), l2blockchain.Config(), l1client, addresses, &txOpts, &txOpts, dataSigner, fatalErrChan, l1ChainId, nil /* blob reader */) + Require(t, err) + + l2client := ClientForStack(t, l2stack) + + StartWatchChanErr(t, ctx, fatalErrChan, l2node) + + rollupUserLogic, err := rollupgen.NewRollupUserLogic(addresses.Rollup, l1client) + Require(t, err) + chalManagerAddr, err := rollupUserLogic.ChallengeManager(&bind.CallOpts{}) + Require(t, err) + evilOpts := l1info.GetDefaultTransactOpts("EvilAsserter", ctx) + dp, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(l2arbDb, storage.StakerPrefix), + l2node.L1Reader, + &evilOpts, + NewFetcherFromConfig(nodeConfig), + l2node.SyncMonitor, + l1ChainId, + ) + Require(t, err) + assertionChain, err := solimpl.NewAssertionChain( + ctx, + addresses.Rollup, + chalManagerAddr, + &evilOpts, + butil.NewBackendWrapper(l1client, rpc.LatestBlockNumber), + bold.NewDataPosterTransactor(dp), + ) + Require(t, err) + + return l2client, l2node, assertionChain +} + +func makeBoldBatch( + t *testing.T, + l2Node *arbnode.Node, + l2Info *BlockchainTestInfo, + backend *ethclient.Client, + sequencer *bind.TransactOpts, + seqInbox *bridgegen.SequencerInbox, + seqInboxAddr common.Address, + numMessages, + divergeAtIndex int64, +) { + ctx := context.Background() + + batchBuffer := bytes.NewBuffer([]byte{}) + for i := int64(0); i < numMessages; i++ { + value := i + if i == divergeAtIndex { + value++ + } + err := writeTxToBatchBold(batchBuffer, l2Info.PrepareTx("Owner", "Destination", 1000000, big.NewInt(value), []byte{})) + Require(t, err) + } + compressed, err := arbcompress.CompressWell(batchBuffer.Bytes()) + Require(t, err) + message := append([]byte{0}, compressed...) + + seqNum := new(big.Int).Lsh(common.Big1, 256) + seqNum.Sub(seqNum, common.Big1) + tx, err := seqInbox.AddSequencerL2BatchFromOrigin8f111f3c(sequencer, seqNum, message, big.NewInt(1), common.Address{}, big.NewInt(0), big.NewInt(0)) + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + nodeSeqInbox, err := arbnode.NewSequencerInbox(backend, seqInboxAddr, 0) + Require(t, err) + batches, err := nodeSeqInbox.LookupBatchesInRange(ctx, receipt.BlockNumber, receipt.BlockNumber) + Require(t, err) + if len(batches) == 0 { + Fatal(t, "batch not found after AddSequencerL2BatchFromOrigin") + } + err = l2Node.InboxTracker.AddSequencerBatches(ctx, backend, batches) + Require(t, err) + batchMetaData, err := l2Node.InboxTracker.GetBatchMetadata(batches[0].SequenceNumber) + log.Info("Batch metadata", "md", batchMetaData) + Require(t, err, "failed to get batch metadata after adding batch:") +} + +func writeTxToBatchBold(writer io.Writer, tx *types.Transaction) error { + txData, err := tx.MarshalBinary() + if err != nil { + return err + } + var segment []byte + segment = append(segment, arbstate.BatchSegmentKindL2Message) + segment = append(segment, arbos.L2MessageKind_SignedTx) + segment = append(segment, txData...) + err = rlp.Encode(writer, segment) + return err +} diff --git a/system_tests/bold_new_challenge_test.go b/system_tests/bold_new_challenge_test.go new file mode 100644 index 0000000000..ad6e44bc71 --- /dev/null +++ b/system_tests/bold_new_challenge_test.go @@ -0,0 +1,358 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +//go:build challengetest && !race + +package arbtest + +import ( + "context" + "fmt" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + protocol "github.com/offchainlabs/bold/chain-abstraction" + solimpl "github.com/offchainlabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/offchainlabs/bold/challenge-manager" + modes "github.com/offchainlabs/bold/challenge-manager/types" + "github.com/offchainlabs/bold/containers/option" + l2stateprovider "github.com/offchainlabs/bold/layer2-state-provider" + "github.com/offchainlabs/bold/solgen/go/challengeV2gen" + "github.com/offchainlabs/bold/solgen/go/mocksgen" + "github.com/offchainlabs/bold/solgen/go/rollupgen" + "github.com/offchainlabs/bold/state-commitments/history" + butil "github.com/offchainlabs/bold/util" + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbnode/dataposter/storage" + "github.com/offchainlabs/nitro/staker/bold" +) + +type incorrectBlockStateProvider struct { + honest BoldStateProviderInterface + chain protocol.AssertionChain + wrongAtFirstVirtual bool + wrongAtBlockHeight uint64 + honestMachineHash common.Hash + evilMachineHash common.Hash +} + +func (s *incorrectBlockStateProvider) ExecutionStateAfterPreviousState( + ctx context.Context, + maxInboxCount uint64, + previousGlobalState protocol.GoGlobalState, +) (*protocol.ExecutionState, error) { + maxNumberOfBlocks := s.chain.SpecChallengeManager().LayerZeroHeights().BlockChallengeHeight.Uint64() + executionState, err := s.honest.ExecutionStateAfterPreviousState(ctx, maxInboxCount, previousGlobalState) + if err != nil { + return nil, err + } + evilStates, err := s.L2MessageStatesUpTo(ctx, previousGlobalState, l2stateprovider.Batch(maxInboxCount), option.Some(l2stateprovider.Height(maxNumberOfBlocks))) + if err != nil { + return nil, err + } + historyCommit, err := history.NewCommitment(evilStates, maxNumberOfBlocks+1) + if err != nil { + return nil, err + } + executionState.EndHistoryRoot = historyCommit.Merkle + return executionState, nil +} + +func (s *incorrectBlockStateProvider) L2MessageStatesUpTo( + ctx context.Context, + fromState protocol.GoGlobalState, + batchLimit l2stateprovider.Batch, + toHeight option.Option[l2stateprovider.Height], +) ([]common.Hash, error) { + states, err := s.honest.L2MessageStatesUpTo(ctx, fromState, batchLimit, toHeight) + if err != nil { + return nil, err + } + // Double check that virtual blocks aren't being enumerated by the honest impl + for i := len(states) - 1; i >= 1; i-- { + if states[i] == states[i-1] { + panic("Virtual block found repeated in honest impl (test case currently doesn't accomodate this)") + } else { + break + } + } + if s.wrongAtFirstVirtual && (toHeight.IsNone() || uint64(len(states)) < uint64(toHeight.Unwrap())) { + // We've found the first virtual block, now let's make it wrong + s.wrongAtFirstVirtual = false + s.wrongAtBlockHeight = uint64(len(states)) + } + if toHeight.IsNone() || uint64(toHeight.Unwrap()) >= s.wrongAtBlockHeight { + for uint64(len(states)) <= s.wrongAtBlockHeight { + states = append(states, states[len(states)-1]) + } + s.honestMachineHash = states[s.wrongAtBlockHeight] + states[s.wrongAtBlockHeight][0] ^= 0xFF + s.evilMachineHash = states[s.wrongAtBlockHeight] + if uint64(len(states)) == s.wrongAtBlockHeight+1 && (toHeight.IsNone() || uint64(len(states)) < uint64(toHeight.Unwrap())) { + // don't break the end inclusion proof + states = append(states, s.honestMachineHash) + } + } + return states, nil +} + +func (s *incorrectBlockStateProvider) CollectMachineHashes( + ctx context.Context, cfg *l2stateprovider.HashCollectorConfig, +) ([]common.Hash, error) { + honestHashes, err := s.honest.CollectMachineHashes(ctx, cfg) + if err != nil { + return nil, err + } + if uint64(cfg.BlockChallengeHeight)+1 == s.wrongAtBlockHeight { + if uint64(len(honestHashes)) < cfg.NumDesiredHashes && honestHashes[len(honestHashes)-1] == s.honestMachineHash { + honestHashes = append(honestHashes, s.evilMachineHash) + } + } else if uint64(cfg.BlockChallengeHeight) >= s.wrongAtBlockHeight { + panic(fmt.Sprintf("challenge occured at block height %v at or after wrongAtBlockHeight %v", cfg.BlockChallengeHeight, s.wrongAtBlockHeight)) + } + return honestHashes, nil +} + +func (s *incorrectBlockStateProvider) CollectProof( + ctx context.Context, + assertionMetadata *l2stateprovider.AssociatedAssertionMetadata, + blockChallengeHeight l2stateprovider.Height, + machineIndex l2stateprovider.OpcodeIndex, +) ([]byte, error) { + return s.honest.CollectProof(ctx, assertionMetadata, blockChallengeHeight, machineIndex) +} + +func testChallengeProtocolBOLDVirtualBlocks(t *testing.T, wrongAtFirstVirtual bool) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true).WithBoldDeployment() + + // Block validation requires db hash scheme + builder.execConfig.Caching.StateScheme = rawdb.HashScheme + builder.nodeConfig.BlockValidator.Enable = true + builder.valnodeConfig.UseJit = false + + cleanup := builder.Build(t) + defer cleanup() + + evilNodeConfig := arbnode.ConfigDefaultL1NonSequencerTest() + evilNodeConfig.BlockValidator.Enable = true + evilNode, cleanupEvilNode := builder.Build2ndNode(t, &SecondNodeParams{ + nodeConfig: evilNodeConfig, + }) + defer cleanupEvilNode() + + go keepChainMoving(t, ctx, builder.L1Info, builder.L1.Client) + + builder.L1Info.GenerateAccount("HonestAsserter") + fundBoldStaker(t, ctx, builder, "HonestAsserter") + builder.L1Info.GenerateAccount("EvilAsserter") + fundBoldStaker(t, ctx, builder, "EvilAsserter") + + assertionChain, cleanupHonestChallengeManager := startBoldChallengeManager(t, ctx, builder, builder.L2, "HonestAsserter", nil) + defer cleanupHonestChallengeManager() + + _, cleanupEvilChallengeManager := startBoldChallengeManager(t, ctx, builder, evilNode, "EvilAsserter", func(stateManager BoldStateProviderInterface) BoldStateProviderInterface { + p := &incorrectBlockStateProvider{ + honest: stateManager, + chain: assertionChain, + wrongAtFirstVirtual: wrongAtFirstVirtual, + } + if !wrongAtFirstVirtual { + p.wrongAtBlockHeight = blockChallengeLeafHeight - 2 + } + return p + }) + defer cleanupEvilChallengeManager() + + TransferBalance(t, "Faucet", "Faucet", common.Big0, builder.L2Info, builder.L2.Client, ctx) + + // Everything's setup, now just wait for the challenge to complete and ensure the honest party won + + chalManager := assertionChain.SpecChallengeManager() + filterer, err := challengeV2gen.NewEdgeChallengeManagerFilterer(chalManager.Address(), builder.L1.Client) + Require(t, err) + + fromBlock := uint64(0) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + latestBlock, err := builder.L1.Client.HeaderByNumber(ctx, nil) + Require(t, err) + toBlock := latestBlock.Number.Uint64() + if fromBlock == toBlock { + continue + } + filterOpts := &bind.FilterOpts{ + Start: fromBlock, + End: &toBlock, + Context: ctx, + } + it, err := filterer.FilterEdgeConfirmedByOneStepProof(filterOpts, nil, nil) + Require(t, err) + for it.Next() { + if it.Error() != nil { + t.Fatalf("Error in filter iterator: %v", it.Error()) + } + t.Log("Received event of OSP confirmation!") + tx, _, err := builder.L1.Client.TransactionByHash(ctx, it.Event.Raw.TxHash) + Require(t, err) + signer := types.NewCancunSigner(tx.ChainId()) + address, err := signer.Sender(tx) + Require(t, err) + if address == builder.L1Info.GetAddress("HonestAsserter") { + t.Log("Honest party won OSP, impossible for evil party to win if honest party continues") + Require(t, it.Close()) + return + } + } + fromBlock = toBlock + case <-ctx.Done(): + return + } + } +} + +func fundBoldStaker(t *testing.T, ctx context.Context, builder *NodeBuilder, name string) { + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + TransferBalance(t, "Faucet", name, balance, builder.L1Info, builder.L1.Client, ctx) + + rollupUserLogic, err := rollupgen.NewRollupUserLogic(builder.addresses.Rollup, builder.L1.Client) + Require(t, err) + stakeToken, err := rollupUserLogic.StakeToken(&bind.CallOpts{Context: ctx}) + Require(t, err) + stakeTokenWeth, err := mocksgen.NewTestWETH9(stakeToken, builder.L1.Client) + Require(t, err) + + txOpts := builder.L1Info.GetDefaultTransactOpts(name, ctx) + + txOpts.Value = big.NewInt(params.Ether) + tx, err := stakeTokenWeth.Deposit(&txOpts) + Require(t, err) + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + txOpts.Value = nil + + tx, err = stakeTokenWeth.Approve(&txOpts, builder.addresses.Rollup, balance) + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + challengeManager, err := rollupUserLogic.ChallengeManager(&bind.CallOpts{Context: ctx}) + Require(t, err) + tx, err = stakeTokenWeth.Approve(&txOpts, challengeManager, balance) + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) +} + +func TestChallengeProtocolBOLDNearLastVirtualBlock(t *testing.T) { + testChallengeProtocolBOLDVirtualBlocks(t, false) +} + +func TestChallengeProtocolBOLDFirstVirtualBlock(t *testing.T) { + testChallengeProtocolBOLDVirtualBlocks(t, true) +} + +type BoldStateProviderInterface interface { + l2stateprovider.L2MessageStateCollector + l2stateprovider.MachineHashCollector + l2stateprovider.ProofCollector + l2stateprovider.ExecutionProvider +} + +func startBoldChallengeManager(t *testing.T, ctx context.Context, builder *NodeBuilder, node *TestClient, addressName string, mockStateProvider func(BoldStateProviderInterface) BoldStateProviderInterface) (*solimpl.AssertionChain, func()) { + if !builder.deployBold { + t.Fatal("bold deployment not enabled") + } + + var stateManager BoldStateProviderInterface + var err error + cacheDir := t.TempDir() + stateManager, err = bold.NewBOLDStateProvider( + node.ConsensusNode.BlockValidator, + node.ConsensusNode.StatelessBlockValidator, + l2stateprovider.Height(blockChallengeLeafHeight), + &bold.StateProviderConfig{ + ValidatorName: addressName, + MachineLeavesCachePath: cacheDir, + CheckBatchFinality: false, + }, + cacheDir, + ) + Require(t, err) + + if mockStateProvider != nil { + stateManager = mockStateProvider(stateManager) + } + + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + stateManager, + nil, // Api db + ) + + rollupUserLogic, err := rollupgen.NewRollupUserLogic(builder.addresses.Rollup, builder.L1.Client) + Require(t, err) + chalManagerAddr, err := rollupUserLogic.ChallengeManager(&bind.CallOpts{}) + Require(t, err) + + txOpts := builder.L1Info.GetDefaultTransactOpts(addressName, ctx) + + dp, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(node.ConsensusNode.ArbDB, storage.StakerPrefix), + node.ConsensusNode.L1Reader, + &txOpts, + NewFetcherFromConfig(builder.nodeConfig), + node.ConsensusNode.SyncMonitor, + builder.L1Info.Signer.ChainID(), + ) + Require(t, err) + + assertionChain, err := solimpl.NewAssertionChain( + ctx, + builder.addresses.Rollup, + chalManagerAddr, + &txOpts, + butil.NewBackendWrapper(builder.L1.Client, rpc.LatestBlockNumber), + bold.NewDataPosterTransactor(dp), + ) + Require(t, err) + + stackOpts := []challengemanager.StackOpt{ + challengemanager.StackWithName(addressName), + challengemanager.StackWithMode(modes.MakeMode), + challengemanager.StackWithPostingInterval(time.Second * 3), + challengemanager.StackWithPollingInterval(time.Second), + challengemanager.StackWithAverageBlockCreationTime(time.Second), + } + + challengeManager, err := challengemanager.NewChallengeStack( + assertionChain, + provider, + stackOpts..., + ) + Require(t, err) + + challengeManager.Start(ctx) + return assertionChain, challengeManager.StopAndWait +} diff --git a/system_tests/bold_state_provider_test.go b/system_tests/bold_state_provider_test.go new file mode 100644 index 0000000000..0ecce5ba64 --- /dev/null +++ b/system_tests/bold_state_provider_test.go @@ -0,0 +1,419 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE + +//go:build challengetest && !race + +package arbtest + +import ( + "context" + "errors" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/staker/bold" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/validator/valnode" + + protocol "github.com/offchainlabs/bold/chain-abstraction" + "github.com/offchainlabs/bold/containers/option" + l2stateprovider "github.com/offchainlabs/bold/layer2-state-provider" + "github.com/offchainlabs/bold/solgen/go/bridgegen" + "github.com/offchainlabs/bold/solgen/go/mocksgen" + prefixproofs "github.com/offchainlabs/bold/state-commitments/prefix-proofs" + mockmanager "github.com/offchainlabs/bold/testing/mocks/state-provider" + "github.com/offchainlabs/bold/testing/setup" +) + +func TestChallengeProtocolBOLD_Bisections(t *testing.T) { + t.Parallel() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + l2node, l1info, l2info, l1stack, l1client, stateManager, blockValidator := setupBoldStateProvider(t, ctx, 1<<5) + defer requireClose(t, l1stack) + defer l2node.StopAndWait() + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + seqInbox := l1info.GetAddress("SequencerInbox") + seqInboxBinding, err := bridgegen.NewSequencerInbox(seqInbox, l1client) + Require(t, err) + + seqInboxABI, err := abi.JSON(strings.NewReader(bridgegen.SequencerInboxABI)) + Require(t, err) + + honestUpgradeExec, err := mocksgen.NewUpgradeExecutorMock(l1info.GetAddress("UpgradeExecutor"), l1client) + Require(t, err) + data, err := seqInboxABI.Pack( + "setIsBatchPoster", + sequencerTxOpts.From, + true, + ) + Require(t, err) + honestRollupOwnerOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + _, err = honestUpgradeExec.ExecuteCall(&honestRollupOwnerOpts, seqInbox, data) + Require(t, err) + + // Make two batchs. One with 5 messages, and one with 10 messages. + numMessagesPerBatch := int64(5) + divergeAt := int64(-1) // No divergence. + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + numMessagesPerBatch = int64(10) + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + totalMessageCount, err := l2node.InboxTracker.GetBatchMessageCount(totalBatches - 1) + Require(t, err) + log.Info("Status", "totalBatches", totalBatches, "totalMessageCount", totalMessageCount) + t.Logf("totalBatches: %v, totalMessageCount: %v\n", totalBatches, totalMessageCount) + + // Wait until the validator has validated the batches. + for { + time.Sleep(time.Millisecond * 100) + lastInfo, err := blockValidator.ReadLastValidatedInfo() + if lastInfo == nil || err != nil { + continue + } + if lastInfo.GlobalState.Batch >= totalBatches { + break + } + batchMsgCount, err := l2node.InboxTracker.GetBatchMessageCount(lastInfo.GlobalState.Batch) + if err != nil { + continue + } + if batchMsgCount >= totalMessageCount { + break + } + } + + historyCommitter := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, []l2stateprovider.Height{ + 1 << 5, + 1 << 5, + 1 << 5, + }, + stateManager, + nil, // api db + ) + bisectionHeight := l2stateprovider.Height(16) + request := &l2stateprovider.HistoryCommitmentRequest{ + AssertionMetadata: &l2stateprovider.AssociatedAssertionMetadata{ + FromState: protocol.GoGlobalState{ + Batch: 1, + }, + BatchLimit: 3, + WasmModuleRoot: common.Hash{}, + }, + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + UpToHeight: option.Some(bisectionHeight), + } + bisectionCommitment, err := historyCommitter.HistoryCommitment(ctx, request) + Require(t, err) + + request.UpToHeight = option.None[l2stateprovider.Height]() + packedProof, err := historyCommitter.PrefixProof(ctx, request, bisectionHeight) + Require(t, err) + + dataItem, err := mockmanager.ProofArgs.Unpack(packedProof) + Require(t, err) + preExpansion, ok := dataItem[0].([][32]byte) + if !ok { + Fatal(t, "wrong type") + } + + hashes := make([]common.Hash, len(preExpansion)) + for i, h := range preExpansion { + hash := h + hashes[i] = hash + } + + computed, err := prefixproofs.Root(hashes) + Require(t, err) + if computed != bisectionCommitment.Merkle { + Fatal(t, "wrong commitment") + } +} + +func TestChallengeProtocolBOLD_StateProvider(t *testing.T) { + // t.Parallel() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + maxNumBlocks := uint64(1 << 14) + l2node, l1info, l2info, l1stack, l1client, stateManager, blockValidator := setupBoldStateProvider(t, ctx, maxNumBlocks) + defer requireClose(t, l1stack) + defer l2node.StopAndWait() + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + seqInbox := l1info.GetAddress("SequencerInbox") + seqInboxBinding, err := bridgegen.NewSequencerInbox(seqInbox, l1client) + Require(t, err) + + seqInboxABI, err := abi.JSON(strings.NewReader(bridgegen.SequencerInboxABI)) + Require(t, err) + + honestUpgradeExec, err := mocksgen.NewUpgradeExecutorMock(l1info.GetAddress("UpgradeExecutor"), l1client) + Require(t, err) + data, err := seqInboxABI.Pack( + "setIsBatchPoster", + sequencerTxOpts.From, + true, + ) + Require(t, err) + honestRollupOwnerOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + _, err = honestUpgradeExec.ExecuteCall(&honestRollupOwnerOpts, seqInbox, data) + Require(t, err) + + // We will make two batches, with 5 messages in each batch. + numMessagesPerBatch := int64(5) + divergeAt := int64(-1) // No divergence. + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + totalMessageCount, err := l2node.InboxTracker.GetBatchMessageCount(totalBatches - 1) + Require(t, err) + + // Wait until the validator has validated the batches. + for { + time.Sleep(time.Millisecond * 100) + lastInfo, err := blockValidator.ReadLastValidatedInfo() + if lastInfo == nil || err != nil { + continue + } + if lastInfo.GlobalState.Batch >= totalBatches { + break + } + } + + t.Run("StatesInBatchRange", func(t *testing.T) { + toBatch := uint64(3) + toHeight := l2stateprovider.Height(10) + fromState := protocol.GoGlobalState{ + Batch: 1, + } + stateRoots, states, err := stateManager.StatesInBatchRange(ctx, fromState, toBatch, toHeight) + Require(t, err) + want := 11 + got := len(stateRoots) + + if got != want { + t.Errorf("len(stateRoots): got %v, want %v", got, want) + } + firstState := states[0] + if firstState.Batch != 1 && firstState.PosInBatch != 0 { + Fatal(t, "wrong first state") + } + lastState := states[len(states)-1] + if lastState.Batch != 3 && lastState.PosInBatch != 0 { + Fatal(t, "wrong last state") + } + }) + t.Run("AgreesWithExecutionState", func(t *testing.T) { + // Non-zero position in batch should fail. + _, err = stateManager.ExecutionStateAfterPreviousState( + ctx, + 0, + protocol.GoGlobalState{ + Batch: 0, + PosInBatch: 1, + }, + ) + if err == nil { + Fatal(t, "should not agree with execution state") + } + if !strings.Contains(err.Error(), "max inbox count cannot be zero") { + Fatal(t, "wrong error message") + } + + // Always agrees with genesis. + genesis, err := stateManager.ExecutionStateAfterPreviousState( + ctx, + 1, + protocol.GoGlobalState{ + Batch: 0, + PosInBatch: 0, + }, + ) + Require(t, err) + if genesis == nil { + Fatal(t, "genesis should not be nil") + } + + // Always agrees with the init message. + first, err := stateManager.ExecutionStateAfterPreviousState( + ctx, + 2, + genesis.GlobalState, + ) + Require(t, err) + if first == nil { + Fatal(t, "genesis should not be nil") + } + + // Chain catching up if it has not seen batch 10. + _, err = stateManager.ExecutionStateAfterPreviousState( + ctx, + 10, + first.GlobalState, + ) + if err == nil { + Fatal(t, "should not agree with execution state") + } + if !errors.Is(err, l2stateprovider.ErrChainCatchingUp) { + Fatal(t, "wrong error") + } + + // Check if we agree with the last posted batch to the inbox. + result, err := l2node.TxStreamer.ResultAtCount(totalMessageCount) + Require(t, err) + _ = result + + state := protocol.GoGlobalState{ + BlockHash: result.BlockHash, + SendRoot: result.SendRoot, + Batch: 3, + } + got, err := stateManager.ExecutionStateAfterPreviousState(ctx, 3, first.GlobalState) + Require(t, err) + if state.Batch != got.GlobalState.Batch { + Fatal(t, "wrong batch") + } + if state.SendRoot != got.GlobalState.SendRoot { + Fatal(t, "wrong send root") + } + if state.BlockHash != got.GlobalState.BlockHash { + Fatal(t, "wrong batch") + } + + // See if we agree with one batch immediately after that and see that we fail with + // "ErrChainCatchingUp". + _, err = stateManager.ExecutionStateAfterPreviousState( + ctx, + state.Batch+1, + got.GlobalState, + ) + if err == nil { + Fatal(t, "should not agree with execution state") + } + if !errors.Is(err, l2stateprovider.ErrChainCatchingUp) { + Fatal(t, "wrong error") + } + }) + t.Run("ExecutionStateAfterBatchCount", func(t *testing.T) { + _, err = stateManager.ExecutionStateAfterPreviousState(ctx, 0, protocol.GoGlobalState{}) + if err == nil { + Fatal(t, "should have failed") + } + if !strings.Contains(err.Error(), "max inbox count cannot be zero") { + Fatal(t, "wrong error message", err) + } + + genesis, err := stateManager.ExecutionStateAfterPreviousState(ctx, 1, protocol.GoGlobalState{}) + Require(t, err) + execState, err := stateManager.ExecutionStateAfterPreviousState(ctx, totalBatches, genesis.GlobalState) + Require(t, err) + if execState == nil { + Fatal(t, "should not be nil") + } + }) +} + +func setupBoldStateProvider(t *testing.T, ctx context.Context, blockChallengeHeight uint64) (*arbnode.Node, *BlockchainTestInfo, *BlockchainTestInfo, *node.Node, *ethclient.Client, *bold.BOLDStateProvider, *staker.BlockValidator) { + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + l2chainConfig := chaininfo.ArbitrumDevTestChainConfig() + l2info := NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(l2chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + ownerBal := big.NewInt(params.Ether) + ownerBal.Mul(ownerBal, big.NewInt(1_000_000)) + l2info.GenerateGenesisAccount("Owner", ownerBal) + sconf := setup.RollupStackConfig{ + UseMockBridge: false, + UseMockOneStepProver: false, + MinimumAssertionPeriod: 0, + } + + _, l2node, _, _, l1info, _, l1client, l1stack, _, _ := createTestNodeOnL1ForBoldProtocol( + t, + ctx, + false, + nil, + l2chainConfig, + nil, + sconf, + l2info, + ) + + valnode.TestValidationConfig.UseJit = false + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + blockValidatorConfig := staker.TestBlockValidatorConfig + + stateless, err := staker.NewStatelessBlockValidator( + l2node.InboxReader, + l2node.InboxTracker, + l2node.TxStreamer, + l2node.Execution, + l2node.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + Require(t, stateless.Start(ctx)) + + blockValidator, err := staker.NewBlockValidator( + stateless, + l2node.InboxTracker, + l2node.TxStreamer, + StaticFetcherFrom(t, &blockValidatorConfig), + nil, + ) + Require(t, err) + Require(t, blockValidator.Initialize(ctx)) + Require(t, blockValidator.Start(ctx)) + + dir := t.TempDir() + stateManager, err := bold.NewBOLDStateProvider( + blockValidator, + stateless, + l2stateprovider.Height(blockChallengeHeight), + &bold.StateProviderConfig{ + ValidatorName: "", + MachineLeavesCachePath: dir, + CheckBatchFinality: false, + }, + dir, + ) + Require(t, err) + + Require(t, l2node.Start(ctx)) + return l2node, l1info, l2info, l1stack, l1client, stateManager, blockValidator +} diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 8d4d01eebf..4860c3ce4d 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -9,7 +9,9 @@ import ( "encoding/binary" "encoding/hex" "encoding/json" + "flag" "io" + "log/slog" "math/big" "net" "net/http" @@ -20,26 +22,7 @@ import ( "testing" "time" - "github.com/go-redis/redis/v8" - "github.com/offchainlabs/nitro/arbos" - "github.com/offchainlabs/nitro/arbos/arbostypes" - "github.com/offchainlabs/nitro/arbos/util" - "github.com/offchainlabs/nitro/arbstate/daprovider" - "github.com/offchainlabs/nitro/blsSignatures" - "github.com/offchainlabs/nitro/cmd/chaininfo" - "github.com/offchainlabs/nitro/cmd/conf" - "github.com/offchainlabs/nitro/cmd/genericconf" - "github.com/offchainlabs/nitro/das" - "github.com/offchainlabs/nitro/deploy" - "github.com/offchainlabs/nitro/execution/gethexec" - "github.com/offchainlabs/nitro/util/arbmath" - "github.com/offchainlabs/nitro/util/headerreader" - "github.com/offchainlabs/nitro/util/redisutil" - "github.com/offchainlabs/nitro/util/signature" - "github.com/offchainlabs/nitro/validator/server_api" - "github.com/offchainlabs/nitro/validator/server_common" - "github.com/offchainlabs/nitro/validator/valnode" - rediscons "github.com/offchainlabs/nitro/validator/valnode/redis" + "github.com/redis/go-redis/v9" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" @@ -68,30 +51,54 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + boldMocksgen "github.com/offchainlabs/bold/solgen/go/mocksgen" + "github.com/offchainlabs/bold/solgen/go/rollupgen" + "github.com/offchainlabs/bold/testing/setup" + butil "github.com/offchainlabs/bold/util" "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/arbstate/daprovider" "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/blsSignatures" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/cmd/conf" + "github.com/offchainlabs/nitro/cmd/genericconf" + "github.com/offchainlabs/nitro/das" + "github.com/offchainlabs/nitro/deploy" + "github.com/offchainlabs/nitro/execution/gethexec" _ "github.com/offchainlabs/nitro/execution/nodeInterface" "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" "github.com/offchainlabs/nitro/statetransfer" + "github.com/offchainlabs/nitro/util/arbmath" + "github.com/offchainlabs/nitro/util/headerreader" + "github.com/offchainlabs/nitro/util/redisutil" + "github.com/offchainlabs/nitro/util/signature" "github.com/offchainlabs/nitro/util/testhelpers" "github.com/offchainlabs/nitro/util/testhelpers/env" "github.com/offchainlabs/nitro/util/testhelpers/github" - "golang.org/x/exp/slog" + "github.com/offchainlabs/nitro/validator/inputs" + "github.com/offchainlabs/nitro/validator/server_api" + "github.com/offchainlabs/nitro/validator/server_arb" + "github.com/offchainlabs/nitro/validator/server_common" + "github.com/offchainlabs/nitro/validator/valnode" + rediscons "github.com/offchainlabs/nitro/validator/valnode/redis" ) type info = *BlockchainTestInfo -type client = arbutil.L1Interface type SecondNodeParams struct { - nodeConfig *arbnode.Config - execConfig *gethexec.Config - stackConfig *node.Config - dasConfig *das.DataAvailabilityConfig - initData *statetransfer.ArbosInitializationInfo - addresses *chaininfo.RollupAddresses + nodeConfig *arbnode.Config + execConfig *gethexec.Config + stackConfig *node.Config + dasConfig *das.DataAvailabilityConfig + initData *statetransfer.ArbosInitializationInfo + addresses *chaininfo.RollupAddresses + wasmCacheTag uint32 } type TestClient struct { @@ -138,8 +145,8 @@ func (tc *TestClient) GetBaseFeeAt(t *testing.T, blockNum *big.Int) *big.Int { return GetBaseFeeAt(t, tc.Client, tc.ctx, blockNum) } -func (tc *TestClient) SendWaitTestTransactions(t *testing.T, txs []*types.Transaction) { - SendWaitTestTransactions(t, tc.ctx, tc.Client, txs) +func (tc *TestClient) SendWaitTestTransactions(t *testing.T, txs []*types.Transaction) []*types.Receipt { + return SendWaitTestTransactions(t, tc.ctx, tc.Client, txs) } func (tc *TestClient) DeploySimple(t *testing.T, auth bind.TransactOpts) (common.Address, *mocksgen.Simple) { @@ -155,19 +162,20 @@ func (tc *TestClient) EnsureTxSucceededWithTimeout(transaction *types.Transactio } var TestCachingConfig = gethexec.CachingConfig{ - Archive: false, - BlockCount: 128, - BlockAge: 30 * time.Minute, - TrieTimeLimit: time.Hour, - TrieDirtyCache: 1024, - TrieCleanCache: 600, - SnapshotCache: 400, - DatabaseCache: 2048, - SnapshotRestoreGasLimit: 300_000_000_000, - MaxNumberOfBlocksToSkipStateSaving: 0, - MaxAmountOfGasToSkipStateSaving: 0, - StylusLRUCache: 0, - StateScheme: env.GetTestStateScheme(), + Archive: false, + BlockCount: 128, + BlockAge: 30 * time.Minute, + TrieTimeLimit: time.Hour, + TrieDirtyCache: 1024, + TrieCleanCache: 600, + SnapshotCache: 400, + DatabaseCache: 2048, + SnapshotRestoreGasLimit: 300_000_000_000, + MaxNumberOfBlocksToSkipStateSaving: 0, + MaxAmountOfGasToSkipStateSaving: 0, + StylusLRUCacheCapacity: 0, + DisableStylusCacheMetricsCollection: true, + StateScheme: env.GetTestStateScheme(), } var DefaultTestForwarderConfig = gethexec.ForwarderConfig{ @@ -197,7 +205,7 @@ var TestSequencerConfig = gethexec.SequencerConfig{ EnableProfiling: false, } -func ExecConfigDefaultNonSequencerTest() *gethexec.Config { +func ExecConfigDefaultNonSequencerTest(t *testing.T) *gethexec.Config { config := gethexec.ConfigDefault config.Caching = TestCachingConfig config.ParentChainReader = headerreader.TestConfig @@ -206,12 +214,12 @@ func ExecConfigDefaultNonSequencerTest() *gethexec.Config { config.ForwardingTarget = "null" config.TxPreChecker.Strictness = gethexec.TxPreCheckerStrictnessNone - _ = config.Validate() + Require(t, config.Validate()) return &config } -func ExecConfigDefaultTest() *gethexec.Config { +func ExecConfigDefaultTest(t *testing.T) *gethexec.Config { config := gethexec.ConfigDefault config.Caching = TestCachingConfig config.Sequencer = TestSequencerConfig @@ -219,7 +227,7 @@ func ExecConfigDefaultTest() *gethexec.Config { config.ForwardingTarget = "null" config.TxPreChecker.Strictness = gethexec.TxPreCheckerStrictnessNone - _ = config.Validate() + Require(t, config.Validate()) return &config } @@ -233,21 +241,76 @@ type NodeBuilder struct { l1StackConfig *node.Config l2StackConfig *node.Config valnodeConfig *valnode.Config + l3Config *NitroConfig + deployBold bool L1Info info L2Info info + L3Info info - // L1, L2 Node parameters + // L1, L2, L3 Node parameters dataDir string isSequencer bool takeOwnership bool withL1 bool addresses *chaininfo.RollupAddresses + l3Addresses *chaininfo.RollupAddresses initMessage *arbostypes.ParsedInitMessage + l3InitMessage *arbostypes.ParsedInitMessage withProdConfirmPeriodBlocks bool + wasmCacheTag uint32 + delayBufferThreshold uint64 // Created nodes L1 *TestClient L2 *TestClient + L3 *TestClient +} + +type NitroConfig struct { + chainConfig *params.ChainConfig + nodeConfig *arbnode.Config + execConfig *gethexec.Config + stackConfig *node.Config + valnodeConfig *valnode.Config + + withProdConfirmPeriodBlocks bool + isSequencer bool +} + +func L3NitroConfigDefaultTest(t *testing.T) *NitroConfig { + chainConfig := ¶ms.ChainConfig{ + ChainID: big.NewInt(333333), + HomesteadBlock: big.NewInt(0), + DAOForkBlock: nil, + DAOForkSupport: true, + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArbitrumChainParams: chaininfo.ArbitrumDevTestParams(), + Clique: ¶ms.CliqueConfig{ + Period: 0, + Epoch: 0, + }, + } + + valnodeConfig := valnode.TestValidationConfig + return &NitroConfig{ + chainConfig: chainConfig, + nodeConfig: arbnode.ConfigDefaultL1Test(), + execConfig: ExecConfigDefaultTest(t), + stackConfig: testhelpers.CreateStackConfigForTest(t.TempDir()), + valnodeConfig: &valnodeConfig, + + withProdConfirmPeriodBlocks: false, + isSequencer: true, + } } func NewNodeBuilder(ctx context.Context) *NodeBuilder { @@ -264,7 +327,7 @@ func (b *NodeBuilder) DefaultConfig(t *testing.T, withL1 bool) *NodeBuilder { b.takeOwnership = true b.nodeConfig = arbnode.ConfigDefaultL2Test() } - b.chainConfig = params.ArbitrumDevTestChainConfig() + b.chainConfig = chaininfo.ArbitrumDevTestChainConfig() b.L1Info = NewL1TestInfo(t) b.L2Info = NewArbTestInfo(t, b.chainConfig.ChainID) b.dataDir = t.TempDir() @@ -272,7 +335,8 @@ func (b *NodeBuilder) DefaultConfig(t *testing.T, withL1 bool) *NodeBuilder { b.l2StackConfig = testhelpers.CreateStackConfigForTest(b.dataDir) cp := valnode.TestValidationConfig b.valnodeConfig = &cp - b.execConfig = ExecConfigDefaultTest() + b.execConfig = ExecConfigDefaultTest(t) + b.l3Config = L3NitroConfigDefaultTest(t) return b } @@ -288,11 +352,38 @@ func (b *NodeBuilder) WithProdConfirmPeriodBlocks() *NodeBuilder { return b } +func (b *NodeBuilder) WithBoldDeployment() *NodeBuilder { + b.deployBold = true + return b +} + func (b *NodeBuilder) WithWasmRootDir(wasmRootDir string) *NodeBuilder { b.valnodeConfig.Wasm.RootPath = wasmRootDir return b } +func (b *NodeBuilder) WithExtraArchs(targets []string) *NodeBuilder { + b.execConfig.StylusTarget.ExtraArchs = targets + return b +} + +func (b *NodeBuilder) WithStylusLongTermCache(enabled bool) *NodeBuilder { + if enabled { + b.wasmCacheTag = 1 + } else { + b.wasmCacheTag = 0 + } + return b +} + +// WithDelayBuffer sets the delay-buffer threshold, which is the number of blocks the batch-poster +// is allowed to delay a batch with a delayed message. +// Setting the threshold to zero disabled the delay buffer (default behaviour). +func (b *NodeBuilder) WithDelayBuffer(threshold uint64) *NodeBuilder { + b.delayBufferThreshold = threshold + return b +} + func (b *NodeBuilder) Build(t *testing.T) func() { b.CheckConfig(t) if b.withL1 { @@ -304,13 +395,13 @@ func (b *NodeBuilder) Build(t *testing.T) func() { func (b *NodeBuilder) CheckConfig(t *testing.T) { if b.chainConfig == nil { - b.chainConfig = params.ArbitrumDevTestChainConfig() + b.chainConfig = chaininfo.ArbitrumDevTestChainConfig() } if b.nodeConfig == nil { b.nodeConfig = arbnode.ConfigDefaultL1Test() } if b.execConfig == nil { - b.execConfig = ExecConfigDefaultTest() + b.execConfig = ExecConfigDefaultTest(t) } if b.L1Info == nil { b.L1Info = NewL1TestInfo(t) @@ -332,64 +423,179 @@ func (b *NodeBuilder) BuildL1(t *testing.T) { b.L1Info, b.L1.Client, b.L1.L1Backend, b.L1.Stack = createTestL1BlockChain(t, b.L1Info) locator, err := server_common.NewMachineLocator(b.valnodeConfig.Wasm.RootPath) Require(t, err) - b.addresses, b.initMessage = DeployOnTestL1(t, b.ctx, b.L1Info, b.L1.Client, b.chainConfig, locator.LatestWasmModuleRoot(), b.withProdConfirmPeriodBlocks) + b.addresses, b.initMessage = deployOnParentChain( + t, + b.ctx, + b.L1Info, + b.L1.Client, + &headerreader.TestConfig, + b.chainConfig, + locator.LatestWasmModuleRoot(), + b.withProdConfirmPeriodBlocks, + true, + b.deployBold, + b.delayBufferThreshold, + ) b.L1.cleanup = func() { requireClose(t, b.L1.Stack) } } -func (b *NodeBuilder) BuildL2OnL1(t *testing.T) func() { - if b.L1 == nil { - t.Fatal("must build L1 before building L2") +func buildOnParentChain( + t *testing.T, + ctx context.Context, + + dataDir string, + + parentChainInfo info, + parentChainTestClient *TestClient, + parentChainId *big.Int, + + chainConfig *params.ChainConfig, + stackConfig *node.Config, + execConfig *gethexec.Config, + nodeConfig *arbnode.Config, + valnodeConfig *valnode.Config, + isSequencer bool, + chainInfo info, + + initMessage *arbostypes.ParsedInitMessage, + addresses *chaininfo.RollupAddresses, + + wasmCacheTag uint32, +) *TestClient { + if parentChainTestClient == nil { + t.Fatal("must build parent chain before building chain") } - b.L2 = NewTestClient(b.ctx) - var l2chainDb ethdb.Database - var l2arbDb ethdb.Database - var l2blockchain *core.BlockChain - _, b.L2.Stack, l2chainDb, l2arbDb, l2blockchain = createL2BlockChainWithStackConfig( - t, b.L2Info, b.dataDir, b.chainConfig, b.initMessage, b.l2StackConfig, &b.execConfig.Caching) + chainTestClient := NewTestClient(ctx) + + var chainDb ethdb.Database + var arbDb ethdb.Database + var blockchain *core.BlockChain + _, chainTestClient.Stack, chainDb, arbDb, blockchain = createNonL1BlockChainWithStackConfig( + t, chainInfo, dataDir, chainConfig, initMessage, stackConfig, execConfig, wasmCacheTag) var sequencerTxOptsPtr *bind.TransactOpts var dataSigner signature.DataSignerFunc - if b.isSequencer { - sequencerTxOpts := b.L1Info.GetDefaultTransactOpts("Sequencer", b.ctx) + if isSequencer { + sequencerTxOpts := parentChainInfo.GetDefaultTransactOpts("Sequencer", ctx) sequencerTxOptsPtr = &sequencerTxOpts - dataSigner = signature.DataSignerFromPrivateKey(b.L1Info.GetInfoWithPrivKey("Sequencer").PrivateKey) + dataSigner = signature.DataSignerFromPrivateKey(parentChainInfo.GetInfoWithPrivKey("Sequencer").PrivateKey) } else { - b.nodeConfig.BatchPoster.Enable = false - b.nodeConfig.Sequencer = false - b.nodeConfig.DelayedSequencer.Enable = false - b.execConfig.Sequencer.Enable = false + nodeConfig.BatchPoster.Enable = false + nodeConfig.Sequencer = false + nodeConfig.DelayedSequencer.Enable = false + execConfig.Sequencer.Enable = false } var validatorTxOptsPtr *bind.TransactOpts - if b.nodeConfig.Staker.Enable { - validatorTxOpts := b.L1Info.GetDefaultTransactOpts("Validator", b.ctx) + if nodeConfig.Staker.Enable { + validatorTxOpts := parentChainInfo.GetDefaultTransactOpts("Validator", ctx) validatorTxOptsPtr = &validatorTxOpts } - AddValNodeIfNeeded(t, b.ctx, b.nodeConfig, true, "", b.valnodeConfig.Wasm.RootPath) + AddValNodeIfNeeded(t, ctx, nodeConfig, true, "", valnodeConfig.Wasm.RootPath) - Require(t, b.execConfig.Validate()) - execConfig := b.execConfig - execConfigFetcher := func() *gethexec.Config { return execConfig } - execNode, err := gethexec.CreateExecutionNode(b.ctx, b.L2.Stack, l2chainDb, l2blockchain, b.L1.Client, execConfigFetcher) + Require(t, execConfig.Validate()) + execConfigToBeUsedInConfigFetcher := execConfig + execConfigFetcher := func() *gethexec.Config { return execConfigToBeUsedInConfigFetcher } + execNode, err := gethexec.CreateExecutionNode(ctx, chainTestClient.Stack, chainDb, blockchain, parentChainTestClient.Client, execConfigFetcher) Require(t, err) fatalErrChan := make(chan error, 10) - b.L2.ConsensusNode, err = arbnode.CreateNode( - b.ctx, b.L2.Stack, execNode, l2arbDb, NewFetcherFromConfig(b.nodeConfig), l2blockchain.Config(), b.L1.Client, - b.addresses, validatorTxOptsPtr, sequencerTxOptsPtr, dataSigner, fatalErrChan, big.NewInt(1337), nil) + chainTestClient.ConsensusNode, err = arbnode.CreateNode( + ctx, chainTestClient.Stack, execNode, arbDb, NewFetcherFromConfig(nodeConfig), blockchain.Config(), parentChainTestClient.Client, + addresses, validatorTxOptsPtr, sequencerTxOptsPtr, dataSigner, fatalErrChan, parentChainId, nil) Require(t, err) - err = b.L2.ConsensusNode.Start(b.ctx) + err = chainTestClient.ConsensusNode.Start(ctx) Require(t, err) - b.L2.Client = ClientForStack(t, b.L2.Stack) + chainTestClient.Client = ClientForStack(t, chainTestClient.Stack) - StartWatchChanErr(t, b.ctx, fatalErrChan, b.L2.ConsensusNode) + StartWatchChanErr(t, ctx, fatalErrChan, chainTestClient.ConsensusNode) + + chainTestClient.ExecNode = getExecNode(t, chainTestClient.ConsensusNode) + chainTestClient.cleanup = func() { chainTestClient.ConsensusNode.StopAndWait() } + + return chainTestClient +} + +func (b *NodeBuilder) BuildL3OnL2(t *testing.T) func() { + b.L3Info = NewArbTestInfo(t, b.l3Config.chainConfig.ChainID) + + locator, err := server_common.NewMachineLocator(b.l3Config.valnodeConfig.Wasm.RootPath) + Require(t, err) + + parentChainReaderConfig := headerreader.TestConfig + parentChainReaderConfig.Dangerous.WaitForTxApprovalSafePoll = 0 + b.l3Addresses, b.l3InitMessage = deployOnParentChain( + t, + b.ctx, + b.L2Info, + b.L2.Client, + &parentChainReaderConfig, + b.l3Config.chainConfig, + locator.LatestWasmModuleRoot(), + b.l3Config.withProdConfirmPeriodBlocks, + false, + b.deployBold, + 0, + ) + + b.L3 = buildOnParentChain( + t, + b.ctx, + + b.dataDir, + + b.L2Info, + b.L2, + b.chainConfig.ChainID, + + b.l3Config.chainConfig, + b.l3Config.stackConfig, + b.l3Config.execConfig, + b.l3Config.nodeConfig, + b.l3Config.valnodeConfig, + b.l3Config.isSequencer, + b.L3Info, + + b.l3InitMessage, + b.l3Addresses, + + b.wasmCacheTag, + ) + + return func() { + b.L3.cleanup() + } +} + +func (b *NodeBuilder) BuildL2OnL1(t *testing.T) func() { + b.L2 = buildOnParentChain( + t, + b.ctx, + + b.dataDir, + + b.L1Info, + b.L1, + big.NewInt(1337), + + b.chainConfig, + b.l2StackConfig, + b.execConfig, + b.nodeConfig, + b.valnodeConfig, + b.isSequencer, + b.L2Info, + + b.initMessage, + b.addresses, + + b.wasmCacheTag, + ) - b.L2.ExecNode = getExecNode(t, b.L2.ConsensusNode) - b.L2.cleanup = func() { b.L2.ConsensusNode.StopAndWait() } return func() { b.L2.cleanup() if b.L1 != nil && b.L1.cleanup != nil { @@ -409,7 +615,7 @@ func (b *NodeBuilder) BuildL2(t *testing.T) func() { var arbDb ethdb.Database var blockchain *core.BlockChain b.L2Info, b.L2.Stack, chainDb, arbDb, blockchain = createL2BlockChain( - t, b.L2Info, b.dataDir, b.chainConfig, &b.execConfig.Caching) + t, b.L2Info, b.dataDir, b.chainConfig, b.execConfig, b.wasmCacheTag) Require(t, b.execConfig.Validate()) execConfig := b.execConfig @@ -460,7 +666,7 @@ func (b *NodeBuilder) RestartL2Node(t *testing.T) { } b.L2.cleanup() - l2info, stack, chainDb, arbDb, blockchain := createL2BlockChainWithStackConfig(t, b.L2Info, b.dataDir, b.chainConfig, b.initMessage, b.l2StackConfig, &b.execConfig.Caching) + l2info, stack, chainDb, arbDb, blockchain := createNonL1BlockChainWithStackConfig(t, b.L2Info, b.dataDir, b.chainConfig, b.initMessage, b.l2StackConfig, b.execConfig, b.wasmCacheTag) execConfigFetcher := func() *gethexec.Config { return b.execConfig } execNode, err := gethexec.CreateExecutionNode(b.ctx, stack, chainDb, blockchain, nil, execConfigFetcher) @@ -486,13 +692,25 @@ func (b *NodeBuilder) RestartL2Node(t *testing.T) { b.L2Info = l2info } -func (b *NodeBuilder) Build2ndNode(t *testing.T, params *SecondNodeParams) (*TestClient, func()) { - if b.L2 == nil { - t.Fatal("builder did not previously build a L2 Node") - } - if b.withL1 && b.L1 == nil { - t.Fatal("builder did not previously build a L1 Node") - } +func build2ndNode( + t *testing.T, + ctx context.Context, + + firstNodeStackConfig *node.Config, + firsNodeExecConfig *gethexec.Config, + firstNodeNodeConfig *arbnode.Config, + firstNodeInfo info, + firstNodeTestClient *TestClient, + valnodeConfig *valnode.Config, + + parentChainTestClient *TestClient, + parentChainInfo info, + + params *SecondNodeParams, + + addresses *chaininfo.RollupAddresses, + initMessage *arbostypes.ParsedInitMessage, +) (*TestClient, func()) { if params.nodeConfig == nil { params.nodeConfig = arbnode.ConfigDefaultL1NonSequencerTest() } @@ -500,18 +718,18 @@ func (b *NodeBuilder) Build2ndNode(t *testing.T, params *SecondNodeParams) (*Tes params.nodeConfig.DataAvailability = *params.dasConfig } if params.stackConfig == nil { - params.stackConfig = b.l2StackConfig + params.stackConfig = firstNodeStackConfig // should use different dataDir from the previously used ones params.stackConfig.DataDir = t.TempDir() } if params.initData == nil { - params.initData = &b.L2Info.ArbInitData + params.initData = &firstNodeInfo.ArbInitData } if params.execConfig == nil { - params.execConfig = b.execConfig + params.execConfig = firsNodeExecConfig } if params.addresses == nil { - params.addresses = b.addresses + params.addresses = addresses } if params.execConfig.RPC.MaxRecreateStateDepth == arbitrum.UninitializedMaxRecreateStateDepth { if params.execConfig.Caching.Archive { @@ -520,42 +738,98 @@ func (b *NodeBuilder) Build2ndNode(t *testing.T, params *SecondNodeParams) (*Tes params.execConfig.RPC.MaxRecreateStateDepth = arbitrum.DefaultNonArchiveNodeMaxRecreateStateDepth } } - if b.nodeConfig.BatchPoster.Enable && params.nodeConfig.BatchPoster.Enable && params.nodeConfig.BatchPoster.RedisUrl == "" { + if firstNodeNodeConfig.BatchPoster.Enable && params.nodeConfig.BatchPoster.Enable && params.nodeConfig.BatchPoster.RedisUrl == "" { t.Fatal("The batch poster must use Redis when enabled for multiple nodes") } - l2 := NewTestClient(b.ctx) - l2.Client, l2.ConsensusNode = - Create2ndNodeWithConfig(t, b.ctx, b.L2.ConsensusNode, b.L1.Stack, b.L1Info, params.initData, params.nodeConfig, params.execConfig, params.stackConfig, b.valnodeConfig, params.addresses, b.initMessage) - l2.ExecNode = getExecNode(t, l2.ConsensusNode) - l2.cleanup = func() { l2.ConsensusNode.StopAndWait() } - return l2, func() { l2.cleanup() } + testClient := NewTestClient(ctx) + testClient.Client, testClient.ConsensusNode = + Create2ndNodeWithConfig(t, ctx, firstNodeTestClient.ConsensusNode, parentChainTestClient.Stack, parentChainInfo, params.initData, params.nodeConfig, params.execConfig, params.stackConfig, valnodeConfig, params.addresses, initMessage, params.wasmCacheTag) + testClient.ExecNode = getExecNode(t, testClient.ConsensusNode) + testClient.cleanup = func() { testClient.ConsensusNode.StopAndWait() } + return testClient, func() { testClient.cleanup() } +} + +func (b *NodeBuilder) Build2ndNode(t *testing.T, params *SecondNodeParams) (*TestClient, func()) { + if b.L2 == nil { + t.Fatal("builder did not previously built an L2 Node") + } + if b.withL1 && b.L1 == nil { + t.Fatal("builder did not previously built an L1 Node") + } + return build2ndNode( + t, + b.ctx, + + b.l2StackConfig, + b.execConfig, + b.nodeConfig, + b.L2Info, + b.L2, + b.valnodeConfig, + + b.L1, + b.L1Info, + + params, + + b.addresses, + b.initMessage, + ) +} + +func (b *NodeBuilder) Build2ndNodeOnL3(t *testing.T, params *SecondNodeParams) (*TestClient, func()) { + if b.L3 == nil { + t.Fatal("builder did not previously built an L3 Node") + } + return build2ndNode( + t, + b.ctx, + + b.l3Config.stackConfig, + b.l3Config.execConfig, + b.l3Config.nodeConfig, + b.L3Info, + b.L3, + b.l3Config.valnodeConfig, + + b.L2, + b.L2Info, + + params, + + b.l3Addresses, + b.l3InitMessage, + ) } func (b *NodeBuilder) BridgeBalance(t *testing.T, account string, amount *big.Int) (*types.Transaction, *types.Receipt) { return BridgeBalance(t, account, amount, b.L1Info, b.L2Info, b.L1.Client, b.L2.Client, b.ctx) } -func SendWaitTestTransactions(t *testing.T, ctx context.Context, client client, txs []*types.Transaction) { +func SendWaitTestTransactions(t *testing.T, ctx context.Context, client *ethclient.Client, txs []*types.Transaction) []*types.Receipt { t.Helper() + receipts := make([]*types.Receipt, len(txs)) for _, tx := range txs { Require(t, client.SendTransaction(ctx, tx)) } - for _, tx := range txs { - _, err := EnsureTxSucceeded(ctx, client, tx) + for i, tx := range txs { + var err error + receipts[i], err = EnsureTxSucceeded(ctx, client, tx) Require(t, err) } + return receipts } func TransferBalance( - t *testing.T, from, to string, amount *big.Int, l2info info, client client, ctx context.Context, + t *testing.T, from, to string, amount *big.Int, l2info info, client *ethclient.Client, ctx context.Context, ) (*types.Transaction, *types.Receipt) { t.Helper() return TransferBalanceTo(t, from, l2info.GetAddress(to), amount, l2info, client, ctx) } func TransferBalanceTo( - t *testing.T, from string, to common.Address, amount *big.Int, l2info info, client client, ctx context.Context, + t *testing.T, from string, to common.Address, amount *big.Int, l2info info, client *ethclient.Client, ctx context.Context, ) (*types.Transaction, *types.Receipt) { t.Helper() tx := l2info.PrepareTxTo(from, &to, l2info.TransferGas, amount, nil) @@ -568,7 +842,7 @@ func TransferBalanceTo( // if l2client is not nil - will wait until balance appears in l2 func BridgeBalance( - t *testing.T, account string, amount *big.Int, l1info info, l2info info, l1client client, l2client client, ctx context.Context, + t *testing.T, account string, amount *big.Int, l1info info, l2info info, l1client *ethclient.Client, l2client *ethclient.Client, ctx context.Context, ) (*types.Transaction, *types.Receipt) { t.Helper() @@ -614,7 +888,7 @@ func BridgeBalance( break } TransferBalance(t, "Faucet", "User", big.NewInt(1), l1info, l1client, ctx) - if i > 20 { + if i > 200 { Fatal(t, "bridging failed") } <-time.After(time.Millisecond * 100) @@ -624,12 +898,27 @@ func BridgeBalance( return tx, res } +// AdvanceL1 sends dummy transactions to L1 to create blocks. +func AdvanceL1( + t *testing.T, + ctx context.Context, + l1client *ethclient.Client, + l1info *BlockchainTestInfo, + numBlocks int, +) { + for i := 0; i < numBlocks; i++ { + SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ + l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), + }) + } +} + func SendSignedTxesInBatchViaL1( t *testing.T, ctx context.Context, l1info *BlockchainTestInfo, - l1client arbutil.L1Interface, - l2client arbutil.L1Interface, + l1client *ethclient.Client, + l2client *ethclient.Client, delayedTxes types.Transactions, ) types.Receipts { delayedInboxContract, err := bridgegen.NewInbox(l1info.GetAddress("Inbox"), l1client) @@ -643,12 +932,7 @@ func SendSignedTxesInBatchViaL1( _, err = EnsureTxSucceeded(ctx, l1client, l1tx) Require(t, err) - // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in - for i := 0; i < 30; i++ { - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), - }) - } + AdvanceL1(t, ctx, l1client, l1info, 30) var receipts types.Receipts for _, tx := range delayedTxes { receipt, err := EnsureTxSucceeded(ctx, l2client, tx) @@ -667,7 +951,7 @@ func l2MessageBatchDataFromTxes(txes types.Transactions) ([]byte, error) { if err != nil { return nil, err } - binary.BigEndian.PutUint64(sizeBuf, uint64(len(txBytes)+1)) + binary.BigEndian.PutUint64(sizeBuf, uint64(len(txBytes))+1) l2Message = append(l2Message, sizeBuf...) l2Message = append(l2Message, arbos.L2MessageKind_SignedTx) l2Message = append(l2Message, txBytes...) @@ -679,8 +963,8 @@ func SendSignedTxViaL1( t *testing.T, ctx context.Context, l1info *BlockchainTestInfo, - l1client arbutil.L1Interface, - l2client arbutil.L1Interface, + l1client *ethclient.Client, + l2client *ethclient.Client, delayedTx *types.Transaction, ) *types.Receipt { delayedInboxContract, err := bridgegen.NewInbox(l1info.GetAddress("Inbox"), l1client) @@ -695,12 +979,7 @@ func SendSignedTxViaL1( _, err = EnsureTxSucceeded(ctx, l1client, l1tx) Require(t, err) - // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in - for i := 0; i < 30; i++ { - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), - }) - } + AdvanceL1(t, ctx, l1client, l1info, 30) receipt, err := EnsureTxSucceeded(ctx, l2client, delayedTx) Require(t, err) return receipt @@ -710,8 +989,8 @@ func SendUnsignedTxViaL1( t *testing.T, ctx context.Context, l1info *BlockchainTestInfo, - l1client arbutil.L1Interface, - l2client arbutil.L1Interface, + l1client *ethclient.Client, + l2client *ethclient.Client, templateTx *types.Transaction, ) *types.Receipt { delayedInboxContract, err := bridgegen.NewInbox(l1info.GetAddress("Inbox"), l1client) @@ -746,24 +1025,19 @@ func SendUnsignedTxViaL1( _, err = EnsureTxSucceeded(ctx, l1client, l1tx) Require(t, err) - // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in - for i := 0; i < 30; i++ { - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), - }) - } + AdvanceL1(t, ctx, l1client, l1info, 30) receipt, err := EnsureTxSucceeded(ctx, l2client, unsignedTx) Require(t, err) return receipt } -func GetBaseFee(t *testing.T, client client, ctx context.Context) *big.Int { +func GetBaseFee(t *testing.T, client *ethclient.Client, ctx context.Context) *big.Int { header, err := client.HeaderByNumber(ctx, nil) Require(t, err) return header.BaseFee } -func GetBaseFeeAt(t *testing.T, client client, ctx context.Context, blockNum *big.Int) *big.Int { +func GetBaseFeeAt(t *testing.T, client *ethclient.Client, ctx context.Context, blockNum *big.Int) *big.Int { header, err := client.HeaderByNumber(ctx, blockNum) Require(t, err) return header.BaseFee @@ -830,7 +1104,7 @@ func destroyRedisGroup(ctx context.Context, t *testing.T, streamName string, cli } } -func createTestValidationNode(t *testing.T, ctx context.Context, config *valnode.Config) (*valnode.ValidationNode, *node.Node) { +func createTestValidationNode(t *testing.T, ctx context.Context, config *valnode.Config, spawnerOpts ...server_arb.SpawnerOption) (*valnode.ValidationNode, *node.Node) { stackConf := node.DefaultConfig stackConf.HTTPPort = 0 stackConf.DataDir = "" @@ -847,7 +1121,7 @@ func createTestValidationNode(t *testing.T, ctx context.Context, config *valnode Require(t, err) configFetcher := func() *valnode.Config { return config } - valnode, err := valnode.CreateValidationNode(configFetcher, stack, nil) + valnode, err := valnode.CreateValidationNode(configFetcher, stack, nil, spawnerOpts...) Require(t, err) err = stack.Start() @@ -930,7 +1204,7 @@ func createTestL1BlockChain(t *testing.T, l1info info) (info, *ethclient.Client, stackConfig := testhelpers.CreateStackConfigForTest(t.TempDir()) l1info.GenerateAccount("Faucet") - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() chainConfig.ArbitrumChainParams = params.ArbitrumChainParams{} stack, err := node.New(stackConfig) @@ -947,6 +1221,7 @@ func createTestL1BlockChain(t *testing.T, l1info info) (info, *ethclient.Client, l1Genesis.BaseFee = big.NewInt(50 * params.GWei) nodeConf.Genesis = l1Genesis nodeConf.Miner.Etherbase = l1info.GetAddress("Faucet") + nodeConf.Miner.PendingFeeRecipient = l1info.GetAddress("Faucet") nodeConf.SyncMode = downloader.FullSync l1backend, err := eth.New(stack, &nodeConf) @@ -957,26 +1232,23 @@ func createTestL1BlockChain(t *testing.T, l1info info) (info, *ethclient.Client, catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon) stack.RegisterLifecycle(simBeacon) - tempKeyStore := keystore.NewPlaintextKeyStore(t.TempDir()) + tempKeyStore := keystore.NewKeyStore(t.TempDir(), keystore.LightScryptN, keystore.LightScryptP) faucetAccount, err := tempKeyStore.ImportECDSA(l1info.Accounts["Faucet"].PrivateKey, "passphrase") Require(t, err) Require(t, tempKeyStore.Unlock(faucetAccount, "passphrase")) l1backend.AccountManager().AddBackend(tempKeyStore) - l1backend.SetEtherbase(l1info.GetAddress("Faucet")) stack.RegisterLifecycle(&lifecycle{stop: func() error { - l1backend.StopMining() - return nil + return l1backend.Stop() }}) stack.RegisterAPIs([]rpc.API{{ Namespace: "eth", - Service: filters.NewFilterAPI(filters.NewFilterSystem(l1backend.APIBackend, filters.Config{}), false), + Service: filters.NewFilterAPI(filters.NewFilterSystem(l1backend.APIBackend, filters.Config{})), }}) stack.RegisterAPIs(tracers.APIs(l1backend.APIBackend)) Require(t, stack.Start()) - Require(t, l1backend.StartMining()) rpcClient := stack.Attach() @@ -985,8 +1257,8 @@ func createTestL1BlockChain(t *testing.T, l1info info) (info, *ethclient.Client, return l1info, l1Client, l1backend, stack } -func getInitMessage(ctx context.Context, t *testing.T, l1client client, addresses *chaininfo.RollupAddresses) *arbostypes.ParsedInitMessage { - bridge, err := arbnode.NewDelayedBridge(l1client, addresses.Bridge, addresses.DeployedAt) +func getInitMessage(ctx context.Context, t *testing.T, parentChainClient *ethclient.Client, addresses *chaininfo.RollupAddresses) *arbostypes.ParsedInitMessage { + bridge, err := arbnode.NewDelayedBridge(parentChainClient, addresses.Bridge, addresses.DeployedAt) Require(t, err) deployedAtBig := arbmath.UintToBig(addresses.DeployedAt) messages, err := bridge.LookupMessagesInRange(ctx, deployedAtBig, deployedAtBig, nil) @@ -1000,82 +1272,176 @@ func getInitMessage(ctx context.Context, t *testing.T, l1client client, addresse return initMessage } -func DeployOnTestL1( - t *testing.T, ctx context.Context, l1info info, l1client client, chainConfig *params.ChainConfig, wasmModuleRoot common.Hash, prodConfirmPeriodBlocks bool, +var ( + blockChallengeLeafHeight = uint64(1 << 5) // 32 + bigStepChallengeLeafHeight = uint64(1 << 10) + smallStepChallengeLeafHeight = uint64(1 << 10) +) + +func deployOnParentChain( + t *testing.T, + ctx context.Context, + parentChainInfo info, + parentChainClient *ethclient.Client, + parentChainReaderConfig *headerreader.Config, + chainConfig *params.ChainConfig, + wasmModuleRoot common.Hash, + prodConfirmPeriodBlocks bool, + chainSupportsBlobs bool, + deployBold bool, + delayBufferThreshold uint64, ) (*chaininfo.RollupAddresses, *arbostypes.ParsedInitMessage) { - l1info.GenerateAccount("RollupOwner") - l1info.GenerateAccount("Sequencer") - l1info.GenerateAccount("Validator") - l1info.GenerateAccount("User") - - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "RollupOwner", 30000, big.NewInt(9223372036854775807), nil), - l1info.PrepareTx("Faucet", "Sequencer", 30000, big.NewInt(9223372036854775807), nil), - l1info.PrepareTx("Faucet", "Validator", 30000, big.NewInt(9223372036854775807), nil), - l1info.PrepareTx("Faucet", "User", 30000, big.NewInt(9223372036854775807), nil)}) - - l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + parentChainInfo.GenerateAccount("RollupOwner") + parentChainInfo.GenerateAccount("Sequencer") + parentChainInfo.GenerateAccount("Validator") + parentChainInfo.GenerateAccount("User") + + SendWaitTestTransactions(t, ctx, parentChainClient, []*types.Transaction{ + parentChainInfo.PrepareTx("Faucet", "RollupOwner", parentChainInfo.TransferGas, big.NewInt(9223372036854775807), nil), + parentChainInfo.PrepareTx("Faucet", "Sequencer", parentChainInfo.TransferGas, big.NewInt(9223372036854775807), nil), + parentChainInfo.PrepareTx("Faucet", "Validator", parentChainInfo.TransferGas, big.NewInt(9223372036854775807), nil), + parentChainInfo.PrepareTx("Faucet", "User", parentChainInfo.TransferGas, big.NewInt(9223372036854775807), nil)}) + + parentChainTransactionOpts := parentChainInfo.GetDefaultTransactOpts("RollupOwner", ctx) serializedChainConfig, err := json.Marshal(chainConfig) Require(t, err) - arbSys, _ := precompilesgen.NewArbSys(types.ArbSysAddress, l1client) - l1Reader, err := headerreader.New(ctx, l1client, func() *headerreader.Config { return &headerreader.TestConfig }, arbSys) + arbSys, _ := precompilesgen.NewArbSys(types.ArbSysAddress, parentChainClient) + parentChainReader, err := headerreader.New(ctx, parentChainClient, func() *headerreader.Config { return parentChainReaderConfig }, arbSys) Require(t, err) - l1Reader.Start(ctx) - defer l1Reader.StopAndWait() + parentChainReader.Start(ctx) + defer parentChainReader.StopAndWait() nativeToken := common.Address{} maxDataSize := big.NewInt(117964) - addresses, err := deploy.DeployOnL1( - ctx, - l1Reader, - &l1TransactionOpts, - []common.Address{l1info.GetAddress("Sequencer")}, - l1info.GetAddress("RollupOwner"), - 0, - arbnode.GenerateRollupConfig(prodConfirmPeriodBlocks, wasmModuleRoot, l1info.GetAddress("RollupOwner"), chainConfig, serializedChainConfig, common.Address{}), - nativeToken, - maxDataSize, - false, - ) - Require(t, err) - l1info.SetContract("Bridge", addresses.Bridge) - l1info.SetContract("SequencerInbox", addresses.SequencerInbox) - l1info.SetContract("Inbox", addresses.Inbox) - l1info.SetContract("UpgradeExecutor", addresses.UpgradeExecutor) - initMessage := getInitMessage(ctx, t, l1client, addresses) + var addresses *chaininfo.RollupAddresses + if deployBold { + stakeToken, tx, _, err := boldMocksgen.DeployTestWETH9( + &parentChainTransactionOpts, + parentChainReader.Client(), + "Weth", + "WETH", + ) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, parentChainReader.Client(), tx) + Require(t, err) + miniStakeValues := []*big.Int{big.NewInt(5), big.NewInt(4), big.NewInt(3), big.NewInt(2), big.NewInt(1)} + genesisExecutionState := rollupgen.AssertionState{ + GlobalState: rollupgen.GlobalState{}, + MachineStatus: 1, // Finished + EndHistoryRoot: [32]byte{}, + } + bufferConfig := rollupgen.BufferConfig{ + Threshold: delayBufferThreshold, // number of blocks + Max: 14400, // 2 days of blocks + ReplenishRateInBasis: 500, // 5% + } + cfg := rollupgen.Config{ + MiniStakeValues: miniStakeValues, + ConfirmPeriodBlocks: 120, + StakeToken: stakeToken, + BaseStake: big.NewInt(1), + WasmModuleRoot: wasmModuleRoot, + Owner: parentChainTransactionOpts.From, + LoserStakeEscrow: parentChainTransactionOpts.From, + ChainId: chainConfig.ChainID, + ChainConfig: string(serializedChainConfig), + SequencerInboxMaxTimeVariation: rollupgen.ISequencerInboxMaxTimeVariation{ + DelayBlocks: big.NewInt(60 * 60 * 24 / 15), + FutureBlocks: big.NewInt(12), + DelaySeconds: big.NewInt(60 * 60 * 24), + FutureSeconds: big.NewInt(60 * 60), + }, + LayerZeroBlockEdgeHeight: new(big.Int).SetUint64(blockChallengeLeafHeight), + LayerZeroBigStepEdgeHeight: new(big.Int).SetUint64(bigStepChallengeLeafHeight), + LayerZeroSmallStepEdgeHeight: new(big.Int).SetUint64(smallStepChallengeLeafHeight), + GenesisAssertionState: genesisExecutionState, + GenesisInboxCount: common.Big0, + AnyTrustFastConfirmer: common.Address{}, + NumBigStepLevel: 3, + ChallengeGracePeriodBlocks: 3, + BufferConfig: bufferConfig, + } + wrappedClient := butil.NewBackendWrapper(parentChainReader.Client(), rpc.LatestBlockNumber) + boldAddresses, err := setup.DeployFullRollupStack( + ctx, + wrappedClient, + &parentChainTransactionOpts, + parentChainInfo.GetAddress("Sequencer"), + cfg, + setup.RollupStackConfig{ + UseMockBridge: false, + UseMockOneStepProver: false, + MinimumAssertionPeriod: 0, + }, + ) + Require(t, err) + addresses = &chaininfo.RollupAddresses{ + Bridge: boldAddresses.Bridge, + Inbox: boldAddresses.Inbox, + SequencerInbox: boldAddresses.SequencerInbox, + Rollup: boldAddresses.Rollup, + NativeToken: nativeToken, + UpgradeExecutor: boldAddresses.UpgradeExecutor, + ValidatorUtils: boldAddresses.ValidatorUtils, + ValidatorWalletCreator: boldAddresses.ValidatorWalletCreator, + StakeToken: stakeToken, + DeployedAt: boldAddresses.DeployedAt, + } + } else { + addresses, err = deploy.DeployOnParentChain( + ctx, + parentChainReader, + &parentChainTransactionOpts, + []common.Address{parentChainInfo.GetAddress("Sequencer")}, + parentChainInfo.GetAddress("RollupOwner"), + 0, + arbnode.GenerateRollupConfig(prodConfirmPeriodBlocks, wasmModuleRoot, parentChainInfo.GetAddress("RollupOwner"), chainConfig, serializedChainConfig, common.Address{}), + nativeToken, + maxDataSize, + chainSupportsBlobs, + ) + } + Require(t, err) + parentChainInfo.SetContract("Bridge", addresses.Bridge) + parentChainInfo.SetContract("SequencerInbox", addresses.SequencerInbox) + parentChainInfo.SetContract("Inbox", addresses.Inbox) + parentChainInfo.SetContract("UpgradeExecutor", addresses.UpgradeExecutor) + initMessage := getInitMessage(ctx, t, parentChainClient, addresses) return addresses, initMessage } func createL2BlockChain( - t *testing.T, l2info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, cacheConfig *gethexec.CachingConfig, + t *testing.T, l2info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, execConfig *gethexec.Config, wasmCacheTag uint32, ) (*BlockchainTestInfo, *node.Node, ethdb.Database, ethdb.Database, *core.BlockChain) { - return createL2BlockChainWithStackConfig(t, l2info, dataDir, chainConfig, nil, nil, cacheConfig) + return createNonL1BlockChainWithStackConfig(t, l2info, dataDir, chainConfig, nil, nil, execConfig, wasmCacheTag) } -func createL2BlockChainWithStackConfig( - t *testing.T, l2info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, initMessage *arbostypes.ParsedInitMessage, stackConfig *node.Config, cacheConfig *gethexec.CachingConfig, +func createNonL1BlockChainWithStackConfig( + t *testing.T, info *BlockchainTestInfo, dataDir string, chainConfig *params.ChainConfig, initMessage *arbostypes.ParsedInitMessage, stackConfig *node.Config, execConfig *gethexec.Config, wasmCacheTag uint32, ) (*BlockchainTestInfo, *node.Node, ethdb.Database, ethdb.Database, *core.BlockChain) { - if l2info == nil { - l2info = NewArbTestInfo(t, chainConfig.ChainID) + if info == nil { + info = NewArbTestInfo(t, chainConfig.ChainID) } - var stack *node.Node - var err error if stackConfig == nil { stackConfig = testhelpers.CreateStackConfigForTest(dataDir) } - stack, err = node.New(stackConfig) + if execConfig == nil { + execConfig = ExecConfigDefaultTest(t) + } + + stack, err := node.New(stackConfig) Require(t, err) chainData, err := stack.OpenDatabaseWithExtraOptions("l2chaindata", 0, 0, "l2chaindata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("l2chaindata")) Require(t, err) wasmData, err := stack.OpenDatabaseWithExtraOptions("wasm", 0, 0, "wasm/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("wasm")) Require(t, err) - chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, 0) + chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, wasmCacheTag, execConfig.StylusTarget.WasmTargets()) arbDb, err := stack.OpenDatabaseWithExtraOptions("arbitrumdata", 0, 0, "arbitrumdata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("arbitrumdata")) Require(t, err) - initReader := statetransfer.NewMemoryInitDataReader(&l2info.ArbInitData) + initReader := statetransfer.NewMemoryInitDataReader(&info.ArbInitData) if initMessage == nil { serializedChainConfig, err := json.Marshal(chainConfig) Require(t, err) @@ -1086,14 +1452,11 @@ func createL2BlockChainWithStackConfig( SerializedChainConfig: serializedChainConfig, } } - var coreCacheConfig *core.CacheConfig - if cacheConfig != nil { - coreCacheConfig = gethexec.DefaultCacheConfigFor(stack, cacheConfig) - } - blockchain, err := gethexec.WriteOrTestBlockChain(chainDb, coreCacheConfig, initReader, chainConfig, initMessage, ExecConfigDefaultTest().TxLookupLimit, 0) + coreCacheConfig := gethexec.DefaultCacheConfigFor(stack, &execConfig.Caching) + blockchain, err := gethexec.WriteOrTestBlockChain(chainDb, coreCacheConfig, initReader, chainConfig, initMessage, ExecConfigDefaultTest(t).TxLookupLimit, 0) Require(t, err) - return l2info, stack, chainDb, arbDb, blockchain + return info, stack, chainDb, arbDb, blockchain } func ClientForStack(t *testing.T, backend *node.Node) *ethclient.Client { @@ -1136,51 +1499,52 @@ func Create2ndNodeWithConfig( t *testing.T, ctx context.Context, first *arbnode.Node, - l1stack *node.Node, - l1info *BlockchainTestInfo, - l2InitData *statetransfer.ArbosInitializationInfo, + parentChainStack *node.Node, + parentChainInfo *BlockchainTestInfo, + chainInitData *statetransfer.ArbosInitializationInfo, nodeConfig *arbnode.Config, execConfig *gethexec.Config, stackConfig *node.Config, valnodeConfig *valnode.Config, addresses *chaininfo.RollupAddresses, initMessage *arbostypes.ParsedInitMessage, + wasmCacheTag uint32, ) (*ethclient.Client, *arbnode.Node) { if nodeConfig == nil { nodeConfig = arbnode.ConfigDefaultL1NonSequencerTest() } if execConfig == nil { - execConfig = ExecConfigDefaultNonSequencerTest() + execConfig = ExecConfigDefaultNonSequencerTest(t) } feedErrChan := make(chan error, 10) - l1rpcClient := l1stack.Attach() - l1client := ethclient.NewClient(l1rpcClient) + parentChainRpcClient := parentChainStack.Attach() + parentChainClient := ethclient.NewClient(parentChainRpcClient) if stackConfig == nil { stackConfig = testhelpers.CreateStackConfigForTest(t.TempDir()) } - l2stack, err := node.New(stackConfig) + chainStack, err := node.New(stackConfig) Require(t, err) - l2chainData, err := l2stack.OpenDatabaseWithExtraOptions("l2chaindata", 0, 0, "l2chaindata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("l2chaindata")) + chainData, err := chainStack.OpenDatabaseWithExtraOptions("l2chaindata", 0, 0, "l2chaindata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("l2chaindata")) Require(t, err) - wasmData, err := l2stack.OpenDatabaseWithExtraOptions("wasm", 0, 0, "wasm/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("wasm")) + wasmData, err := chainStack.OpenDatabaseWithExtraOptions("wasm", 0, 0, "wasm/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("wasm")) Require(t, err) - l2chainDb := rawdb.WrapDatabaseWithWasm(l2chainData, wasmData, 0) + chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmData, wasmCacheTag, execConfig.StylusTarget.WasmTargets()) - l2arbDb, err := l2stack.OpenDatabaseWithExtraOptions("arbitrumdata", 0, 0, "arbitrumdata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("arbitrumdata")) + arbDb, err := chainStack.OpenDatabaseWithExtraOptions("arbitrumdata", 0, 0, "arbitrumdata/", false, conf.PersistentConfigDefault.Pebble.ExtraOptions("arbitrumdata")) Require(t, err) - initReader := statetransfer.NewMemoryInitDataReader(l2InitData) + initReader := statetransfer.NewMemoryInitDataReader(chainInitData) - dataSigner := signature.DataSignerFromPrivateKey(l1info.GetInfoWithPrivKey("Sequencer").PrivateKey) - sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) - validatorTxOpts := l1info.GetDefaultTransactOpts("Validator", ctx) + dataSigner := signature.DataSignerFromPrivateKey(parentChainInfo.GetInfoWithPrivKey("Sequencer").PrivateKey) + sequencerTxOpts := parentChainInfo.GetDefaultTransactOpts("Sequencer", ctx) + validatorTxOpts := parentChainInfo.GetDefaultTransactOpts("Validator", ctx) firstExec := getExecNode(t, first) chainConfig := firstExec.ArbInterface.BlockChain().Config() - coreCacheConfig := gethexec.DefaultCacheConfigFor(l2stack, &execConfig.Caching) - l2blockchain, err := gethexec.WriteOrTestBlockChain(l2chainDb, coreCacheConfig, initReader, chainConfig, initMessage, ExecConfigDefaultTest().TxLookupLimit, 0) + coreCacheConfig := gethexec.DefaultCacheConfigFor(chainStack, &execConfig.Caching) + blockchain, err := gethexec.WriteOrTestBlockChain(chainDb, coreCacheConfig, initReader, chainConfig, initMessage, ExecConfigDefaultTest(t).TxLookupLimit, 0) Require(t, err) AddValNodeIfNeeded(t, ctx, nodeConfig, true, "", valnodeConfig.Wasm.RootPath) @@ -1188,19 +1552,19 @@ func Create2ndNodeWithConfig( Require(t, execConfig.Validate()) Require(t, nodeConfig.Validate()) configFetcher := func() *gethexec.Config { return execConfig } - currentExec, err := gethexec.CreateExecutionNode(ctx, l2stack, l2chainDb, l2blockchain, l1client, configFetcher) + currentExec, err := gethexec.CreateExecutionNode(ctx, chainStack, chainDb, blockchain, parentChainClient, configFetcher) Require(t, err) - currentNode, err := arbnode.CreateNode(ctx, l2stack, currentExec, l2arbDb, NewFetcherFromConfig(nodeConfig), l2blockchain.Config(), l1client, addresses, &validatorTxOpts, &sequencerTxOpts, dataSigner, feedErrChan, big.NewInt(1337), nil) + currentNode, err := arbnode.CreateNode(ctx, chainStack, currentExec, arbDb, NewFetcherFromConfig(nodeConfig), blockchain.Config(), parentChainClient, addresses, &validatorTxOpts, &sequencerTxOpts, dataSigner, feedErrChan, big.NewInt(1337), nil) Require(t, err) err = currentNode.Start(ctx) Require(t, err) - l2client := ClientForStack(t, l2stack) + chainClient := ClientForStack(t, chainStack) StartWatchChanErr(t, ctx, feedErrChan, currentNode) - return l2client, currentNode + return chainClient, currentNode } func GetBalance(t *testing.T, ctx context.Context, client *ethclient.Client, account common.Address) *big.Int { @@ -1220,7 +1584,7 @@ func authorizeDASKeyset( ctx context.Context, dasSignerKey *blsSignatures.PublicKey, l1info info, - l1client arbutil.L1Interface, + l1client *ethclient.Client, ) { if dasSignerKey == nil { return @@ -1254,7 +1618,7 @@ func setupConfigWithDAS( t *testing.T, ctx context.Context, dasModeString string, ) (*params.ChainConfig, *arbnode.Config, *das.LifecycleManager, string, *blsSignatures.PublicKey) { l1NodeConfigA := arbnode.ConfigDefaultL1Test() - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() var dbPath string var err error @@ -1262,10 +1626,10 @@ func setupConfigWithDAS( switch dasModeString { case "db": enableDbStorage = true - chainConfig = params.ArbitrumDevTestDASChainConfig() + chainConfig = chaininfo.ArbitrumDevTestDASChainConfig() case "files": enableFileStorage = true - chainConfig = params.ArbitrumDevTestDASChainConfig() + chainConfig = chaininfo.ArbitrumDevTestDASChainConfig() case "onchain": enableDas = false default: @@ -1460,6 +1824,57 @@ func logParser[T any](t *testing.T, source string, name string) func(*types.Log) } } +var ( + recordBlockInputsEnable = flag.Bool("recordBlockInputs.enable", true, "Whether to record block inputs as a json file") + recordBlockInputsWithSlug = flag.String("recordBlockInputs.WithSlug", "", "Slug directory for validationInputsWriter") + recordBlockInputsWithBaseDir = flag.String("recordBlockInputs.WithBaseDir", "", "Base directory for validationInputsWriter") + recordBlockInputsWithTimestampDirEnabled = flag.Bool("recordBlockInputs.WithTimestampDirEnabled", true, "Whether to add timestamp directory while recording block inputs") + recordBlockInputsWithBlockIdInFileNameEnabled = flag.Bool("recordBlockInputs.WithBlockIdInFileNameEnabled", true, "Whether to record block inputs using test specific block_id") +) + +// recordBlock writes a json file with all of the data needed to validate a block. +// +// This can be used as an input to the arbitrator prover to validate a block. +func recordBlock(t *testing.T, block uint64, builder *NodeBuilder, targets ...ethdb.WasmTarget) { + t.Helper() + flag.Parse() + if !*recordBlockInputsEnable { + return + } + ctx := builder.ctx + inboxPos := arbutil.MessageIndex(block) + for { + time.Sleep(250 * time.Millisecond) + batches, err := builder.L2.ConsensusNode.InboxTracker.GetBatchCount() + Require(t, err) + haveMessages, err := builder.L2.ConsensusNode.InboxTracker.GetBatchMessageCount(batches - 1) + Require(t, err) + if haveMessages >= inboxPos { + break + } + } + var options []inputs.WriterOption + options = append(options, inputs.WithTimestampDirEnabled(*recordBlockInputsWithTimestampDirEnabled)) + options = append(options, inputs.WithBlockIdInFileNameEnabled(*recordBlockInputsWithBlockIdInFileNameEnabled)) + if *recordBlockInputsWithBaseDir != "" { + options = append(options, inputs.WithBaseDir(*recordBlockInputsWithBaseDir)) + } + if *recordBlockInputsWithSlug != "" { + options = append(options, inputs.WithSlug(*recordBlockInputsWithSlug)) + } else { + options = append(options, inputs.WithSlug(t.Name())) + } + validationInputsWriter, err := inputs.NewWriter(options...) + Require(t, err) + inputJson, err := builder.L2.ConsensusNode.StatelessBlockValidator.ValidationInputsAt(ctx, inboxPos, targets...) + if err != nil { + Fatal(t, "failed to get validation inputs", block, err) + } + if err := validationInputsWriter.Write(&inputJson); err != nil { + Fatal(t, "failed to write validation inputs", block, err) + } +} + func populateMachineDir(t *testing.T, cr *github.ConsensusRelease) string { baseDir := t.TempDir() machineDir := baseDir + "/machines" diff --git a/system_tests/conditionaltx_test.go b/system_tests/conditionaltx_test.go index 286060e666..2d9140ffcd 100644 --- a/system_tests/conditionaltx_test.go +++ b/system_tests/conditionaltx_test.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/execution/gethexec" "github.com/offchainlabs/nitro/solgen/go/mocksgen" ) diff --git a/system_tests/contract_tx_test.go b/system_tests/contract_tx_test.go index 7d66e516b4..ad4f51204a 100644 --- a/system_tests/contract_tx_test.go +++ b/system_tests/contract_tx_test.go @@ -14,9 +14,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -51,9 +52,10 @@ func TestContractTxDeploy(t *testing.T) { 0xF3, // RETURN } var requestId common.Hash + // #nosec G115 requestId[0] = uint8(stateNonce) contractTx := &types.ArbitrumContractTx{ - ChainId: params.ArbitrumDevTestChainConfig().ChainID, + ChainId: chaininfo.ArbitrumDevTestChainConfig().ChainID, RequestId: requestId, From: from, GasFeeCap: big.NewInt(1e9), @@ -85,7 +87,7 @@ func TestContractTxDeploy(t *testing.T) { }, DelayedMessagesRead: delayedMessagesRead, }, - }) + }, nil) Require(t, err) txHash := types.NewTx(contractTx).Hash() diff --git a/system_tests/das_test.go b/system_tests/das_test.go index 9f4d153b6f..52703c879d 100644 --- a/system_tests/das_test.go +++ b/system_tests/das_test.go @@ -6,7 +6,9 @@ package arbtest import ( "context" "encoding/base64" + "errors" "io" + "log/slog" "math/big" "net" "net/http" @@ -19,43 +21,36 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbnode" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/blsSignatures" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/das" "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/testhelpers" - "golang.org/x/exp/slog" ) func startLocalDASServer( t *testing.T, ctx context.Context, dataDir string, - l1client arbutil.L1Interface, + l1client *ethclient.Client, seqInboxAddress common.Address, ) (*http.Server, *blsSignatures.PublicKey, das.BackendConfig, *das.RestfulDasServer, string) { keyDir := t.TempDir() pubkey, _, err := das.GenerateAndStoreKeys(keyDir) Require(t, err) - config := das.DataAvailabilityConfig{ - Enable: true, - Key: das.KeyConfig{ - KeyDir: keyDir, - }, - LocalFileStorage: das.LocalFileStorageConfig{ - Enable: true, - DataDir: dataDir, - }, - ParentChainNodeURL: "none", - RequestTimeout: 5 * time.Second, - } + config := das.DefaultDataAvailabilityConfig + config.Enable = true + config.Key = das.KeyConfig{KeyDir: keyDir} + config.ParentChainNodeURL = "none" + config.LocalFileStorage = das.DefaultLocalFileStorageConfig + config.LocalFileStorage.Enable = true + config.LocalFileStorage.DataDir = dataDir storageService, lifecycleManager, err := das.CreatePersistentStorageService(ctx, &config) defer lifecycleManager.StopAndWaitUntil(time.Second) @@ -206,7 +201,7 @@ func TestDASComplexConfigAndRestMirror(t *testing.T) { // Setup L1 chain and contracts builder := NewNodeBuilder(ctx).DefaultConfig(t, true) - builder.chainConfig = params.ArbitrumDevTestDASChainConfig() + builder.chainConfig = chaininfo.ArbitrumDevTestDASChainConfig() builder.BuildL1(t) arbSys, _ := precompilesgen.NewArbSys(types.ArbSysAddress, builder.L1.Client) @@ -327,3 +322,80 @@ func initTest(t *testing.T) { enableLogging(logLvl) } } + +func TestDASBatchPosterFallback(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Setup L1 + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + builder.chainConfig = chaininfo.ArbitrumDevTestDASChainConfig() + builder.BuildL1(t) + l1client := builder.L1.Client + l1info := builder.L1Info + + // Setup DAS server + dasDataDir := t.TempDir() + dasRpcServer, pubkey, backendConfig, _, restServerUrl := startLocalDASServer( + t, ctx, dasDataDir, l1client, builder.addresses.SequencerInbox) + authorizeDASKeyset(t, ctx, pubkey, l1info, l1client) + + // Setup sequence/batch-poster L2 node + builder.nodeConfig.DataAvailability.Enable = true + builder.nodeConfig.DataAvailability.RPCAggregator = aggConfigForBackend(backendConfig) + builder.nodeConfig.DataAvailability.RestAggregator = das.DefaultRestfulClientAggregatorConfig + builder.nodeConfig.DataAvailability.RestAggregator.Enable = true + builder.nodeConfig.DataAvailability.RestAggregator.Urls = []string{restServerUrl} + builder.nodeConfig.DataAvailability.ParentChainNodeURL = "none" + builder.nodeConfig.BatchPoster.DisableDapFallbackStoreDataOnChain = true // Disable DAS fallback + builder.nodeConfig.BatchPoster.ErrorDelay = time.Millisecond * 250 // Increase error delay because we expect errors + builder.L2Info = NewArbTestInfo(t, builder.chainConfig.ChainID) + builder.L2Info.GenerateAccount("User2") + cleanup := builder.BuildL2OnL1(t) + defer cleanup() + l2client := builder.L2.Client + l2info := builder.L2Info + + // Setup secondary L2 node + nodeConfigB := arbnode.ConfigDefaultL1NonSequencerTest() + nodeConfigB.BlockValidator.Enable = false + nodeConfigB.DataAvailability.Enable = true + nodeConfigB.DataAvailability.RestAggregator = das.DefaultRestfulClientAggregatorConfig + nodeConfigB.DataAvailability.RestAggregator.Enable = true + nodeConfigB.DataAvailability.RestAggregator.Urls = []string{restServerUrl} + nodeConfigB.DataAvailability.ParentChainNodeURL = "none" + nodeBParams := SecondNodeParams{ + nodeConfig: nodeConfigB, + initData: &l2info.ArbInitData, + } + l2B, cleanupB := builder.Build2ndNode(t, &nodeBParams) + defer cleanupB() + + // Check batch posting using the DAS + checkBatchPosting(t, ctx, l1client, l2client, l1info, l2info, big.NewInt(1e12), l2B.Client) + + // Shutdown the DAS + err := dasRpcServer.Shutdown(ctx) + Require(t, err) + + // Send 2nd transaction and check it doesn't arrive on second node + tx, _ := TransferBalanceTo(t, "Owner", l2info.GetAddress("User2"), big.NewInt(1e12), l2info, l2client, ctx) + _, err = WaitForTx(ctx, l2B.Client, tx.Hash(), time.Second*3) + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + Fatal(t, "expected context-deadline exceeded error, but got:", err) + } + + // Enable the DAP fallback and check the transaction on the second node. + // (We don't need to restart the node because of the hot-reload.) + builder.nodeConfig.BatchPoster.DisableDapFallbackStoreDataOnChain = false + _, err = WaitForTx(ctx, l2B.Client, tx.Hash(), time.Second*3) + Require(t, err) + l2balance, err := l2B.Client.BalanceAt(ctx, l2info.GetAddress("User2"), nil) + Require(t, err) + if l2balance.Cmp(big.NewInt(2e12)) != 0 { + Fatal(t, "Unexpected balance:", l2balance) + } + + // Send another transaction with fallback on + checkBatchPosting(t, ctx, l1client, l2client, l1info, l2info, big.NewInt(3e12), l2B.Client) +} diff --git a/system_tests/db_conversion_test.go b/system_tests/db_conversion_test.go index aca28262cb..d19629fade 100644 --- a/system_tests/db_conversion_test.go +++ b/system_tests/db_conversion_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie" + "github.com/offchainlabs/nitro/cmd/dbconv/dbconv" "github.com/offchainlabs/nitro/util/arbmath" ) diff --git a/system_tests/debugapi_test.go b/system_tests/debugapi_test.go index 30a2bee03e..fd1aa746a3 100644 --- a/system_tests/debugapi_test.go +++ b/system_tests/debugapi_test.go @@ -3,6 +3,8 @@ package arbtest import ( "context" "encoding/json" + "fmt" + "math/big" "testing" "github.com/ethereum/go-ethereum/common" @@ -10,9 +12,16 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/gasestimator" "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/arbos/retryables" + "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/arbmath" ) func TestDebugAPI(t *testing.T) { @@ -43,7 +52,7 @@ func TestDebugAPI(t *testing.T) { arbSys, err := precompilesgen.NewArbSys(types.ArbSysAddress, builder.L2.Client) Require(t, err) auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) - tx, err := arbSys.WithdrawEth(&auth, common.Address{}) + tx, err := arbSys.SendTxToL1(&auth, common.Address{}, []byte{}) Require(t, err) receipt, err := builder.L2.EnsureTxSucceeded(tx) Require(t, err) @@ -56,3 +65,230 @@ func TestDebugAPI(t *testing.T) { err = l2rpc.CallContext(ctx, &result, "debug_traceTransaction", tx.Hash(), &tracers.TraceConfig{Tracer: &flatCallTracer}) Require(t, err) } + +type account struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Code []byte `json:"code,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` +} +type prestateTrace struct { + Post map[common.Address]*account `json:"post"` + Pre map[common.Address]*account `json:"pre"` +} + +func TestPrestateTracingSimple(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanup := builder.Build(t) + defer cleanup() + + builder.L2Info.GenerateAccount("User2") + sender := builder.L2Info.GetAddress("Owner") + receiver := builder.L2Info.GetAddress("User2") + ownerOldBalance, err := builder.L2.Client.BalanceAt(ctx, sender, nil) + Require(t, err) + user2OldBalance, err := builder.L2.Client.BalanceAt(ctx, receiver, nil) + Require(t, err) + + value := big.NewInt(1e6) + tx := builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, value, nil) + Require(t, builder.L2.Client.SendTransaction(ctx, tx)) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + l2rpc := builder.L2.Stack.Attach() + + var result prestateTrace + traceConfig := map[string]interface{}{ + "tracer": "prestateTracer", + "tracerConfig": map[string]interface{}{ + "diffMode": true, + }, + } + err = l2rpc.CallContext(ctx, &result, "debug_traceTransaction", tx.Hash(), traceConfig) + Require(t, err) + + if !arbmath.BigEquals(result.Pre[sender].Balance.ToInt(), ownerOldBalance) { + Fatal(t, "Unexpected initial balance of sender") + } + if !arbmath.BigEquals(result.Pre[receiver].Balance.ToInt(), user2OldBalance) { + Fatal(t, "Unexpected initial balance of receiver") + } + if !arbmath.BigEquals(result.Post[sender].Balance.ToInt(), arbmath.BigSub(ownerOldBalance, value)) { + Fatal(t, "Unexpected final balance of sender") + } + if !arbmath.BigEquals(result.Post[receiver].Balance.ToInt(), value) { + Fatal(t, "Unexpected final balance of receiver") + } + if result.Post[sender].Nonce != result.Pre[sender].Nonce+1 { + Fatal(t, "sender nonce increment wasn't registered") + } + if result.Post[receiver].Nonce != result.Pre[receiver].Nonce { + Fatal(t, "receiver nonce shouldn't change") + } +} + +func TestPrestateTracingComplex(t *testing.T) { + builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) + defer teardown() + + // Test prestate tracing of a ArbitrumDepositTx type tx + faucetAddr := builder.L1Info.GetAddress("Faucet") + oldBalance, err := builder.L2.Client.BalanceAt(ctx, faucetAddr, nil) + Require(t, err) + + txOpts := builder.L1Info.GetDefaultTransactOpts("Faucet", ctx) + txOpts.Value = big.NewInt(13) + + l1tx, err := delayedInbox.DepositEth439370b1(&txOpts) + Require(t, err) + + l1Receipt, err := builder.L1.EnsureTxSucceeded(l1tx) + Require(t, err) + if l1Receipt.Status != types.ReceiptStatusSuccessful { + t.Errorf("Got transaction status: %v, want: %v", l1Receipt.Status, types.ReceiptStatusSuccessful) + } + waitForL1DelayBlocks(t, builder) + + l2Tx := lookupL2Tx(l1Receipt) + l2Receipt, err := builder.L2.EnsureTxSucceeded(l2Tx) + Require(t, err) + newBalance, err := builder.L2.Client.BalanceAt(ctx, faucetAddr, l2Receipt.BlockNumber) + Require(t, err) + if got := new(big.Int); got.Sub(newBalance, oldBalance).Cmp(txOpts.Value) != 0 { + t.Errorf("Got transferred: %v, want: %v", got, txOpts.Value) + } + + l2rpc := builder.L2.Stack.Attach() + var result prestateTrace + traceConfig := map[string]interface{}{ + "tracer": "prestateTracer", + "tracerConfig": map[string]interface{}{ + "diffMode": true, + }, + } + err = l2rpc.CallContext(ctx, &result, "debug_traceTransaction", l2Tx.Hash(), traceConfig) + Require(t, err) + + if _, ok := result.Pre[faucetAddr]; !ok { + Fatal(t, "Faucet account not found in the result of prestate tracer") + } + // Nonce shouldn't exist (in this case defaults to 0) in the Post map of the trace in DiffMode + if l2Tx.SkipAccountChecks() && result.Post[faucetAddr].Nonce != 0 { + Fatal(t, "Faucet account's nonce should remain unchanged ") + } + if !arbmath.BigEquals(result.Pre[faucetAddr].Balance.ToInt(), oldBalance) { + Fatal(t, "Unexpected initial balance of Faucet") + } + if !arbmath.BigEquals(result.Post[faucetAddr].Balance.ToInt(), arbmath.BigAdd(oldBalance, txOpts.Value)) { + Fatal(t, "Unexpected final balance of Faucet") + } + + // Test prestate tracing of a ArbitrumSubmitRetryableTx type tx + user2Address := builder.L2Info.GetAddress("User2") + beneficiaryAddress := builder.L2Info.GetAddress("Beneficiary") + + deposit := arbmath.BigMul(big.NewInt(1e12), big.NewInt(1e12)) + callValue := big.NewInt(1e6) + + nodeInterface, err := node_interfacegen.NewNodeInterface(types.NodeInterfaceAddress, builder.L2.Client) + Require(t, err, "failed to deploy NodeInterface") + + // estimate the gas needed to auto redeem the retryable + usertxoptsL2 := builder.L2Info.GetDefaultTransactOpts("Faucet", ctx) + usertxoptsL2.NoSend = true + usertxoptsL2.GasMargin = 0 + tx, err := nodeInterface.EstimateRetryableTicket( + &usertxoptsL2, + usertxoptsL2.From, + deposit, + user2Address, + callValue, + beneficiaryAddress, + beneficiaryAddress, + []byte{0x32, 0x42, 0x32, 0x88}, // increase the cost to beyond that of params.TxGas + ) + Require(t, err, "failed to estimate retryable submission") + estimate := tx.Gas() + expectedEstimate := params.TxGas + params.TxDataNonZeroGasEIP2028*4 + if float64(estimate) > float64(expectedEstimate)*(1+gasestimator.EstimateGasErrorRatio) { + t.Errorf("estimated retryable ticket at %v gas but expected %v, with error margin of %v", + estimate, + expectedEstimate, + gasestimator.EstimateGasErrorRatio, + ) + } + + // submit & auto redeem the retryable using the gas estimate + usertxoptsL1 := builder.L1Info.GetDefaultTransactOpts("Faucet", ctx) + usertxoptsL1.Value = deposit + l1tx, err = delayedInbox.CreateRetryableTicket( + &usertxoptsL1, + user2Address, + callValue, + big.NewInt(1e16), + beneficiaryAddress, + beneficiaryAddress, + arbmath.UintToBig(estimate), + big.NewInt(l2pricing.InitialBaseFeeWei*2), + []byte{0x32, 0x42, 0x32, 0x88}, + ) + Require(t, err) + + l1Receipt, err = builder.L1.EnsureTxSucceeded(l1tx) + Require(t, err) + if l1Receipt.Status != types.ReceiptStatusSuccessful { + Fatal(t, "l1Receipt indicated failure") + } + + waitForL1DelayBlocks(t, builder) + + l2Tx = lookupL2Tx(l1Receipt) + receipt, err := builder.L2.EnsureTxSucceeded(l2Tx) + Require(t, err) + if receipt.Status != types.ReceiptStatusSuccessful { + Fatal(t) + } + + l2balance, err := builder.L2.Client.BalanceAt(ctx, builder.L2Info.GetAddress("User2"), nil) + Require(t, err) + if !arbmath.BigEquals(l2balance, callValue) { + Fatal(t, "Unexpected balance:", l2balance) + } + + ticketId := receipt.Logs[0].Topics[1] + firstRetryTxId := receipt.Logs[1].Topics[2] + fmt.Println("submitretryable txid ", ticketId) + fmt.Println("auto redeem txid ", firstRetryTxId) + + // Trace ArbitrumSubmitRetryableTx + result = prestateTrace{} + err = l2rpc.CallContext(ctx, &result, "debug_traceTransaction", l2Tx.Hash(), traceConfig) + Require(t, err) + + escrowAddr := retryables.RetryableEscrowAddress(ticketId) + if _, ok := result.Pre[escrowAddr]; !ok { + Fatal(t, "Escrow account not found in the result of prestate tracer for a ArbitrumSubmitRetryableTx transaction") + } + + if !arbmath.BigEquals(result.Pre[escrowAddr].Balance.ToInt(), common.Big0) { + Fatal(t, "Unexpected initial balance of Escrow") + } + if !arbmath.BigEquals(result.Post[escrowAddr].Balance.ToInt(), callValue) { + Fatal(t, "Unexpected final balance of Escrow") + } + + // Trace ArbitrumRetryTx + result = prestateTrace{} + err = l2rpc.CallContext(ctx, &result, "debug_traceTransaction", firstRetryTxId, traceConfig) + Require(t, err) + + if !arbmath.BigEquals(result.Pre[user2Address].Balance.ToInt(), common.Big0) { + Fatal(t, "Unexpected initial balance of User2") + } + if !arbmath.BigEquals(result.Post[user2Address].Balance.ToInt(), callValue) { + Fatal(t, "Unexpected final balance of User2") + } +} diff --git a/system_tests/delayedinbox_test.go b/system_tests/delayedinbox_test.go index ca3e7b5999..346b0fbc2f 100644 --- a/system_tests/delayedinbox_test.go +++ b/system_tests/delayedinbox_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/solgen/go/bridgegen" ) diff --git a/system_tests/estimation_test.go b/system_tests/estimation_test.go index 284c709fad..37e1efe8c5 100644 --- a/system_tests/estimation_test.go +++ b/system_tests/estimation_test.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/gasestimator" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" @@ -161,7 +162,7 @@ func TestDifficultyForArbOSTen(t *testing.T) { defer cancel() builder := NewNodeBuilder(ctx).DefaultConfig(t, false) - builder.chainConfig.ArbitrumChainParams.InitialArbOSVersion = 10 + builder.chainConfig.ArbitrumChainParams.InitialArbOSVersion = params.ArbosVersion_10 cleanup := builder.Build(t) defer cleanup() @@ -214,7 +215,7 @@ func TestComponentEstimate(t *testing.T) { userBalance := big.NewInt(1e16) maxPriorityFeePerGas := big.NewInt(0) - maxFeePerGas := arbmath.BigMulByUfrac(l2BaseFee, 3, 2) + maxFeePerGas := arbmath.BigMulByUFrac(l2BaseFee, 3, 2) builder.L2Info.GenerateAccount("User") builder.L2.TransferBalance(t, "Owner", "User", userBalance, builder.L2Info) diff --git a/system_tests/eth_sync_test.go b/system_tests/eth_sync_test.go index 1f07f7c45f..ce9994fb1e 100644 --- a/system_tests/eth_sync_test.go +++ b/system_tests/eth_sync_test.go @@ -71,7 +71,7 @@ func TestEthSyncing(t *testing.T) { if progress == nil { Fatal(t, "eth_syncing returned nil but shouldn't have") } - for testClientB.ConsensusNode.TxStreamer.ExecuteNextMsg(ctx, testClientB.ExecNode) { + for testClientB.ConsensusNode.TxStreamer.ExecuteNextMsg(ctx) { } progress, err = testClientB.Client.SyncProgress(ctx) Require(t, err) diff --git a/system_tests/fast_confirm_test.go b/system_tests/fast_confirm_test.go index 4a679e5077..8eb71bffd4 100644 --- a/system_tests/fast_confirm_test.go +++ b/system_tests/fast_confirm_test.go @@ -10,6 +10,7 @@ package arbtest import ( "context" "errors" + "fmt" "math/big" "strings" "testing" @@ -32,6 +33,7 @@ import ( "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" "github.com/offchainlabs/nitro/staker" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" "github.com/offchainlabs/nitro/staker/validatorwallet" "github.com/offchainlabs/nitro/util" "github.com/offchainlabs/nitro/validator/valnode" @@ -79,15 +81,6 @@ func TestFastConfirmation(t *testing.T) { builder.L1.TransferBalance(t, "Faucet", "Validator", balance, builder.L1Info) l1auth := builder.L1Info.GetDefaultTransactOpts("Validator", ctx) - valWalletAddrPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2node.DeployInfo.ValidatorWalletCreator, 0, &l1auth, l2node.L1Reader, true) - Require(t, err) - valWalletAddr := *valWalletAddrPtr - valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2node.DeployInfo.ValidatorWalletCreator, 0, &l1auth, l2node.L1Reader, true) - Require(t, err) - if valWalletAddr == *valWalletAddrCheck { - Require(t, err, "didn't cache validator wallet address", valWalletAddr.String(), "vs", valWalletAddrCheck.String()) - } - rollup, err := rollupgen.NewRollupAdminLogic(l2node.DeployInfo.Rollup, builder.L1.Client) Require(t, err) @@ -96,28 +89,14 @@ func TestFastConfirmation(t *testing.T) { rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) Require(t, err, "unable to parse rollup ABI") - setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddr, srv.Address}, []bool{true, true}) - Require(t, err, "unable to generate setValidator calldata") - tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setValidatorCalldata) - Require(t, err, "unable to set validators") - _, err = builder.L1.EnsureTxSucceeded(tx) - Require(t, err) - setMinAssertPeriodCalldata, err := rollupABI.Pack("setMinimumAssertionPeriod", big.NewInt(1)) Require(t, err, "unable to generate setMinimumAssertionPeriod calldata") - tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setMinAssertPeriodCalldata) + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setMinAssertPeriodCalldata) Require(t, err, "unable to set minimum assertion period") _, err = builder.L1.EnsureTxSucceeded(tx) Require(t, err) - setAnyTrustFastConfirmerCalldata, err := rollupABI.Pack("setAnyTrustFastConfirmer", valWalletAddr) - Require(t, err, "unable to generate setAnyTrustFastConfirmer calldata") - tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setAnyTrustFastConfirmerCalldata) - Require(t, err, "unable to set anytrust fast confirmer") - _, err = builder.L1.EnsureTxSucceeded(tx) - Require(t, err) - - valConfig := staker.TestL1ValidatorConfig + valConfig := legacystaker.TestL1ValidatorConfig valConfig.EnableFastConfirmation = true parentChainID, err := builder.L1.Client.ChainID(ctx) if err != nil { @@ -138,6 +117,29 @@ func TestFastConfirmation(t *testing.T) { Require(t, err) valConfig.Strategy = "MakeNodes" + valWalletAddrPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2node.DeployInfo.ValidatorWalletCreator, 0, l2node.L1Reader, true, valWallet.DataPoster(), valWallet.GetExtraGas()) + Require(t, err) + valWalletAddr := *valWalletAddrPtr + valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2node.DeployInfo.ValidatorWalletCreator, 0, l2node.L1Reader, true, valWallet.DataPoster(), valWallet.GetExtraGas()) + Require(t, err) + if valWalletAddr == *valWalletAddrCheck { + Require(t, err, "didn't cache validator wallet address", valWalletAddr.String(), "vs", valWalletAddrCheck.String()) + } + + setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddr, srv.Address}, []bool{true, true}) + Require(t, err, "unable to generate setValidator calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setValidatorCalldata) + Require(t, err, "unable to set validators") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + setAnyTrustFastConfirmerCalldata, err := rollupABI.Pack("setAnyTrustFastConfirmer", valWalletAddr) + Require(t, err, "unable to generate setAnyTrustFastConfirmer calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setAnyTrustFastConfirmerCalldata) + Require(t, err, "unable to set anytrust fast confirmer") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) blockValidatorConfig := staker.TestBlockValidatorConfig @@ -156,11 +158,11 @@ func TestFastConfirmation(t *testing.T) { Require(t, err) err = valWallet.Initialize(ctx) Require(t, err) - stakerA, err := staker.NewStaker( + stakerA, err := legacystaker.NewStaker( l2node.L1Reader, valWallet, bind.CallOpts{}, - func() *staker.L1ValidatorConfig { return &valConfig }, + func() *legacystaker.L1ValidatorConfig { return &valConfig }, nil, stateless, nil, @@ -211,7 +213,7 @@ func TestFastConfirmation(t *testing.T) { latestConfirmAfterAct, err := rollup.LatestConfirmed(&bind.CallOpts{}) Require(t, err) if latestConfirmAfterAct <= latestConfirmBeforeAct { - Fatal(t, "staker A didn't advance the latest confirmed node") + Fatal(t, fmt.Sprintf("staker A didn't advance the latest confirmed node: want > %d, got: %d", latestConfirmBeforeAct, latestConfirmAfterAct)) } } @@ -278,15 +280,6 @@ func TestFastConfirmationWithSafe(t *testing.T) { builder.L1.TransferBalance(t, "Faucet", "ValidatorB", balance, builder.L1Info) l1authB := builder.L1Info.GetDefaultTransactOpts("ValidatorB", ctx) - valWalletAddrAPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, &l1authA, l2nodeA.L1Reader, true) - Require(t, err) - valWalletAddrA := *valWalletAddrAPtr - valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, &l1authA, l2nodeA.L1Reader, true) - Require(t, err) - if valWalletAddrA == *valWalletAddrCheck { - Require(t, err, "didn't cache validator wallet address", valWalletAddrA.String(), "vs", valWalletAddrCheck.String()) - } - rollup, err := rollupgen.NewRollupAdminLogic(l2nodeA.DeployInfo.Rollup, builder.L1.Client) Require(t, err) @@ -295,29 +288,14 @@ func TestFastConfirmationWithSafe(t *testing.T) { rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) Require(t, err, "unable to parse rollup ABI") - safeAddress := deploySafe(t, builder.L1, builder.L1.Client, deployAuth, []common.Address{valWalletAddrA, srv.Address}) - setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddrA, l1authB.From, srv.Address, safeAddress}, []bool{true, true, true, true}) - Require(t, err, "unable to generate setValidator calldata") - tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setValidatorCalldata) - Require(t, err, "unable to set validators") - _, err = builder.L1.EnsureTxSucceeded(tx) - Require(t, err) - setMinAssertPeriodCalldata, err := rollupABI.Pack("setMinimumAssertionPeriod", big.NewInt(1)) Require(t, err, "unable to generate setMinimumAssertionPeriod calldata") - tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setMinAssertPeriodCalldata) + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setMinAssertPeriodCalldata) Require(t, err, "unable to set minimum assertion period") _, err = builder.L1.EnsureTxSucceeded(tx) Require(t, err) - setAnyTrustFastConfirmerCalldata, err := rollupABI.Pack("setAnyTrustFastConfirmer", safeAddress) - Require(t, err, "unable to generate setAnyTrustFastConfirmer calldata") - tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setAnyTrustFastConfirmerCalldata) - Require(t, err, "unable to set anytrust fast confirmer") - _, err = builder.L1.EnsureTxSucceeded(tx) - Require(t, err) - - valConfigA := staker.TestL1ValidatorConfig + valConfigA := legacystaker.TestL1ValidatorConfig valConfigA.EnableFastConfirmation = true parentChainID, err := builder.L1.Client.ChainID(ctx) @@ -339,6 +317,30 @@ func TestFastConfirmationWithSafe(t *testing.T) { Require(t, err) valConfigA.Strategy = "MakeNodes" + valWalletAddrAPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, l2nodeA.L1Reader, true, valWalletA.DataPoster(), valWalletA.GetExtraGas()) + Require(t, err) + valWalletAddrA := *valWalletAddrAPtr + valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, l2nodeA.L1Reader, true, valWalletA.DataPoster(), valWalletA.GetExtraGas()) + Require(t, err) + if valWalletAddrA == *valWalletAddrCheck { + Require(t, err, "didn't cache validator wallet address", valWalletAddrA.String(), "vs", valWalletAddrCheck.String()) + } + + safeAddress := deploySafe(t, builder.L1, builder.L1.Client, deployAuth, []common.Address{valWalletAddrA, srv.Address}) + setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddrA, l1authB.From, srv.Address, safeAddress}, []bool{true, true, true, true}) + Require(t, err, "unable to generate setValidator calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setValidatorCalldata) + Require(t, err, "unable to set validators") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + setAnyTrustFastConfirmerCalldata, err := rollupABI.Pack("setAnyTrustFastConfirmer", safeAddress) + Require(t, err, "unable to generate setAnyTrustFastConfirmer calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setAnyTrustFastConfirmerCalldata) + Require(t, err, "unable to set anytrust fast confirmer") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) blockValidatorConfig := staker.TestBlockValidatorConfig @@ -357,11 +359,11 @@ func TestFastConfirmationWithSafe(t *testing.T) { Require(t, err) err = valWalletA.Initialize(ctx) Require(t, err) - stakerA, err := staker.NewStaker( + stakerA, err := legacystaker.NewStaker( l2nodeA.L1Reader, valWalletA, bind.CallOpts{}, - func() *staker.L1ValidatorConfig { return &valConfigA }, + func() *legacystaker.L1ValidatorConfig { return &valConfigA }, nil, statelessA, nil, @@ -391,7 +393,7 @@ func TestFastConfirmationWithSafe(t *testing.T) { } valWalletB, err := validatorwallet.NewEOA(dpB, l2nodeB.DeployInfo.Rollup, l2nodeB.L1Reader.Client(), func() uint64 { return 0 }) Require(t, err) - valConfigB := staker.TestL1ValidatorConfig + valConfigB := legacystaker.TestL1ValidatorConfig valConfigB.EnableFastConfirmation = true valConfigB.Strategy = "watchtower" statelessB, err := staker.NewStatelessBlockValidator( @@ -409,11 +411,11 @@ func TestFastConfirmationWithSafe(t *testing.T) { Require(t, err) err = valWalletB.Initialize(ctx) Require(t, err) - stakerB, err := staker.NewStaker( + stakerB, err := legacystaker.NewStaker( l2nodeB.L1Reader, valWalletB, bind.CallOpts{}, - func() *staker.L1ValidatorConfig { return &valConfigB }, + func() *legacystaker.L1ValidatorConfig { return &valConfigB }, nil, statelessB, nil, diff --git a/system_tests/fees_test.go b/system_tests/fees_test.go index ccca82e009..5540728df8 100644 --- a/system_tests/fees_test.go +++ b/system_tests/fees_test.go @@ -55,6 +55,12 @@ func TestSequencerFeePaid(t *testing.T) { l1Estimate, err := arbGasInfo.GetL1BaseFeeEstimate(callOpts) Require(t, err) + l1EstimateThroughGetL1GasPriceEstimate, err := arbGasInfo.GetL1GasPriceEstimate(callOpts) + Require(t, err) + if !arbmath.BigEquals(l1Estimate, l1EstimateThroughGetL1GasPriceEstimate) { + Fatal(t, "GetL1BaseFeeEstimate and GetL1GasPriceEstimate should return the same value") + } + baseFee := builder.L2.GetBaseFee(t) builder.L2Info.GasPrice = baseFee @@ -83,10 +89,10 @@ func TestSequencerFeePaid(t *testing.T) { feePaidForL2 := arbmath.BigMulByUint(gasPrice, gasUsedForL2) tipPaidToNet := arbmath.BigMulByUint(tipCap, receipt.GasUsedForL1) gotTip := arbmath.BigEquals(networkRevenue, arbmath.BigAdd(feePaidForL2, tipPaidToNet)) - if !gotTip && version == 9 { + if !gotTip && version == params.ArbosVersion_9 { Fatal(t, "network didn't receive expected payment", networkRevenue, feePaidForL2, tipPaidToNet) } - if gotTip && version != 9 { + if gotTip && version != params.ArbosVersion_9 { Fatal(t, "tips are somehow enabled") } @@ -104,7 +110,7 @@ func TestSequencerFeePaid(t *testing.T) { return networkRevenue, tipPaidToNet } - if version != 9 { + if version != params.ArbosVersion_9 { testFees(3) return } diff --git a/system_tests/forwarder_test.go b/system_tests/forwarder_test.go index 9fe419593e..843668454d 100644 --- a/system_tests/forwarder_test.go +++ b/system_tests/forwarder_test.go @@ -15,7 +15,9 @@ import ( "time" "github.com/alicebob/miniredis/v2" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/util/redisutil" ) @@ -38,7 +40,7 @@ func TestStaticForwarder(t *testing.T) { clientA := builder.L2.Client nodeConfigB := arbnode.ConfigDefaultL1Test() - execConfigB := ExecConfigDefaultTest() + execConfigB := ExecConfigDefaultTest(t) execConfigB.Sequencer.Enable = false nodeConfigB.Sequencer = false nodeConfigB.DelayedSequencer.Enable = false @@ -109,7 +111,7 @@ func createForwardingNode(t *testing.T, builder *NodeBuilder, ipcPath string, re nodeConfig.Sequencer = false nodeConfig.DelayedSequencer.Enable = false nodeConfig.BatchPoster.Enable = false - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.Sequencer.Enable = false execConfig.Forwarder.RedisUrl = redisUrl execConfig.ForwardingTarget = fallbackPath @@ -170,7 +172,7 @@ func waitForSequencerLockout(ctx context.Context, node *arbnode.Node, duration t case <-time.After(duration): return fmt.Errorf("no sequencer was chosen") default: - if c, err := node.SeqCoordinator.CurrentChosenSequencer(ctx); err == nil && c != "" { + if c, err := node.SeqCoordinator.RedisCoordinator().CurrentChosenSequencer(ctx); err == nil && c != "" { return nil } time.Sleep(100 * time.Millisecond) @@ -246,6 +248,7 @@ func TestRedisForwarder(t *testing.T) { for i := range seqClients { userA := user("A", i) builder.L2Info.GenerateAccount(userA) + // #nosec G115 tx := builder.L2Info.PrepareTx("Owner", userA, builder.L2Info.TransferGas, big.NewInt(1e12+int64(builder.L2Info.TransferGas)*builder.L2Info.GasPrice.Int64()), nil) err := fallbackClient.SendTransaction(ctx, tx) Require(t, err) diff --git a/system_tests/full_challenge_impl_test.go b/system_tests/full_challenge_impl_test.go index ddc229074c..4d902f87ba 100644 --- a/system_tests/full_challenge_impl_test.go +++ b/system_tests/full_challenge_impl_test.go @@ -27,12 +27,12 @@ import ( "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbstate" - "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/challengegen" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/ospgen" "github.com/offchainlabs/nitro/solgen/go/yulgen" "github.com/offchainlabs/nitro/staker" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_common" ) @@ -102,8 +102,8 @@ func CreateChallenge( auth, wasmModuleRoot, [2]uint8{ - staker.StatusFinished, - staker.StatusFinished, + legacystaker.StatusFinished, + legacystaker.StatusFinished, }, [2]mocksgen.GlobalState{ { @@ -178,7 +178,7 @@ func makeBatch(t *testing.T, l2Node *arbnode.Node, l2Info *BlockchainTestInfo, b Require(t, err, "failed to get batch metadata after adding batch:") } -func confirmLatestBlock(ctx context.Context, t *testing.T, l1Info *BlockchainTestInfo, backend arbutil.L1Interface) { +func confirmLatestBlock(ctx context.Context, t *testing.T, l1Info *BlockchainTestInfo, backend *ethclient.Client) { t.Helper() // With SimulatedBeacon running in on-demand block production mode, the // finalized block is considered to be be the nearest multiple of 32 less @@ -190,7 +190,7 @@ func confirmLatestBlock(ctx context.Context, t *testing.T, l1Info *BlockchainTes } } -func setupSequencerInboxStub(ctx context.Context, t *testing.T, l1Info *BlockchainTestInfo, l1Client arbutil.L1Interface, chainConfig *params.ChainConfig) (common.Address, *mocksgen.SequencerInboxStub, common.Address) { +func setupSequencerInboxStub(ctx context.Context, t *testing.T, l1Info *BlockchainTestInfo, l1Client *ethclient.Client, chainConfig *params.ChainConfig) (common.Address, *mocksgen.SequencerInboxStub, common.Address) { txOpts := l1Info.GetDefaultTransactOpts("deployer", ctx) bridgeAddr, tx, bridge, err := mocksgen.DeployBridgeUnproxied(&txOpts, l1Client) Require(t, err) @@ -398,7 +398,7 @@ func RunChallengeTest(t *testing.T, asserterIsCorrect bool, useStubs bool, chall Fatal(t, err) } defer asserterValidator.Stop() - asserterManager, err := staker.NewChallengeManager(ctx, l1Backend, &asserterTxOpts, asserterTxOpts.From, challengeManagerAddr, 1, asserterValidator, 0, 0) + asserterManager, err := legacystaker.NewChallengeManager(ctx, l1Backend, &asserterTxOpts, asserterTxOpts.From, challengeManagerAddr, 1, asserterValidator, 0, 0) if err != nil { Fatal(t, err) } @@ -415,7 +415,7 @@ func RunChallengeTest(t *testing.T, asserterIsCorrect bool, useStubs bool, chall Fatal(t, err) } defer challengerValidator.Stop() - challengerManager, err := staker.NewChallengeManager(ctx, l1Backend, &challengerTxOpts, challengerTxOpts.From, challengeManagerAddr, 1, challengerValidator, 0, 0) + challengerManager, err := legacystaker.NewChallengeManager(ctx, l1Backend, &challengerTxOpts, challengerTxOpts.From, challengeManagerAddr, 1, challengerValidator, 0, 0) if err != nil { Fatal(t, err) } diff --git a/system_tests/infra_fee_test.go b/system_tests/infra_fee_test.go index 9366fc204e..2e03eb0815 100644 --- a/system_tests/infra_fee_test.go +++ b/system_tests/infra_fee_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" diff --git a/system_tests/initialization_test.go b/system_tests/initialization_test.go index f0797404a9..4807a8e67a 100644 --- a/system_tests/initialization_test.go +++ b/system_tests/initialization_test.go @@ -10,7 +10,8 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/statetransfer" "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -21,6 +22,7 @@ func InitOneContract(prand *testhelpers.PseudoRandomDataSource) (*statetransfer. storageMap := make(map[common.Hash]common.Hash) code := []byte{0x60, 0x0} // PUSH1 0 sum := big.NewInt(0) + // #nosec G115 numCells := int(prand.GetUint64() % 1000) for i := 0; i < numCells; i++ { storageAddr := prand.GetHash() @@ -49,7 +51,7 @@ func TestInitContract(t *testing.T) { defer cancel() expectedSums := make(map[common.Address]*big.Int) prand := testhelpers.NewPseudoRandomDataSource(t, 1) - l2info := NewArbTestInfo(t, params.ArbitrumDevTestChainConfig().ChainID) + l2info := NewArbTestInfo(t, chaininfo.ArbitrumDevTestChainConfig().ChainID) for i := 0; i < 50; i++ { contractData, sum := InitOneContract(prand) accountAddress := prand.GetAddress() diff --git a/system_tests/l3_test.go b/system_tests/l3_test.go new file mode 100644 index 0000000000..97eabcee78 --- /dev/null +++ b/system_tests/l3_test.go @@ -0,0 +1,53 @@ +package arbtest + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/offchainlabs/nitro/arbnode" +) + +func TestSimpleL3(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanupL1AndL2 := builder.Build(t) + defer cleanupL1AndL2() + + cleanupL3FirstNode := builder.BuildL3OnL2(t) + defer cleanupL3FirstNode() + firstNodeTestClient := builder.L3 + + secondNodeNodeConfig := arbnode.ConfigDefaultL1NonSequencerTest() + secondNodeTestClient, cleanupL3SecondNode := builder.Build2ndNodeOnL3(t, &SecondNodeParams{nodeConfig: secondNodeNodeConfig}) + defer cleanupL3SecondNode() + + accountName := "User2" + builder.L3Info.GenerateAccount(accountName) + tx := builder.L3Info.PrepareTx("Owner", accountName, builder.L3Info.TransferGas, big.NewInt(1e12), nil) + + err := firstNodeTestClient.Client.SendTransaction(ctx, tx) + Require(t, err) + + // Checks that first node has the correct balance + _, err = firstNodeTestClient.EnsureTxSucceeded(tx) + Require(t, err) + l2balance, err := firstNodeTestClient.Client.BalanceAt(ctx, builder.L3Info.GetAddress(accountName), nil) + Require(t, err) + if l2balance.Cmp(big.NewInt(1e12)) != 0 { + t.Fatal("Unexpected balance:", l2balance) + } + + // Checks that second node has the correct balance + _, err = WaitForTx(ctx, secondNodeTestClient.Client, tx.Hash(), time.Second*15) + Require(t, err) + l2balance, err = secondNodeTestClient.Client.BalanceAt(ctx, builder.L3Info.GetAddress(accountName), nil) + Require(t, err) + if l2balance.Cmp(big.NewInt(1e12)) != 0 { + t.Fatal("Unexpected balance:", l2balance) + } +} diff --git a/system_tests/log_subscription_test.go b/system_tests/log_subscription_test.go index e4402533a6..4d38ea6e9c 100644 --- a/system_tests/log_subscription_test.go +++ b/system_tests/log_subscription_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" ) diff --git a/system_tests/meaningless_reorg_test.go b/system_tests/meaningless_reorg_test.go index 06a5d3d2b3..350b21a6cf 100644 --- a/system_tests/meaningless_reorg_test.go +++ b/system_tests/meaningless_reorg_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" ) diff --git a/system_tests/mock_machine_test.go b/system_tests/mock_machine_test.go new file mode 100644 index 0000000000..ea7fcbaef1 --- /dev/null +++ b/system_tests/mock_machine_test.go @@ -0,0 +1,41 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package arbtest + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/validator/server_arb" +) + +// IncorrectIntermediateMachine will report an incorrect hash while running from incorrectStep onwards. +// However, it'll reach the correct final hash and global state once finished. +type IncorrectIntermediateMachine struct { + server_arb.MachineInterface + incorrectStep uint64 +} + +var _ server_arb.MachineInterface = (*IncorrectIntermediateMachine)(nil) + +func NewIncorrectIntermediateMachine(inner server_arb.MachineInterface, incorrectStep uint64) *IncorrectIntermediateMachine { + return &IncorrectIntermediateMachine{ + MachineInterface: inner, + incorrectStep: incorrectStep, + } +} + +func (m *IncorrectIntermediateMachine) CloneMachineInterface() server_arb.MachineInterface { + return &IncorrectIntermediateMachine{ + MachineInterface: m.MachineInterface.CloneMachineInterface(), + incorrectStep: m.incorrectStep, + } +} + +func (m *IncorrectIntermediateMachine) Hash() common.Hash { + h := m.MachineInterface.Hash() + if m.GetStepCount() >= m.incorrectStep && m.IsRunning() { + h[0] ^= 0xFF + } + return h +} diff --git a/system_tests/nodeinterface_test.go b/system_tests/nodeinterface_test.go index 17bfb18892..5c32dcf20f 100644 --- a/system_tests/nodeinterface_test.go +++ b/system_tests/nodeinterface_test.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" ) @@ -163,6 +164,7 @@ func TestGetL1Confirmations(t *testing.T) { numTransactions := 200 + // #nosec G115 if l1Confs >= uint64(numTransactions) { t.Fatalf("L1Confirmations for latest block %v is already %v (over %v)", genesisBlock.Number(), l1Confs, numTransactions) } @@ -175,6 +177,7 @@ func TestGetL1Confirmations(t *testing.T) { Require(t, err) // Allow a gap of 10 for asynchronicity, just in case + // #nosec G115 if l1Confs+10 < uint64(numTransactions) { t.Fatalf("L1Confirmations for latest block %v is only %v (did not hit expected %v)", genesisBlock.Number(), l1Confs, numTransactions) } diff --git a/system_tests/outbox_test.go b/system_tests/outbox_test.go index 739d756a31..10d1ebec42 100644 --- a/system_tests/outbox_test.go +++ b/system_tests/outbox_test.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/gethhook" "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" @@ -53,7 +54,6 @@ func TestP256VerifyEnabled(t *testing.T) { func TestOutboxProofs(t *testing.T) { t.Parallel() gethhook.RequireHookedGeth() - rand.Seed(time.Now().UTC().UnixNano()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -146,6 +146,7 @@ func TestOutboxProofs(t *testing.T) { treeSize := root.size balanced := treeSize == arbmath.NextPowerOf2(treeSize)/2 + // #nosec G115 treeLevels := int(arbmath.Log2ceil(treeSize)) // the # of levels in the tree proofLevels := treeLevels - 1 // the # of levels where a hash is needed (all but root) walkLevels := treeLevels // the # of levels we need to consider when building walks @@ -174,6 +175,7 @@ func TestOutboxProofs(t *testing.T) { sibling := place ^ which position := merkletree.LevelAndLeaf{ + // #nosec G115 Level: uint64(level), Leaf: sibling, } @@ -200,6 +202,7 @@ func TestOutboxProofs(t *testing.T) { leaf := total - 1 // preceding it. We subtract 1 since we count from 0 partial := merkletree.LevelAndLeaf{ + // #nosec G115 Level: uint64(level), Leaf: leaf, } @@ -288,6 +291,7 @@ func TestOutboxProofs(t *testing.T) { step.Leaf += 1 << step.Level // we start on the min partial's zero-hash sibling known[step] = zero + // #nosec G115 for step.Level < uint64(treeLevels) { curr, ok := known[step] diff --git a/system_tests/overflow_assertions_test.go b/system_tests/overflow_assertions_test.go new file mode 100644 index 0000000000..c024a43070 --- /dev/null +++ b/system_tests/overflow_assertions_test.go @@ -0,0 +1,316 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see: +// https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +//go:build challengetest && !race + +package arbtest + +import ( + "context" + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/ccoveille/go-safecast" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + + protocol "github.com/offchainlabs/bold/chain-abstraction" + challengemanager "github.com/offchainlabs/bold/challenge-manager" + modes "github.com/offchainlabs/bold/challenge-manager/types" + l2stateprovider "github.com/offchainlabs/bold/layer2-state-provider" + "github.com/offchainlabs/bold/solgen/go/bridgegen" + "github.com/offchainlabs/bold/solgen/go/mocksgen" + "github.com/offchainlabs/bold/solgen/go/rollupgen" + "github.com/offchainlabs/bold/testing/setup" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/staker/bold" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/validator/valnode" +) + +func TestOverflowAssertions(t *testing.T) { + // Get a simulated geth backend running. + // + // Create enough messages in batches to overflow the block level challenge + // height. (height == 32, messages = 45) + // + // Start the challenge manager with a minimumAssertionPeriod of 7 and make + // sure that it posts overflow-assertions right away instead of waiting for + // the 7 blocks to pass. + goodDir, err := os.MkdirTemp("", "good_*") + Require(t, err) + t.Cleanup(func() { + Require(t, os.RemoveAll(goodDir)) + }) + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + l2chainConfig := chaininfo.ArbitrumDevTestChainConfig() + l2info := NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(l2chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + // This is important to show that overflow assertions don't wait. + minAssertionBlocks := int64(7) + ownerBal := big.NewInt(params.Ether) + ownerBal.Mul(ownerBal, big.NewInt(1_000_000)) + l2info.GenerateGenesisAccount("Owner", ownerBal) + sconf := setup.RollupStackConfig{ + UseMockBridge: false, + UseMockOneStepProver: false, + MinimumAssertionPeriod: minAssertionBlocks, + } + + _, l2node, _, _, l1info, _, l1client, l1stack, assertionChain, _ := createTestNodeOnL1ForBoldProtocol(t, ctx, true, nil, l2chainConfig, nil, sconf, l2info) + defer requireClose(t, l1stack) + defer l2node.StopAndWait() + + // Make sure we shut down test functionality before the rest of the node + ctx, cancelCtx = context.WithCancel(ctx) + defer cancelCtx() + + go keepChainMoving(t, ctx, l1info, l1client) + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + TransferBalance(t, "Faucet", "Asserter", balance, l1info, l1client, ctx) + + valCfg := valnode.TestValidationConfig + valCfg.UseJit = false + _, valStack := createTestValidationNode(t, ctx, &valCfg) + blockValidatorConfig := staker.TestBlockValidatorConfig + + stateless, err := staker.NewStatelessBlockValidator( + l2node.InboxReader, + l2node.InboxTracker, + l2node.TxStreamer, + l2node.Execution, + l2node.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = stateless.Start(ctx) + Require(t, err) + + blockValidator, err := staker.NewBlockValidator( + stateless, + l2node.InboxTracker, + l2node.TxStreamer, + StaticFetcherFrom(t, &blockValidatorConfig), + nil, + ) + Require(t, err) + Require(t, blockValidator.Initialize(ctx)) + Require(t, blockValidator.Start(ctx)) + + stateManager, err := bold.NewBOLDStateProvider( + blockValidator, + stateless, + l2stateprovider.Height(blockChallengeLeafHeight), + &bold.StateProviderConfig{ + ValidatorName: "good", + MachineLeavesCachePath: goodDir, + CheckBatchFinality: false, + }, + goodDir, + ) + Require(t, err) + + Require(t, l2node.Start(ctx)) + + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + honestSeqInbox := l1info.GetAddress("SequencerInbox") + honestSeqInboxBinding, err := bridgegen.NewSequencerInbox(honestSeqInbox, l1client) + Require(t, err) + + // Post batches to the honest and inbox. + seqInboxABI, err := abi.JSON(strings.NewReader(bridgegen.SequencerInboxABI)) + Require(t, err) + + honestUpgradeExec, err := mocksgen.NewUpgradeExecutorMock(l1info.GetAddress("UpgradeExecutor"), l1client) + Require(t, err) + data, err := seqInboxABI.Pack( + "setIsBatchPoster", + sequencerTxOpts.From, + true, + ) + Require(t, err) + honestRollupOwnerOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + _, err = honestUpgradeExec.ExecuteCall(&honestRollupOwnerOpts, honestSeqInbox, data) + Require(t, err) + + // Post enough messages (45 across 2 batches) to overflow the block level + // challenge height (32). + totalMessagesPosted := int64(0) + numMessagesPerBatch := int64(32) + divergeAt := int64(-1) + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, honestSeqInboxBinding, honestSeqInbox, numMessagesPerBatch, divergeAt) + totalMessagesPosted += numMessagesPerBatch + + numMessagesPerBatch = int64(13) + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, honestSeqInboxBinding, honestSeqInbox, numMessagesPerBatch, divergeAt) + totalMessagesPosted += numMessagesPerBatch + + bc, err := l2node.InboxTracker.GetBatchCount() + Require(t, err) + msgs, err := l2node.InboxTracker.GetBatchMessageCount(bc - 1) + Require(t, err) + + t.Logf("Node batch count %d, msgs %d", bc, msgs) + + // Wait for the node to catch up. + nodeExec, ok := l2node.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + for { + latest := nodeExec.Backend.APIBackend().CurrentHeader() + isCaughtUp := latest.Number.Uint64() == uint64(totalMessagesPosted) + if isCaughtUp { + break + } + time.Sleep(time.Millisecond * 200) + } + + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + + // Wait until the validator has validated the batches. + for { + lastInfo, err := blockValidator.ReadLastValidatedInfo() + if lastInfo == nil || err != nil { + continue + } + t.Log("Batch", lastInfo.GlobalState.Batch, "Total", totalBatches-1) + if lastInfo.GlobalState.Batch >= totalBatches-1 { + break + } + time.Sleep(time.Millisecond * 200) + } + + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + stateManager, + nil, // Api db + ) + + stackOpts := []challengemanager.StackOpt{ + challengemanager.StackWithName("default"), + challengemanager.StackWithMode(modes.MakeMode), + challengemanager.StackWithPostingInterval(time.Second), + challengemanager.StackWithPollingInterval(time.Millisecond * 500), + challengemanager.StackWithAverageBlockCreationTime(time.Second), + } + + manager, err := challengemanager.NewChallengeStack( + assertionChain, + provider, + stackOpts..., + ) + Require(t, err) + manager.Start(ctx) + + filterer, err := rollupgen.NewRollupUserLogicFilterer(assertionChain.RollupAddress(), assertionChain.Backend()) + Require(t, err) + + // The goal of this test is to observe: + // + // 1. The genisis assertion (non-overflow) + // 2. The assertion of the first 32 blocks of the two batches manually set up + // above (non-overflow) + // 3. The overflow assertion that should be posted in fewer than + // minAssertionBlocks. (overflow) + // 4. One more normal assertion in >= minAssertionBlocks. (non-overflow) + + overflow := true + nonOverflow := false + expectedAssertions := []bool{nonOverflow, nonOverflow, overflow, nonOverflow} + mab64, err := safecast.ToUint64(minAssertionBlocks) + Require(t, err) + + lastInboxMax := uint64(0) + lastAssertionBlock := uint64(0) + fromBlock := uint64(0) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for len(expectedAssertions) > 0 { + select { + case <-ticker.C: + latestBlock, err := l1client.HeaderByNumber(ctx, nil) + Require(t, err) + toBlock := latestBlock.Number.Uint64() + if fromBlock >= toBlock { + continue + } + filterOpts := &bind.FilterOpts{ + Start: fromBlock, + End: &toBlock, + Context: ctx, + } + it, err := filterer.FilterAssertionCreated(filterOpts, nil, nil) + Require(t, err) + for it.Next() { + if it.Error() != nil { + t.Fatalf("Error in filter iterator: %v", it.Error()) + } + t.Log("Received event of assertion created!") + assertionHash := protocol.AssertionHash{Hash: it.Event.AssertionHash} + creationInfo, err := assertionChain.ReadAssertionCreationInfo(ctx, assertionHash) + Require(t, err) + t.Logf("Created assertion in block: %d", creationInfo.CreationBlock) + newState := protocol.GoGlobalStateFromSolidity(creationInfo.AfterState.GlobalState) + t.Logf("NewState PosInBatch: %d", newState.PosInBatch) + inboxMax := creationInfo.InboxMaxCount.Uint64() + t.Logf("InboxMax: %d", inboxMax) + blocks := creationInfo.CreationBlock - lastAssertionBlock + // PosInBatch == 0 && inboxMax > lastInboxMax means it is NOT an overflow assertion. + if newState.PosInBatch == 0 && inboxMax > lastInboxMax { + if expectedAssertions[0] == overflow { + t.Errorf("Expected overflow assertion, got non-overflow assertion") + } + if blocks < mab64 { + t.Errorf("non-overflow assertions should have >= =%d blocks between them. Got: %d", mab64, blocks) + } + } else { + if expectedAssertions[0] == nonOverflow { + t.Errorf("Expected non-overflow assertion, got overflow assertion") + } + if blocks >= mab64 { + t.Errorf("overflow assertions should not have %d blocks between them. Got: %d", mab64, blocks) + } + } + lastAssertionBlock = creationInfo.CreationBlock + lastInboxMax = inboxMax + expectedAssertions = expectedAssertions[1:] + } + fromBlock = toBlock + 1 + case <-ctx.Done(): + return + } + } + // PASS: All expected assertions were seen. +} diff --git a/system_tests/precompile_doesnt_revert_test.go b/system_tests/precompile_doesnt_revert_test.go new file mode 100644 index 0000000000..fc4b0e745e --- /dev/null +++ b/system_tests/precompile_doesnt_revert_test.go @@ -0,0 +1,249 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package arbtest + +import ( + "context" + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" +) + +// DoesntRevert tests are useful to check if precompile calls revert due to differences in the +// return types of a contract between go and solidity. +// They are not a substitute for unit tests, as they don't test the actual functionality of the precompile. + +func TestArbAddressTableDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAddressTable, err := precompilesgen.NewArbAddressTable(types.ArbAddressTableAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + exists, err := arbAddressTable.AddressExists(callOpts, addr) + Require(t, err) + if exists { + Fatal(t, "expected address to not exist") + } + + tx, err := arbAddressTable.Register(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + idx, err := arbAddressTable.Lookup(callOpts, addr) + Require(t, err) + + retrievedAddr, err := arbAddressTable.LookupIndex(callOpts, idx) + Require(t, err) + if retrievedAddr.Cmp(addr) != 0 { + Fatal(t, "expected retrieved address to be", addr, "got", retrievedAddr) + } + + size, err := arbAddressTable.Size(callOpts) + Require(t, err) + if size.Cmp(big.NewInt(1)) != 0 { + Fatal(t, "expected size to be 1, got", size) + } + + tx, err = arbAddressTable.Compress(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + res := []uint8{128} + _, _, err = arbAddressTable.Decompress(callOpts, res, big.NewInt(0)) + Require(t, err) +} + +func TestArbAggregatorDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + tx, err := arbAggregator.SetFeeCollector(&auth, l1pricing.BatchPosterAddress, common.Address{}) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + _, err = arbAggregator.GetFeeCollector(callOpts, l1pricing.BatchPosterAddress) + Require(t, err) +} + +func TestArbosTestDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbosTest, err := precompilesgen.NewArbosTest(types.ArbosTestAddress, builder.L2.Client) + Require(t, err) + + err = arbosTest.BurnArbGas(callOpts, big.NewInt(1)) + Require(t, err) +} + +func TestArbSysDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbSys, err := precompilesgen.NewArbSys(types.ArbSysAddress, builder.L2.Client) + Require(t, err) + + addr1 := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + addr2 := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + _, err = arbSys.MapL1SenderContractAddressToL2Alias(callOpts, addr1, addr2) + Require(t, err) +} + +func TestArbOwnerDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + chainConfig := chaininfo.ArbitrumDevTestChainConfig() + chainConfig.ArbitrumChainParams.MaxCodeSize = 100 + serializedChainConfig, err := json.Marshal(chainConfig) + Require(t, err) + tx, err := arbOwner.SetChainConfig(&auth, string(serializedChainConfig)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + tx, err = arbOwner.SetAmortizedCostCapBips(&auth, 77734) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + tx, err = arbOwner.ReleaseL1PricerSurplusFunds(&auth, big.NewInt(1)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + tx, err = arbOwner.SetL2BaseFee(&auth, big.NewInt(1)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) +} + +func TestArbGasInfoDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + arbGasInfo, err := precompilesgen.NewArbGasInfo(types.ArbGasInfoAddress, builder.L2.Client) + Require(t, err) + + _, err = arbGasInfo.GetGasBacklog(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetLastL1PricingUpdateTime(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetL1PricingFundsDueForRewards(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetL1PricingUnitsSinceUpdate(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetLastL1PricingSurplus(callOpts) + Require(t, err) + + _, _, _, err = arbGasInfo.GetPricesInArbGas(callOpts) + Require(t, err) + + _, _, _, err = arbGasInfo.GetPricesInArbGasWithAggregator(callOpts, addr) + Require(t, err) + + _, err = arbGasInfo.GetAmortizedCostCapBips(callOpts) + Require(t, err) + + _, err = arbGasInfo.GetL1FeesAvailable(callOpts) + Require(t, err) + + _, _, _, _, _, _, err = arbGasInfo.GetPricesInWeiWithAggregator(callOpts, addr) + Require(t, err) +} + +func TestArbRetryableTxDoesntRevert(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(common.HexToAddress("6e"), builder.L2.Client) + Require(t, err) + + _, err = arbRetryableTx.GetCurrentRedeemer(callOpts) + Require(t, err) +} diff --git a/system_tests/precompile_fuzz_test.go b/system_tests/precompile_fuzz_test.go index 8ab133cf58..82dd393ea2 100644 --- a/system_tests/precompile_fuzz_test.go +++ b/system_tests/precompile_fuzz_test.go @@ -12,10 +12,11 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/gethhook" "github.com/offchainlabs/nitro/precompiles" ) @@ -32,7 +33,7 @@ func FuzzPrecompiles(f *testing.F) { panic(err) } burner := burn.NewSystemBurner(nil, false) - chainConfig := params.ArbitrumDevTestChainConfig() + chainConfig := chaininfo.ArbitrumDevTestChainConfig() _, err = arbosState.InitializeArbosState(sdb, burner, chainConfig, arbostypes.TestInitMessage) if err != nil { panic(err) diff --git a/system_tests/precompile_test.go b/system_tests/precompile_test.go index 9e829124ee..5bc6315086 100644 --- a/system_tests/precompile_test.go +++ b/system_tests/precompile_test.go @@ -7,12 +7,18 @@ import ( "context" "fmt" "math/big" + "sort" "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" @@ -22,7 +28,10 @@ func TestPurePrecompileMethodCalls(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + arbosVersion := params.ArbosVersion_31 + builder := NewNodeBuilder(ctx). + DefaultConfig(t, false). + WithArbOSVersion(arbosVersion) cleanup := builder.Build(t) defer cleanup() @@ -30,9 +39,22 @@ func TestPurePrecompileMethodCalls(t *testing.T) { Require(t, err, "could not deploy ArbSys contract") chainId, err := arbSys.ArbChainID(&bind.CallOpts{}) Require(t, err, "failed to get the ChainID") - if chainId.Uint64() != params.ArbitrumDevTestChainConfig().ChainID.Uint64() { + if chainId.Uint64() != chaininfo.ArbitrumDevTestChainConfig().ChainID.Uint64() { Fatal(t, "Wrong ChainID", chainId.Uint64()) } + + expectedArbosVersion := 55 + arbosVersion // Nitro versions start at 56 + arbSysArbosVersion, err := arbSys.ArbOSVersion(&bind.CallOpts{}) + Require(t, err) + if arbSysArbosVersion.Uint64() != expectedArbosVersion { + Fatal(t, "Expected ArbOS version", expectedArbosVersion, "got", arbSysArbosVersion) + } + + storageGasAvailable, err := arbSys.GetStorageGasAvailable(&bind.CallOpts{}) + Require(t, err) + if storageGasAvailable.Cmp(big.NewInt(0)) != 0 { + Fatal(t, "Expected 0 storage gas available, got", storageGasAvailable) + } } func TestViewLogReverts(t *testing.T) { @@ -52,7 +74,29 @@ func TestViewLogReverts(t *testing.T) { } } -func TestCustomSolidityErrors(t *testing.T) { +func TestArbDebugPanic(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err) + + _, err = arbDebug.Panic(&auth) + if err == nil { + Fatal(t, "unexpected success") + } + if err.Error() != "method handler crashed" { + Fatal(t, "expected method handler to crash") + } +} + +func TestArbDebugLegacyError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -61,32 +105,97 @@ func TestCustomSolidityErrors(t *testing.T) { defer cleanup() callOpts := &bind.CallOpts{Context: ctx} + arbDebug, err := precompilesgen.NewArbDebug(common.HexToAddress("0xff"), builder.L2.Client) - Require(t, err, "could not bind ArbDebug contract") - customError := arbDebug.CustomRevert(callOpts, 1024) - if customError == nil { - Fatal(t, "customRevert call should have errored") + Require(t, err) + + err = arbDebug.LegacyError(callOpts) + if err == nil { + Fatal(t, "unexpected success") } - observedMessage := customError.Error() - expectedError := "Custom(1024, This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\, true)" - // The first error is server side. The second error is client side ABI decoding. - expectedMessage := fmt.Sprintf("execution reverted: error %v: %v", expectedError, expectedError) - if observedMessage != expectedMessage { - Fatal(t, observedMessage) +} + +func TestCustomSolidityErrors(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + ensure := func( + customError error, + expectedError string, + scenario string, + ) { + if customError == nil { + Fatal(t, "should have errored", "scenario", scenario) + } + observedMessage := customError.Error() + // The first error is server side. The second error is client side ABI decoding. + expectedMessage := fmt.Sprintf("execution reverted: error %v: %v", expectedError, expectedError) + if observedMessage != expectedMessage { + Fatal(t, observedMessage, "scenario", scenario) + } } + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err, "could not bind ArbDebug contract") + ensure( + arbDebug.CustomRevert(callOpts, 1024), + "Custom(1024, This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\, true)", + "arbDebug.CustomRevert", + ) + arbSys, err := precompilesgen.NewArbSys(arbos.ArbSysAddress, builder.L2.Client) Require(t, err, "could not bind ArbSys contract") - _, customError = arbSys.ArbBlockHash(callOpts, big.NewInt(1e9)) - if customError == nil { - Fatal(t, "out of range ArbBlockHash call should have errored") - } - observedMessage = customError.Error() - expectedError = "InvalidBlockNumber(1000000000, 1)" - expectedMessage = fmt.Sprintf("execution reverted: error %v: %v", expectedError, expectedError) - if observedMessage != expectedMessage { - Fatal(t, observedMessage) - } + _, customError := arbSys.ArbBlockHash(callOpts, big.NewInt(1e9)) + ensure( + customError, + "InvalidBlockNumber(1000000000, 1)", + "arbSys.ArbBlockHash", + ) + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(types.ArbRetryableTxAddress, builder.L2.Client) + Require(t, err) + _, customError = arbRetryableTx.SubmitRetryable( + &auth, + [32]byte{}, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + 0, + big.NewInt(0), + common.Address{}, + common.Address{}, + common.Address{}, + []byte{}, + ) + ensure( + customError, + "NotCallable()", + "arbRetryableTx.SubmitRetryable", + ) + + arbosActs, err := precompilesgen.NewArbosActs(types.ArbosAddress, builder.L2.Client) + Require(t, err) + _, customError = arbosActs.StartBlock(&auth, big.NewInt(0), 0, 0, 0) + ensure( + customError, + "CallerNotArbOS()", + "arbosActs.StartBlock", + ) + + _, customError = arbosActs.BatchPostingReport(&auth, big.NewInt(0), common.Address{}, 0, 0, big.NewInt(0)) + ensure( + customError, + "CallerNotArbOS()", + "arbosActs.BatchPostingReport", + ) } func TestPrecompileErrorGasLeft(t *testing.T) { @@ -125,7 +234,245 @@ func TestPrecompileErrorGasLeft(t *testing.T) { assertNotAllGasConsumed(common.HexToAddress("0xff"), arbDebug.Methods["legacyError"].ID) } -func TestScheduleArbosUpgrade(t *testing.T) { +func setupArbOwnerAndArbGasInfo( + t *testing.T, +) ( + *NodeBuilder, + func(), + bind.TransactOpts, + *precompilesgen.ArbOwner, + *precompilesgen.ArbGasInfo, +) { + ctx, cancel := context.WithCancel(context.Background()) + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + builderCleanup := builder.Build(t) + + cleanup := func() { + builderCleanup() + cancel() + } + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + arbOwner, err := precompilesgen.NewArbOwner(common.HexToAddress("0x70"), builder.L2.Client) + Require(t, err) + arbGasInfo, err := precompilesgen.NewArbGasInfo(common.HexToAddress("0x6c"), builder.L2.Client) + Require(t, err) + + return builder, cleanup, auth, arbOwner, arbGasInfo +} + +func TestL1BaseFeeEstimateInertia(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + inertia := uint64(11) + tx, err := arbOwner.SetL1BaseFeeEstimateInertia(&auth, inertia) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoInertia, err := arbGasInfo.GetL1BaseFeeEstimateInertia(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoInertia != inertia { + Fatal(t, "expected inertia to be", inertia, "got", arbGasInfoInertia) + } +} + +// Similar to TestL1BaseFeeEstimateInertia, but now using a different setter from ArbOwner +func TestL1PricingInertia(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + inertia := uint64(12) + tx, err := arbOwner.SetL1PricingInertia(&auth, inertia) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoInertia, err := arbGasInfo.GetL1BaseFeeEstimateInertia(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoInertia != inertia { + Fatal(t, "expected inertia to be", inertia, "got", arbGasInfoInertia) + } +} + +func TestL1PricingRewardRate(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + perUnitReward := uint64(13) + tx, err := arbOwner.SetL1PricingRewardRate(&auth, perUnitReward) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoPerUnitReward, err := arbGasInfo.GetL1RewardRate(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoPerUnitReward != perUnitReward { + Fatal(t, "expected per unit reward to be", perUnitReward, "got", arbGasInfoPerUnitReward) + } +} + +func TestL1PricingRewardRecipient(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + rewardRecipient := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + tx, err := arbOwner.SetL1PricingRewardRecipient(&auth, rewardRecipient) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoRewardRecipient, err := arbGasInfo.GetL1RewardRecipient(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoRewardRecipient.Cmp(rewardRecipient) != 0 { + Fatal(t, "expected reward recipient to be", rewardRecipient, "got", arbGasInfoRewardRecipient) + } +} + +func TestL2GasPricingInertia(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + inertia := uint64(14) + tx, err := arbOwner.SetL2GasPricingInertia(&auth, inertia) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoInertia, err := arbGasInfo.GetPricingInertia(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoInertia != inertia { + Fatal(t, "expected inertia to be", inertia, "got", arbGasInfoInertia) + } +} + +func TestL2GasBacklogTolerance(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + gasTolerance := uint64(15) + tx, err := arbOwner.SetL2GasBacklogTolerance(&auth, gasTolerance) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoGasTolerance, err := arbGasInfo.GetGasBacklogTolerance(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoGasTolerance != gasTolerance { + Fatal(t, "expected gas tolerance to be", gasTolerance, "got", arbGasInfoGasTolerance) + } +} + +func TestPerBatchGasCharge(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + perBatchGasCharge := int64(16) + tx, err := arbOwner.SetPerBatchGasCharge(&auth, perBatchGasCharge) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoPerBatchGasCharge, err := arbGasInfo.GetPerBatchGasCharge(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoPerBatchGasCharge != perBatchGasCharge { + Fatal(t, "expected per batch gas charge to be", perBatchGasCharge, "got", arbGasInfoPerBatchGasCharge) + } +} + +func TestL1PricingEquilibrationUnits(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + equilUnits := big.NewInt(17) + tx, err := arbOwner.SetL1PricingEquilibrationUnits(&auth, equilUnits) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoEquilUnits, err := arbGasInfo.GetL1PricingEquilibrationUnits(&bind.CallOpts{Context: ctx}) + Require(t, err) + if arbGasInfoEquilUnits.Cmp(equilUnits) != 0 { + Fatal(t, "expected equilibration units to be", equilUnits, "got", arbGasInfoEquilUnits) + } +} + +func TestGasAccountingParams(t *testing.T) { + t.Parallel() + + builder, cleanup, auth, arbOwner, arbGasInfo := setupArbOwnerAndArbGasInfo(t) + defer cleanup() + ctx := builder.ctx + + speedLimit := uint64(18) + txGasLimit := uint64(19) + tx, err := arbOwner.SetSpeedLimit(&auth, speedLimit) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + tx, err = arbOwner.SetMaxTxGasLimit(&auth, txGasLimit) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + arbGasInfoSpeedLimit, arbGasInfoPoolSize, arbGasInfoTxGasLimit, err := arbGasInfo.GetGasAccountingParams(&bind.CallOpts{Context: ctx}) + Require(t, err) + // #nosec G115 + if arbGasInfoSpeedLimit.Cmp(big.NewInt(int64(speedLimit))) != 0 { + Fatal(t, "expected speed limit to be", speedLimit, "got", arbGasInfoSpeedLimit) + } + // #nosec G115 + if arbGasInfoPoolSize.Cmp(big.NewInt(int64(txGasLimit))) != 0 { + Fatal(t, "expected pool size to be", txGasLimit, "got", arbGasInfoPoolSize) + } + // #nosec G115 + if arbGasInfoTxGasLimit.Cmp(big.NewInt(int64(txGasLimit))) != 0 { + Fatal(t, "expected tx gas limit to be", txGasLimit, "got", arbGasInfoTxGasLimit) + } +} + +func TestCurrentTxL1GasFees(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + arbGasInfo, err := precompilesgen.NewArbGasInfo(types.ArbGasInfoAddress, builder.L2.Client) + Require(t, err) + + currTxL1GasFees, err := arbGasInfo.GetCurrentTxL1GasFees(&bind.CallOpts{Context: ctx}) + Require(t, err) + if currTxL1GasFees == nil { + Fatal(t, "currTxL1GasFees is nil") + } + if currTxL1GasFees.Cmp(big.NewInt(0)) != 1 { + Fatal(t, "expected currTxL1GasFees to be greater than 0, got", currTxL1GasFees) + } +} + +func TestGetBrotliCompressionLevel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -135,43 +482,313 @@ func TestScheduleArbosUpgrade(t *testing.T) { auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) - arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(common.HexToAddress("0x6b"), builder.L2.Client) - Require(t, err, "could not bind ArbOwner contract") + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) - arbOwner, err := precompilesgen.NewArbOwner(common.HexToAddress("0x70"), builder.L2.Client) - Require(t, err, "could not bind ArbOwner contract") + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + brotliCompressionLevel := uint64(11) + + // sets brotli compression level + tx, err := arbOwner.SetBrotliCompressionLevel(&auth, brotliCompressionLevel) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // retrieves brotli compression level + callOpts := &bind.CallOpts{Context: ctx} + retrievedBrotliCompressionLevel, err := arbOwnerPublic.GetBrotliCompressionLevel(callOpts) + Require(t, err) + if retrievedBrotliCompressionLevel != brotliCompressionLevel { + Fatal(t, "expected brotli compression level to be", brotliCompressionLevel, "got", retrievedBrotliCompressionLevel) + } +} + +func TestArbStatistics(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + arbStatistics, err := precompilesgen.NewArbStatistics(types.ArbStatisticsAddress, builder.L2.Client) + Require(t, err) + + callOpts := &bind.CallOpts{Context: ctx} + blockNum, _, _, _, _, _, err := arbStatistics.GetStats(callOpts) + Require(t, err) + + expectedBlockNum, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + + if blockNum.Uint64() != expectedBlockNum { + Fatal(t, "expected block number to be", expectedBlockNum, "got", blockNum) + } +} + +func TestArbFunctionTable(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbFunctionTable, err := precompilesgen.NewArbFunctionTable(types.ArbFunctionTableAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + // should be a noop + tx, err := arbFunctionTable.Upload(&auth, []byte{0, 0, 0, 0}) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + size, err := arbFunctionTable.Size(callOpts, addr) + Require(t, err) + if size.Cmp(big.NewInt(0)) != 0 { + t.Fatal("Size should be 0") + } + + _, _, _, err = arbFunctionTable.Get(callOpts, addr, big.NewInt(10)) + if err == nil { + t.Fatal("Should error") + } +} + +func TestArbAggregatorBaseFee(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + tx, err := arbAggregator.SetTxBaseFee(&auth, common.Address{}, big.NewInt(1)) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + fee, err := arbAggregator.GetTxBaseFee(callOpts, common.Address{}) + Require(t, err) + if fee.Cmp(big.NewInt(0)) != 0 { + Fatal(t, "expected fee to be 0, got", fee) + } +} + +func TestFeeAccounts(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + builder.L2Info.GenerateAccount("User2") + addr := builder.L2Info.GetAddress("User2") + + tx, err := arbOwner.SetNetworkFeeAccount(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + feeAccount, err := arbOwner.GetNetworkFeeAccount(callOpts) + Require(t, err) + if feeAccount.Cmp(addr) != 0 { + Fatal(t, "expected fee account to be", addr, "got", feeAccount) + } + + tx, err = arbOwner.SetInfraFeeAccount(&auth, addr) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + feeAccount, err = arbOwner.GetInfraFeeAccount(callOpts) + Require(t, err) + if feeAccount.Cmp(addr) != 0 { + Fatal(t, "expected fee account to be", addr, "got", feeAccount) + } +} + +func TestChainOwners(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) callOpts := &bind.CallOpts{Context: ctx} - scheduled, err := arbOwnerPublic.GetScheduledUpgrade(callOpts) - Require(t, err, "failed to call GetScheduledUpgrade before scheduling upgrade") - if scheduled.ArbosVersion != 0 || scheduled.ScheduledForTimestamp != 0 { - t.Errorf("expected no upgrade to be scheduled, got version %v timestamp %v", scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) + + arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(types.ArbOwnerPublicAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + + builder.L2Info.GenerateAccount("Owner2") + chainOwnerAddr2 := builder.L2Info.GetAddress("Owner2") + tx, err := arbOwner.AddChainOwner(&auth, chainOwnerAddr2) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + isChainOwner, err := arbOwnerPublic.IsChainOwner(callOpts, chainOwnerAddr2) + Require(t, err) + if !isChainOwner { + Fatal(t, "expected owner2 to be a chain owner") + } + + // check that the chain owners retrieved from arbOwnerPublic and arbOwner are the same + chainOwnersArbOwnerPublic, err := arbOwnerPublic.GetAllChainOwners(callOpts) + Require(t, err) + chainOwnersArbOwner, err := arbOwner.GetAllChainOwners(callOpts) + Require(t, err) + if len(chainOwnersArbOwnerPublic) != len(chainOwnersArbOwner) { + Fatal(t, "expected chain owners to be the same length") + } + // sort the chain owners to ensure they are in the same order + sort.Slice(chainOwnersArbOwnerPublic, func(i, j int) bool { + return chainOwnersArbOwnerPublic[i].Cmp(chainOwnersArbOwnerPublic[j]) < 0 + }) + for i := 0; i < len(chainOwnersArbOwnerPublic); i += 1 { + if chainOwnersArbOwnerPublic[i].Cmp(chainOwnersArbOwner[i]) != 0 { + Fatal(t, "expected chain owners to be the same") + } + } + chainOwnerAddr := builder.L2Info.GetAddress("Owner") + chainOwnerInChainOwners := false + for _, chainOwner := range chainOwnersArbOwner { + if chainOwner.Cmp(chainOwnerAddr) == 0 { + chainOwnerInChainOwners = true + } + } + if !chainOwnerInChainOwners { + Fatal(t, "expected owner to be in chain owners") } - // Schedule a noop upgrade, which should test GetScheduledUpgrade in the same way an already completed upgrade would. - tx, err := arbOwner.ScheduleArbOSUpgrade(&auth, 1, 1) + // remove chain owner 2 + tx, err = arbOwner.RemoveChainOwner(&auth, chainOwnerAddr2) Require(t, err) _, err = builder.L2.EnsureTxSucceeded(tx) Require(t, err) + isChainOwner, err = arbOwnerPublic.IsChainOwner(callOpts, chainOwnerAddr2) + Require(t, err) + if isChainOwner { + Fatal(t, "expected owner2 to not be a chain owner") + } + + _, err = arbOwnerPublic.RectifyChainOwner(&auth, chainOwnerAddr) + if (err == nil) || (err.Error() != "execution reverted") { + Fatal(t, "expected rectify chain owner to revert since it is already an owner") + } +} + +func TestArbAggregatorBatchPosters(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + arbDebug, err := precompilesgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) - scheduled, err = arbOwnerPublic.GetScheduledUpgrade(callOpts) - Require(t, err, "failed to call GetScheduledUpgrade after scheduling noop upgrade") - if scheduled.ArbosVersion != 0 || scheduled.ScheduledForTimestamp != 0 { - t.Errorf("expected completed scheduled upgrade to be ignored, got version %v timestamp %v", scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) + // initially should have one batch poster + bps, err := arbAggregator.GetBatchPosters(callOpts) + Require(t, err) + if len(bps) != 1 { + Fatal(t, "expected one batch poster") } - // TODO: Once we have an ArbOS 30, test a real upgrade with it - // We can't test 11 -> 20 because 11 doesn't have the GetScheduledUpgrade method we want to test - var testVersion uint64 = 100 - var testTimestamp uint64 = 1 << 62 - tx, err = arbOwner.ScheduleArbOSUpgrade(&auth, 100, 1<<62) + // add addr as a batch poster + tx, err := arbDebug.BecomeChainOwner(&auth) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + tx, err = arbAggregator.AddBatchPoster(&auth, addr) Require(t, err) _, err = builder.L2.EnsureTxSucceeded(tx) Require(t, err) - scheduled, err = arbOwnerPublic.GetScheduledUpgrade(callOpts) - Require(t, err, "failed to call GetScheduledUpgrade after scheduling upgrade") - if scheduled.ArbosVersion != testVersion || scheduled.ScheduledForTimestamp != testTimestamp { - t.Errorf("expected upgrade to be scheduled for version %v timestamp %v, got version %v timestamp %v", testVersion, testTimestamp, scheduled.ArbosVersion, scheduled.ScheduledForTimestamp) + // there should now be two batch posters, and addr should be one of them + bps, err = arbAggregator.GetBatchPosters(callOpts) + Require(t, err) + if len(bps) != 2 { + Fatal(t, "expected two batch posters") + } + if bps[0] != addr && bps[1] != addr { + Fatal(t, "expected addr to be a batch poster") + } +} + +func TestArbAggregatorGetPreferredAggregator(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbAggregator, err := precompilesgen.NewArbAggregator(types.ArbAggregatorAddress, builder.L2.Client) + Require(t, err) + + addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + + prefAgg, isDefault, err := arbAggregator.GetPreferredAggregator(callOpts, addr) + Require(t, err) + if !isDefault { + Fatal(t, "expected default preferred aggregator") + } + if prefAgg != l1pricing.BatchPosterAddress { + Fatal(t, "expected default preferred aggregator to be", l1pricing.BatchPosterAddress, "got", prefAgg) + } + + prefAgg, err = arbAggregator.GetDefaultAggregator(callOpts) + Require(t, err) + if prefAgg != l1pricing.BatchPosterAddress { + Fatal(t, "expected default preferred aggregator to be", l1pricing.BatchPosterAddress, "got", prefAgg) } } diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go new file mode 100644 index 0000000000..3450214a14 --- /dev/null +++ b/system_tests/program_gas_test.go @@ -0,0 +1,624 @@ +package arbtest + +import ( + "context" + "encoding/binary" + "fmt" + "math" + "math/big" + "regexp" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +const HOSTIO_INK = 8400 + +func checkInkUsage( + t *testing.T, + builder *NodeBuilder, + stylusProgram common.Address, + hostio string, + signature string, + params []uint32, + expectedInk uint64, +) { + toU256ByteSlice := func(i uint32) []byte { + arr := make([]byte, 32) + binary.BigEndian.PutUint32(arr[28:32], i) + return arr + } + + testName := fmt.Sprintf("%v_%v", signature, params) + + data := crypto.Keccak256([]byte(signature))[:4] + for _, p := range params { + data = append(data, toU256ByteSlice(p)...) + } + + const txGas uint64 = 32_000_000 + tx := builder.L2Info.PrepareTxTo("Owner", &stylusProgram, txGas, nil, data) + + err := builder.L2.Client.SendTransaction(builder.ctx, tx) + Require(t, err, "testName", testName) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err, "testName", testName) + + stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), tx) + Require(t, err, "testName", testName) + + _, ok := stylusGasUsage[hostio] + if !ok { + Fatal(t, "hostio not found in gas usage", "hostio", hostio, "stylusGasUsage", stylusGasUsage, "testName", testName) + } + + if len(stylusGasUsage[hostio]) != 1 { + Fatal(t, "unexpected number of gas usage", "hostio", hostio, "stylusGasUsage", stylusGasUsage, "testName", testName) + } + + expectedGas := float64(expectedInk) / 10000 + returnedGas := stylusGasUsage[hostio][0] + if math.Abs(expectedGas-returnedGas) > 1e-9 { + Fatal(t, "unexpected gas usage", "hostio", hostio, "expected", expectedGas, "returned", returnedGas, "testName", testName) + } +} + +func TestWriteResultGasUsage(t *testing.T) { + t.Parallel() + + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + + hostio := "write_result" + + // writeResultEmpty doesn't return any value + signature := "writeResultEmpty()" + expectedInk := HOSTIO_INK + 16381*2 + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk)) + + // writeResult(uint256) returns an array of uint256 + signature = "writeResult(uint256)" + numberOfElementsInReturnedArray := 10000 + arrayOverhead := 32 + 32 // 32 bytes for the array length and 32 bytes for the array offset + expectedInk = HOSTIO_INK + (16381+55*(32*numberOfElementsInReturnedArray+arrayOverhead-32))*2 + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{uint32(numberOfElementsInReturnedArray)}, uint64(expectedInk)) + + signature = "writeResult(uint256)" + numberOfElementsInReturnedArray = 0 + expectedInk = HOSTIO_INK + (16381+55*(arrayOverhead-32))*2 + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{uint32(numberOfElementsInReturnedArray)}, uint64(expectedInk)) +} + +func TestReadArgsGasUsage(t *testing.T) { + t.Parallel() + + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + + hostio := "read_args" + + signature := "readArgsNoArgs()" + expectedInk := HOSTIO_INK + 5040 + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk)) + + signature = "readArgsOneArg(uint256)" + signatureOverhead := 4 + expectedInk = HOSTIO_INK + 5040 + 30*(32+signatureOverhead-32) + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{1}, uint64(expectedInk)) + + signature = "readArgsThreeArgs(uint256,uint256,uint256)" + expectedInk = HOSTIO_INK + 5040 + 30*(3*32+signatureOverhead-32) + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{1, 1, 1}, uint64(expectedInk)) +} + +func TestMsgReentrantGasUsage(t *testing.T) { + t.Parallel() + + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + + hostio := "msg_reentrant" + + signature := "writeResultEmpty()" + expectedInk := HOSTIO_INK + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk)) +} + +func TestStorageCacheBytes32GasUsage(t *testing.T) { + t.Parallel() + + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + + hostio := "storage_cache_bytes32" + + signature := "storageCacheBytes32()" + expectedInk := HOSTIO_INK + (13440-HOSTIO_INK)*2 + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk)) +} + +func TestPayForMemoryGrowGasUsage(t *testing.T) { + t.Parallel() + + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + + hostio := "pay_for_memory_grow" + signature := "payForMemoryGrow(uint256)" + + expectedInk := 9320660000 + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{100}, uint64(expectedInk)) + + expectedInk = HOSTIO_INK + // #nosec G115 + checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{0}, uint64(expectedInk)) +} + +func TestProgramSimpleCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + otherProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("storage")) + matchSnake := regexp.MustCompile("_[a-z]") + + for _, tc := range []struct { + hostio string + opcode vm.OpCode + params []any + maxDiff float64 + }{ + {hostio: "exit_early", opcode: vm.STOP}, + {hostio: "transient_load_bytes32", opcode: vm.TLOAD, params: []any{common.HexToHash("dead")}}, + {hostio: "transient_store_bytes32", opcode: vm.TSTORE, params: []any{common.HexToHash("dead"), common.HexToHash("beef")}}, + {hostio: "return_data_size", opcode: vm.RETURNDATASIZE, maxDiff: 1.5}, + {hostio: "account_balance", opcode: vm.BALANCE, params: []any{builder.L2Info.GetAddress("Owner")}}, + {hostio: "account_code", opcode: vm.EXTCODECOPY, params: []any{otherProgram}}, + {hostio: "account_code_size", opcode: vm.EXTCODESIZE, params: []any{otherProgram}, maxDiff: 0.3}, + {hostio: "account_codehash", opcode: vm.EXTCODEHASH, params: []any{otherProgram}}, + {hostio: "evm_gas_left", opcode: vm.GAS, maxDiff: 1.5}, + {hostio: "evm_ink_left", opcode: vm.GAS, maxDiff: 1.5}, + {hostio: "block_basefee", opcode: vm.BASEFEE, maxDiff: 0.5}, + {hostio: "chainid", opcode: vm.CHAINID, maxDiff: 1.5}, + {hostio: "block_coinbase", opcode: vm.COINBASE, maxDiff: 0.5}, + {hostio: "block_gas_limit", opcode: vm.GASLIMIT, maxDiff: 1.5}, + {hostio: "block_number", opcode: vm.NUMBER, maxDiff: 1.5}, + {hostio: "block_timestamp", opcode: vm.TIMESTAMP, maxDiff: 1.5}, + {hostio: "contract_address", opcode: vm.ADDRESS, maxDiff: 0.5}, + {hostio: "math_div", opcode: vm.DIV, params: []any{big.NewInt(1), big.NewInt(3)}}, + {hostio: "math_mod", opcode: vm.MOD, params: []any{big.NewInt(1), big.NewInt(3)}}, + {hostio: "math_add_mod", opcode: vm.ADDMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.7}, + {hostio: "math_mul_mod", opcode: vm.MULMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.7}, + {hostio: "msg_sender", opcode: vm.CALLER, maxDiff: 0.5}, + {hostio: "msg_value", opcode: vm.CALLVALUE, maxDiff: 0.5}, + {hostio: "tx_gas_price", opcode: vm.GASPRICE, maxDiff: 0.5}, + {hostio: "tx_ink_price", opcode: vm.GASPRICE, maxDiff: 1.5}, + {hostio: "tx_origin", opcode: vm.ORIGIN, maxDiff: 0.5}, + } { + t.Run(tc.hostio, func(t *testing.T) { + solFunc := matchSnake.ReplaceAllStringFunc(tc.hostio, func(s string) string { + return strings.ToUpper(strings.TrimPrefix(s, "_")) + }) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, solFunc) + data, err := packer(tc.params...) + Require(t, err) + compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, tc.maxDiff, compareGasPair{tc.opcode, tc.hostio}) + }) + } +} + +func TestProgramPowCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "mathPow") + + for _, exponentNumBytes := range []uint{1, 2, 10, 32} { + name := fmt.Sprintf("exponentNumBytes%v", exponentNumBytes) + t.Run(name, func(t *testing.T) { + exponent := new(big.Int).Lsh(big.NewInt(1), exponentNumBytes*8-1) + params := []any{big.NewInt(1), exponent} + data, err := packer(params...) + Require(t, err) + evmGasUsage, stylusGasUsage := measureGasUsage(t, builder, evmProgram, stylusProgram, data, nil) + expectedGas := 2.652 + 1.75*float64(exponentNumBytes+1) + t.Logf("evm EXP usage: %v - stylus math_pow usage: %v - expected math_pow usage: %v", + evmGasUsage[vm.EXP][0], stylusGasUsage["math_pow"][0], expectedGas) + // The math_pow HostIO uses significally less gas than the EXP opcode. So, + // instead of comparing it to EVM, we compare it to the expected gas usage + // for each test case. + checkPercentDiff(t, stylusGasUsage["math_pow"][0], expectedGas, 0.001) + }) + } +} + +func TestProgramStorageCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusMulticall := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("multicall")) + evmMulticall := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.MultiCallTestMetaData) + + const numSlots = 42 + rander := testhelpers.NewPseudoRandomDataSource(t, 0) + readData := multicallEmptyArgs() + writeRandAData := multicallEmptyArgs() + writeRandBData := multicallEmptyArgs() + writeZeroData := multicallEmptyArgs() + for i := 0; i < numSlots; i++ { + slot := rander.GetHash() + readData = multicallAppendLoad(readData, slot, false) + writeRandAData = multicallAppendStore(writeRandAData, slot, rander.GetHash(), false) + writeRandBData = multicallAppendStore(writeRandBData, slot, rander.GetHash(), false) + writeZeroData = multicallAppendStore(writeZeroData, slot, common.Hash{}, false) + } + + writePair := compareGasPair{vm.SSTORE, "storage_flush_cache"} + readPair := compareGasPair{vm.SLOAD, "storage_load_bytes32"} + + for _, tc := range []struct { + name string + data []byte + pair compareGasPair + }{ + {"initialWrite", writeRandAData, writePair}, + {"read", readData, readPair}, + {"writeAgain", writeRandBData, writePair}, + {"delete", writeZeroData, writePair}, + {"readZeros", readData, readPair}, + {"writeAgainAgain", writeRandAData, writePair}, + } { + t.Run(tc.name, func(t *testing.T) { + compareGasUsage(t, builder, evmMulticall, stylusMulticall, tc.data, nil, compareGasSum, 0, tc.pair) + }) + } +} + +func TestProgramLogCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "emitLog") + + for ntopics := int8(0); ntopics < 5; ntopics++ { + for _, dataSize := range []uint64{10, 100, 1000} { + name := fmt.Sprintf("emitLog%dData%d", ntopics, dataSize) + t.Run(name, func(t *testing.T) { + args := []any{ + testhelpers.RandomSlice(dataSize), + ntopics, + } + for t := 0; t < 4; t++ { + args = append(args, testhelpers.RandomHash()) + } + data, err := packer(args...) + Require(t, err) + opcode := vm.LOG0 + vm.OpCode(ntopics) + compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, 0, compareGasPair{opcode, "emit_log"}) + }) + } + } + +} + +func TestProgramCallCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusMulticall := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("multicall")) + evmMulticall := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.MultiCallTestMetaData) + otherStylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + otherEvmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "msgValue") + otherData, err := packer() + Require(t, err) + + for _, pair := range []compareGasPair{ + {vm.CALL, "call_contract"}, + {vm.DELEGATECALL, "delegate_call_contract"}, + {vm.STATICCALL, "static_call_contract"}, + } { + t.Run(pair.hostio+"/burnGas", func(t *testing.T) { + arbTest := common.HexToAddress("0x0000000000000000000000000000000000000069") + burnArbGas, _ := util.NewCallParser(precompilesgen.ArbosTestABI, "burnArbGas") + burnData, err := burnArbGas(big.NewInt(0)) + Require(t, err) + data := argsForMulticall(pair.opcode, arbTest, nil, burnData) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) + }) + + t.Run(pair.hostio+"/evmContract", func(t *testing.T) { + data := argsForMulticall(pair.opcode, otherEvmProgram, nil, otherData) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, + compareGasPair{vm.RETURNDATACOPY, "read_return_data"}) // also test read_return_data + }) + + t.Run(pair.hostio+"/stylusContract", func(t *testing.T) { + data := argsForMulticall(pair.opcode, otherStylusProgram, nil, otherData) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, + compareGasPair{vm.RETURNDATACOPY, "read_return_data"}) // also test read_return_data + }) + + t.Run(pair.hostio+"/multipleTimes", func(t *testing.T) { + data := multicallEmptyArgs() + for i := 0; i < 9; i++ { + data = multicallAppend(data, pair.opcode, otherEvmProgram, otherData) + } + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) + }) + } + + t.Run("call_contract/evmContractWithValue", func(t *testing.T) { + value := big.NewInt(1000) + data := argsForMulticall(vm.CALL, otherEvmProgram, value, otherData) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, value, compareGasForEach, 0, compareGasPair{vm.CALL, "call_contract"}) + }) +} + +func TestProgramCreateCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusCreate := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("create")) + evmCreate := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.CreateTestMetaData) + deployCode := common.FromHex(mocksgen.ProgramTestMetaData.Bin) + + t.Run("create1", func(t *testing.T) { + data := []byte{0x01} + data = append(data, (common.Hash{}).Bytes()...) // endowment + data = append(data, deployCode...) + compareGasUsage(t, builder, evmCreate, stylusCreate, data, nil, compareGasForEach, 0, compareGasPair{vm.CREATE, "create1"}) + }) + + t.Run("create2", func(t *testing.T) { + data := []byte{0x02} + data = append(data, (common.Hash{}).Bytes()...) // endowment + data = append(data, (common.HexToHash("beef")).Bytes()...) // salt + data = append(data, deployCode...) + compareGasUsage(t, builder, evmCreate, stylusCreate, data, nil, compareGasForEach, 0, compareGasPair{vm.CREATE2, "create2"}) + }) +} + +func TestProgramKeccakCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "keccak") + + for i := 1; i < 5; i++ { + size := uint64(math.Pow10(i)) + name := fmt.Sprintf("keccak%d", size) + t.Run(name, func(t *testing.T) { + preImage := testhelpers.RandomSlice(size) + preImage[len(preImage)-1] = 0 + data, err := packer(preImage) + Require(t, err) + const maxDiff = 2.5 // stylus keccak charges significantly less gas + compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, maxDiff, compareGasPair{vm.KECCAK256, "native_keccak256"}) + }) + } +} + +func setupGasCostTest(t *testing.T) *NodeBuilder { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanup := builder.Build(t) + t.Cleanup(cleanup) + return builder +} + +// deployEvmContract deploys an Evm contract and return its address. +func deployEvmContract(t *testing.T, ctx context.Context, auth bind.TransactOpts, client *ethclient.Client, metadata *bind.MetaData) common.Address { + t.Helper() + parsed, err := metadata.GetAbi() + Require(t, err) + address, tx, _, err := bind.DeployContract(&auth, *parsed, common.FromHex(metadata.Bin), client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + return address +} + +// measureGasUsage calls an EVM and a Wasm contract passing the same data and the same value. +func measureGasUsage( + t *testing.T, + builder *NodeBuilder, + evmContract common.Address, + stylusContract common.Address, + txData []byte, + txValue *big.Int, +) (map[vm.OpCode][]uint64, map[string][]float64) { + const txGas uint64 = 32_000_000 + txs := []*types.Transaction{ + builder.L2Info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData), + builder.L2Info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData), + } + receipts := builder.L2.SendWaitTestTransactions(t, txs) + + evmGas := receipts[0].GasUsedForL2() + evmGasUsage, err := evmOpcodesGasUsage(builder.ctx, builder.L2.Client.Client(), txs[0]) + Require(t, err) + + stylusGas := receipts[1].GasUsedForL2() + stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), txs[1]) + Require(t, err) + + t.Logf("evm total usage: %v - stylus total usage: %v", evmGas, stylusGas) + + return evmGasUsage, stylusGasUsage +} + +type compareGasPair struct { + opcode vm.OpCode + hostio string +} + +type compareGasMode int + +const ( + compareGasForEach compareGasMode = iota + compareGasSum +) + +// compareGasUsage calls measureGasUsage and then it ensures the given opcodes and hostios cost +// roughly the same amount of gas. +func compareGasUsage( + t *testing.T, + builder *NodeBuilder, + evmContract common.Address, + stylusContract common.Address, + txData []byte, + txValue *big.Int, + mode compareGasMode, + maxAllowedDifference float64, + pairs ...compareGasPair, +) { + if evmContract == stylusContract { + Fatal(t, "evm and stylus contract are the same") + } + evmGasUsage, stylusGasUsage := measureGasUsage(t, builder, evmContract, stylusContract, txData, txValue) + for i := range pairs { + opcode := pairs[i].opcode + hostio := pairs[i].hostio + switch mode { + case compareGasForEach: + if len(evmGasUsage[opcode]) != len(stylusGasUsage[hostio]) { + Fatal(t, "mismatch between opcode ", opcode, " - ", evmGasUsage[opcode], " and hostio ", hostio, " - ", stylusGasUsage[hostio]) + } + for i := range evmGasUsage[opcode] { + opcodeGas := evmGasUsage[opcode][i] + hostioGas := stylusGasUsage[hostio][i] + t.Logf("evm %v usage: %v - stylus %v usage: %v", opcode, opcodeGas, hostio, hostioGas) + checkPercentDiff(t, float64(opcodeGas), hostioGas, maxAllowedDifference) + } + case compareGasSum: + evmSum := float64(0) + for _, v := range evmGasUsage[opcode] { + evmSum += float64(v) + } + stylusSum := float64(0) + for _, v := range stylusGasUsage[hostio] { + stylusSum += v + } + t.Logf("evm %v usage: %v - stylus %v usage: %v", opcode, evmSum, hostio, stylusSum) + checkPercentDiff(t, evmSum, stylusSum, maxAllowedDifference) + } + } +} + +func evmOpcodesGasUsage(ctx context.Context, rpcClient rpc.ClientInterface, tx *types.Transaction) ( + map[vm.OpCode][]uint64, error) { + + var result logger.ExecutionResult + err := rpcClient.CallContext(ctx, &result, "debug_traceTransaction", tx.Hash(), nil) + if err != nil { + return nil, fmt.Errorf("failed to trace evm call: %w", err) + } + + gasUsage := map[vm.OpCode][]uint64{} + for i := range result.StructLogs { + op := vm.StringToOp(result.StructLogs[i].Op) + gasUsed := uint64(0) + if op == vm.CALL || op == vm.STATICCALL || op == vm.DELEGATECALL || op == vm.CREATE || op == vm.CREATE2 { + // For the CALL* opcodes, the GasCost in the tracer represents the gas sent + // to the callee contract, which is 63/64 of the remaining gas. This happens + // because the tracer is evaluated before the call is executed, so the EVM + // doesn't know how much gas will being used. + // + // In the case of the Stylus tracer, the trace is emitted after the + // execution, so the EndInk field is set to the ink after the call returned. + // Hence, it also includes the ink spent by the callee contract. + // + // To make a precise comparison between the EVM and Stylus, we modify the + // EVM measurement to include the gas spent by the callee contract. To do + // so, we go through the opcodes after CALL until we find the first opcode + // in the caller's depth. Then, we subtract the gas before the call by the + // gas after the call returned. + var gasAfterCall uint64 + var found bool + for j := i + 1; j < len(result.StructLogs); j++ { + if result.StructLogs[j].Depth == result.StructLogs[i].Depth { + // back to the original call + gasAfterCall = result.StructLogs[j].Gas + result.StructLogs[j].GasCost + found = true + break + } + } + if !found { + return nil, fmt.Errorf("malformed log: didn't get back to call original depth") + } + if i == 0 { + return nil, fmt.Errorf("malformed log: call is first opcode") + } + gasUsed = result.StructLogs[i-1].Gas - gasAfterCall + } else { + gasUsed = result.StructLogs[i].GasCost + } + gasUsage[op] = append(gasUsage[op], gasUsed) + } + return gasUsage, nil +} + +func stylusHostiosGasUsage(ctx context.Context, rpcClient rpc.ClientInterface, tx *types.Transaction) ( + map[string][]float64, error) { + + traceOpts := struct { + Tracer string `json:"tracer"` + }{ + Tracer: "stylusTracer", + } + var result []gethexec.HostioTraceInfo + err := rpcClient.CallContext(ctx, &result, "debug_traceTransaction", tx.Hash(), traceOpts) + if err != nil { + return nil, fmt.Errorf("failed to trace stylus call: %w", err) + } + + const InkPerGas = 10000 + gasUsage := map[string][]float64{} + for _, hostioLog := range result { + gasCost := float64(hostioLog.StartInk-hostioLog.EndInk) / InkPerGas + gasUsage[hostioLog.Name] = append(gasUsage[hostioLog.Name], gasCost) + } + return gasUsage, nil +} + +// checkPercentDiff checks whether the two values are close enough. +func checkPercentDiff(t *testing.T, a, b float64, maxAllowedDifference float64) { + t.Helper() + if maxAllowedDifference == 0 { + maxAllowedDifference = 0.25 + } + percentageDifference := (max(a, b) / min(a, b)) - 1 + if percentageDifference > maxAllowedDifference { + Fatal(t, fmt.Sprintf("gas usages are too different; got %v, max allowed is %v", percentageDifference, maxAllowedDifference)) + } +} diff --git a/system_tests/program_norace_test.go b/system_tests/program_norace_test.go index 56b2046716..b1e5af8395 100644 --- a/system_tests/program_norace_test.go +++ b/system_tests/program_norace_test.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/mocksgen" diff --git a/system_tests/program_recursive_test.go b/system_tests/program_recursive_test.go index dbf527a293..ca726c684d 100644 --- a/system_tests/program_recursive_test.go +++ b/system_tests/program_recursive_test.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/mocksgen" @@ -154,6 +155,7 @@ func testProgramResursiveCalls(t *testing.T, tests [][]multiCallRecurse, jit boo // execute transactions blockNum := uint64(0) for { + // #nosec G115 item := int(rander.GetUint64()/4) % len(tests) blockNum = testProgramRecursiveCall(t, builder, slotVals, rander, tests[item]) tests[item] = tests[len(tests)-1] diff --git a/system_tests/program_test.go b/system_tests/program_test.go index e171f2a444..5fbb1189c7 100644 --- a/system_tests/program_test.go +++ b/system_tests/program_test.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbos/util" @@ -45,18 +46,31 @@ import ( var oneEth = arbmath.UintToBig(1e18) +var allWasmTargets = []string{string(rawdb.TargetWavm), string(rawdb.TargetArm64), string(rawdb.TargetAmd64), string(rawdb.TargetHost)} + func TestProgramKeccak(t *testing.T) { t.Parallel() - keccakTest(t, true) + t.Run("WithDefaultWasmTargets", func(t *testing.T) { + keccakTest(t, true) + }) + + t.Run("WithAllWasmTargets", func(t *testing.T) { + keccakTest(t, true, func(builder *NodeBuilder) { + builder.WithExtraArchs(allWasmTargets) + }) + }) } -func keccakTest(t *testing.T, jit bool) { - builder, auth, cleanup := setupProgramTest(t, jit) +func keccakTest(t *testing.T, jit bool, builderOpts ...func(*NodeBuilder)) { + builder, auth, cleanup := setupProgramTest(t, jit, builderOpts...) ctx := builder.ctx l2client := builder.L2.Client defer cleanup() programAddress := deployWasm(t, ctx, auth, l2client, rustFile("keccak")) + wasmDb := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 1) + wasm, _ := readWasmFile(t, rustFile("keccak")) otherAddressSameCode := deployContract(t, ctx, auth, l2client, wasm) arbWasm, err := pgen.NewArbWasm(types.ArbWasmAddress, l2client) @@ -68,6 +82,7 @@ func keccakTest(t *testing.T, jit bool) { Fatal(t, "activate should have failed with ProgramUpToDate", err) } }) + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 1) if programAddress == otherAddressSameCode { Fatal(t, "expected to deploy at two separate program addresses") @@ -141,11 +156,18 @@ func keccakTest(t *testing.T, jit bool) { func TestProgramActivateTwice(t *testing.T) { t.Parallel() - testActivateTwice(t, true) + t.Run("WithDefaultWasmTargets", func(t *testing.T) { + testActivateTwice(t, true) + }) + t.Run("WithAllWasmTargets", func(t *testing.T) { + testActivateTwice(t, true, func(builder *NodeBuilder) { + builder.WithExtraArchs(allWasmTargets) + }) + }) } -func testActivateTwice(t *testing.T, jit bool) { - builder, auth, cleanup := setupProgramTest(t, jit) +func testActivateTwice(t *testing.T, jit bool, builderOpts ...func(*NodeBuilder)) { + builder, auth, cleanup := setupProgramTest(t, jit, builderOpts...) ctx := builder.ctx l2info := builder.L2Info l2client := builder.L2.Client @@ -171,6 +193,10 @@ func testActivateTwice(t *testing.T, jit bool) { colors.PrintBlue("keccak program B deployed to ", keccakB) multiAddr := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) + + wasmDb := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 1) + preimage := []byte("it's time to du-du-du-du d-d-d-d-d-d-d de-duplicate") keccakArgs := []byte{0x01} // keccak the preimage once @@ -194,6 +220,7 @@ func testActivateTwice(t *testing.T, jit bool) { // Calling the contract pre-activation should fail. checkReverts() + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 1) // mechanisms for creating calldata activateProgram, _ := util.NewCallParser(pgen.ArbWasmABI, "activateProgram") @@ -216,6 +243,7 @@ func testActivateTwice(t *testing.T, jit bool) { // Ensure the revert also reverted keccak's activation checkReverts() + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 1) // Activate keccak program A, then call into B, which should succeed due to being the same codehash args = argsForMulticall(vm.CALL, types.ArbWasmAddress, oneEth, pack(activateProgram(keccakA))) @@ -223,6 +251,7 @@ func testActivateTwice(t *testing.T, jit bool) { tx = l2info.PrepareTxTo("Owner", &multiAddr, 1e9, oneEth, args) ensure(tx, l2client.SendTransaction(ctx, tx)) + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 2) validateBlocks(t, 7, jit, builder) } @@ -389,10 +418,15 @@ func storageTest(t *testing.T, jit bool) { key := testhelpers.RandomHash() value := testhelpers.RandomHash() tx := l2info.PrepareTxTo("Owner", &programAddress, l2info.TransferGas, nil, argsForStorageWrite(key, value)) - ensure(tx, l2client.SendTransaction(ctx, tx)) + receipt := ensure(tx, l2client.SendTransaction(ctx, tx)) + assertStorageAt(t, ctx, l2client, programAddress, key, value) validateBlocks(t, 2, jit, builder) + + // Captures a block_inputs json file for the block that included the + // storage write transaction. Include wasm targets necessary for arbitrator prover and jit binaries + recordBlock(t, receipt.BlockNumber.Uint64(), builder, rawdb.TargetWavm, rawdb.LocalTarget()) } func TestProgramTransientStorage(t *testing.T) { @@ -502,6 +536,16 @@ func testCalls(t *testing.T, jit bool) { defer cleanup() callsAddr := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) + // checks that ArbInfo.GetCode works properly + codeFromFile, _ := readWasmFile(t, rustFile("multicall")) + arbInfo, err := pgen.NewArbInfo(types.ArbInfoAddress, l2client) + Require(t, err) + codeFromArbInfo, err := arbInfo.GetCode(nil, callsAddr) + Require(t, err) + if !bytes.Equal(codeFromFile, codeFromArbInfo) { + t.Fatal("ArbInfo.GetCode returned wrong code") + } + ensure := func(tx *types.Transaction, err error) *types.Receipt { t.Helper() Require(t, err) @@ -582,6 +626,7 @@ func testCalls(t *testing.T, jit bool) { for i := 0; i < 2; i++ { inner := nest(level - 1) + // #nosec G115 args = append(args, arbmath.Uint32ToBytes(uint32(len(inner)))...) args = append(args, inner...) } @@ -637,6 +682,7 @@ func testCalls(t *testing.T, jit bool) { colors.PrintBlue("Calling the ArbosTest precompile (Rust => precompile)") testPrecompile := func(gas uint64) uint64 { // Call the burnArbGas() precompile from Rust + // #nosec G115 burn := pack(burnArbGas(big.NewInt(int64(gas)))) args := argsForMulticall(vm.CALL, types.ArbosTestAddress, nil, burn) tx := l2info.PrepareTxTo("Owner", &callsAddr, 1e9, nil, args) @@ -650,6 +696,7 @@ func testCalls(t *testing.T, jit bool) { large := testPrecompile(largeGas) if !arbmath.Within(large-small, largeGas-smallGas, 2) { + // #nosec G115 ratio := float64(int64(large)-int64(small)) / float64(int64(largeGas)-int64(smallGas)) Fatal(t, "inconsistent burns", large, small, largeGas, smallGas, ratio) } @@ -680,6 +727,13 @@ func testCalls(t *testing.T, jit bool) { Fatal(t, balance, value) } + // checks that ArbInfo.GetBalance works properly + balance, err = arbInfo.GetBalance(nil, eoa) + Require(t, err) + if !arbmath.BigEquals(balance, value) { + Fatal(t, balance, value) + } + blocks := []uint64{10} validateBlockRange(t, blocks, jit, builder) } @@ -947,6 +1001,31 @@ func testCreate(t *testing.T, jit bool) { validateBlockRange(t, blocks, jit, builder) } +func TestProgramInfiniteLoopShouldCauseErrOutOfGas(t *testing.T) { + t.Parallel() + testInfiniteLoopCausesErrOutOfGas(t, true) + testInfiniteLoopCausesErrOutOfGas(t, false) +} + +func testInfiniteLoopCausesErrOutOfGas(t *testing.T, jit bool) { + builder, auth, cleanup := setupProgramTest(t, jit) + ctx := builder.ctx + l2info := builder.L2Info + l2client := builder.L2.Client + defer cleanup() + + userWasm := deployWasm(t, ctx, auth, l2client, "../arbitrator/prover/test-cases/user.wat") + // Passing input of size 4 invokes $infinite_loop function that calls the infinite loop + tx := l2info.PrepareTxTo("Owner", &userWasm, 1000000, nil, make([]byte, 4)) + Require(t, l2client.SendTransaction(ctx, tx)) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + if !strings.Contains(err.Error(), vm.ErrOutOfGas.Error()) { + t.Fatalf("transaction should have failed with out of gas error but instead failed with: %v", err) + } + + validateBlocks(t, receipt.BlockNumber.Uint64(), jit, builder) +} + func TestProgramMemory(t *testing.T) { t.Parallel() testMemory(t, true) @@ -1206,6 +1285,140 @@ func testSdkStorage(t *testing.T, jit bool) { check() } +func TestStylusPrecompileMethodsSimple(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanup := builder.Build(t) + defer cleanup() + + arbOwner, err := pgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + arbDebug, err := pgen.NewArbDebug(types.ArbDebugAddress, builder.L2.Client) + Require(t, err) + arbWasm, err := pgen.NewArbWasm(types.ArbWasmAddress, builder.L2.Client) + Require(t, err) + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, builder.L2.Client, tx) + Require(t, err) + return receipt + } + + ownerAuth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + ensure(arbDebug.BecomeChainOwner(&ownerAuth)) + + wasm, _ := readWasmFile(t, rustFile("keccak")) + programAddress := deployContract(t, ctx, ownerAuth, builder.L2.Client, wasm) + + activateAuth := ownerAuth + activateAuth.Value = oneEth + ensure(arbWasm.ActivateProgram(&activateAuth, programAddress)) + + expectedExpiryDays := uint16(1) + ensure(arbOwner.SetWasmExpiryDays(&ownerAuth, expectedExpiryDays)) + ed, err := arbWasm.ExpiryDays(nil) + Require(t, err) + if ed != expectedExpiryDays { + t.Errorf("ExpiryDays from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", ed, expectedExpiryDays) + } + ptl, err := arbWasm.ProgramTimeLeft(nil, programAddress) + Require(t, err) + expectedExpirySeconds := (uint64(expectedExpiryDays) * 24 * 3600) + // ProgramTimeLeft returns time in seconds to expiry and the current ExpiryDays is set to 1 day + // We expect the lag of 3600 seconds to exist because program.activatedAt uses hoursSinceArbitrum that + // rounds down (the current time since ArbitrumStartTime in hours)/3600 + if expectedExpirySeconds-ptl > 3600 { + t.Errorf("ProgramTimeLeft from arbWasm precompile returned value lesser than expected. %d <= want <= %d, have: %d", expectedExpirySeconds-3600, expectedExpirySeconds, ptl) + } + + ensure(arbOwner.SetWasmBlockCacheSize(&ownerAuth, 100)) + bcs, err := arbWasm.BlockCacheSize(nil) + Require(t, err) + if bcs != 100 { + t.Errorf("BlockCacheSize from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", bcs, 100) + } + + ensure(arbOwner.SetWasmFreePages(&ownerAuth, 3)) + fp, err := arbWasm.FreePages(nil) + Require(t, err) + if fp != 3 { + t.Errorf("FreePages from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", fp, 3) + } + + ensure(arbOwner.SetWasmInitCostScalar(&ownerAuth, uint64(4))) + ics, err := arbWasm.InitCostScalar(nil) + Require(t, err) + if ics != uint64(4) { + t.Errorf("InitCostScalar from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", ics, 4) + } + + ensure(arbOwner.SetInkPrice(&ownerAuth, uint32(5))) + ip, err := arbWasm.InkPrice(nil) + Require(t, err) + if ip != uint32(5) { + t.Errorf("InkPrice from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", ip, 5) + } + + ensure(arbOwner.SetWasmKeepaliveDays(&ownerAuth, 0)) + kad, err := arbWasm.KeepaliveDays(nil) + Require(t, err) + if kad != 0 { + t.Errorf("KeepaliveDays from arbWasm precompile didnt match the value set by arbowner. have: %d, want: 0", kad) + } + + ensure(arbOwner.SetWasmMaxStackDepth(&ownerAuth, uint32(6))) + msd, err := arbWasm.MaxStackDepth(nil) + Require(t, err) + if msd != uint32(6) { + t.Errorf("MaxStackDepth from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", msd, 6) + } + + // Setting low values of gas and cached parameters ensures when MinInitGas is called on ArbWasm precompile, + // the returned values would be programs.MinInitGasUnits and programs.MinCachedGasUnits + ensure(arbOwner.SetWasmMinInitGas(&ownerAuth, 1, 1)) + mig, err := arbWasm.MinInitGas(nil) + Require(t, err) + if mig.Gas != programs.MinInitGasUnits { + t.Errorf("MinInitGas from arbWasm precompile didnt match the Gas value set by arbowner. have: %d, want: %d", mig.Gas, programs.MinInitGasUnits) + } + if mig.Cached != programs.MinCachedGasUnits { + t.Errorf("MinInitGas from arbWasm precompile didnt match the Cached value set by arbowner. have: %d, want: %d", mig.Cached, programs.MinCachedGasUnits) + } + + ensure(arbOwner.SetWasmPageGas(&ownerAuth, 7)) + pg, err := arbWasm.PageGas(nil) + Require(t, err) + if pg != 7 { + t.Errorf("PageGas from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", pg, 7) + } + + ensure(arbOwner.SetWasmPageLimit(&ownerAuth, 8)) + pl, err := arbWasm.PageLimit(nil) + Require(t, err) + if pl != 8 { + t.Errorf("PageLimit from arbWasm precompile didnt match the value set by arbowner. have: %d, want: %d", pl, 8) + } + + // pageramp currently is initialPageRamp = 620674314 value in programs package + _, err = arbWasm.PageRamp(nil) + Require(t, err) + + codehash := crypto.Keccak256Hash(wasm) + cas, err := arbWasm.CodehashAsmSize(nil, codehash) + Require(t, err) + if cas == 0 { + t.Error("CodehashAsmSize from arbWasm precompile returned 0 value") + } + // Since ArbOwner has set wasm KeepaliveDays to 0, it enables us to do this, though this shouldn't have any effect + codehashKeepaliveAuth := ownerAuth + codehashKeepaliveAuth.Value = oneEth + ensure(arbWasm.CodehashKeepalive(&codehashKeepaliveAuth, codehash)) +} + func TestProgramActivationLogs(t *testing.T) { t.Parallel() builder, auth, cleanup := setupProgramTest(t, true) @@ -1349,7 +1562,7 @@ func TestProgramCacheManager(t *testing.T) { isManager, err := arbWasmCache.IsCacheManager(nil, manager) assert(!isManager, err) - // athorize the manager + // authorize the manager ensure(arbOwner.AddWasmCacheManager(&ownerAuth, manager)) assert(arbWasmCache.IsCacheManager(nil, manager)) all, err := arbWasmCache.AllCacheManagers(nil) @@ -1527,6 +1740,7 @@ func readWasmFile(t *testing.T, file string) ([]byte, []byte) { Require(t, err) // chose a random dictionary for testing, but keep the same files consistent + // #nosec G115 randDict := arbcompress.Dictionary((len(file) + len(t.Name())) % 2) wasmSource, err := programs.Wat2Wasm(source) @@ -1597,6 +1811,7 @@ func argsForMulticall(opcode vm.OpCode, address common.Address, value *big.Int, if opcode == vm.CALL { length += 32 } + // #nosec G115 args = append(args, arbmath.Uint32ToBytes(uint32(length))...) args = append(args, kinds[opcode]) if opcode == vm.CALL { @@ -1829,7 +2044,9 @@ func createMapFromDb(db ethdb.KeyValueStore) (map[string][]byte, error) { } func TestWasmStoreRebuilding(t *testing.T) { - builder, auth, cleanup := setupProgramTest(t, true) + builder, auth, cleanup := setupProgramTest(t, true, func(b *NodeBuilder) { + b.WithExtraArchs(allWasmTargets) + }) ctx := builder.ctx l2info := builder.L2Info l2client := builder.L2.Client @@ -1866,6 +2083,7 @@ func TestWasmStoreRebuilding(t *testing.T) { storeMap, err := createMapFromDb(wasmDb) Require(t, err) + checkWasmStoreContent(t, wasmDb, builder.execConfig.StylusTarget.ExtraArchs, 1) // close nodeB cleanupB() @@ -1891,7 +2109,8 @@ func TestWasmStoreRebuilding(t *testing.T) { // Start rebuilding and wait for it to finish log.Info("starting rebuilding of wasm store") - Require(t, gethexec.RebuildWasmStore(ctx, wasmDbAfterDelete, nodeB.ExecNode.ChainDB, nodeB.ExecNode.ConfigFetcher().RPC.MaxRecreateStateDepth, bc, common.Hash{}, bc.CurrentBlock().Hash())) + execConfig := nodeB.ExecNode.ConfigFetcher() + Require(t, gethexec.RebuildWasmStore(ctx, wasmDbAfterDelete, nodeB.ExecNode.ChainDB, execConfig.RPC.MaxRecreateStateDepth, &execConfig.StylusTarget, bc, common.Hash{}, bc.CurrentBlock().Hash())) wasmDbAfterRebuild := nodeB.ExecNode.Backend.ArbInterface().BlockChain().StateCache().WasmStore() @@ -1921,5 +2140,452 @@ func TestWasmStoreRebuilding(t *testing.T) { } } + checkWasmStoreContent(t, wasmDbAfterRebuild, builder.execConfig.StylusTarget.ExtraArchs, 1) cleanupB() } + +func readModuleHashes(t *testing.T, wasmDb ethdb.KeyValueStore) []common.Hash { + modulesSet := make(map[common.Hash]struct{}) + asmPrefix := []byte{0x00, 'w'} + it := wasmDb.NewIterator(asmPrefix, nil) + defer it.Release() + for it.Next() { + key := it.Key() + if len(key) != rawdb.WasmKeyLen { + t.Fatalf("unexpected activated module key length, len: %d, key: %v", len(key), key) + } + moduleHash := key[rawdb.WasmPrefixLen:] + if len(moduleHash) != common.HashLength { + t.Fatalf("Invalid moduleHash length in key: %v, moduleHash: %v", key, moduleHash) + } + modulesSet[common.BytesToHash(moduleHash)] = struct{}{} + } + modules := make([]common.Hash, 0, len(modulesSet)) + for module := range modulesSet { + modules = append(modules, module) + } + return modules +} + +func checkWasmStoreContent(t *testing.T, wasmDb ethdb.KeyValueStore, targets []string, numModules int) { + modules := readModuleHashes(t, wasmDb) + if len(modules) != numModules { + t.Fatalf("Unexpected number of module hashes found in wasm store, want: %d, have: %d", numModules, len(modules)) + } + for _, module := range modules { + for _, target := range targets { + wasmTarget := ethdb.WasmTarget(target) + if !rawdb.IsSupportedWasmTarget(wasmTarget) { + t.Fatalf("internal test error - unsupported target passed to checkWasmStoreContent: %v", target) + } + func() { + defer func() { + if r := recover(); r != nil { + t.Fatalf("Failed to read activated asm for target: %v, module: %v", target, module) + } + }() + _ = rawdb.ReadActivatedAsm(wasmDb, wasmTarget, module) + }() + } + } +} + +func deployWasmAndGetEntrySizeEstimateBytes( + t *testing.T, + builder *NodeBuilder, + auth bind.TransactOpts, + wasmName string, +) (common.Address, uint64) { + ctx := builder.ctx + l2client := builder.L2.Client + + wasm, _ := readWasmFile(t, rustFile(wasmName)) + arbWasm, err := pgen.NewArbWasm(types.ArbWasmAddress, l2client) + Require(t, err, ", wasmName:", wasmName) + + programAddress := deployContract(t, ctx, auth, l2client, wasm) + tx, err := arbWasm.ActivateProgram(&auth, programAddress) + Require(t, err, ", wasmName:", wasmName) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err, ", wasmName:", wasmName) + + if len(receipt.Logs) != 1 { + Fatal(t, "expected 1 log while activating, got ", len(receipt.Logs), ", wasmName:", wasmName) + } + log, err := arbWasm.ParseProgramActivated(*receipt.Logs[0]) + Require(t, err, ", wasmName:", wasmName) + + statedb, err := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().State() + Require(t, err, ", wasmName:", wasmName) + + module, err := statedb.TryGetActivatedAsm(rawdb.LocalTarget(), log.ModuleHash) + Require(t, err, ", wasmName:", wasmName) + + entrySizeEstimateBytes := programs.GetEntrySizeEstimateBytes(module, log.Version, true) + // just a sanity check + if entrySizeEstimateBytes == 0 { + Fatal(t, "entrySizeEstimateBytes is 0, wasmName:", wasmName) + } + return programAddress, entrySizeEstimateBytes +} + +func TestWasmLruCache(t *testing.T) { + builder, auth, cleanup := setupProgramTest(t, true) + ctx := builder.ctx + l2info := builder.L2Info + l2client := builder.L2.Client + defer cleanup() + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt + } + + auth.GasLimit = 32000000 + auth.Value = oneEth + + fallibleProgramAddress, fallibleEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, auth, "fallible") + keccakProgramAddress, keccakEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, auth, "keccak") + mathProgramAddress, mathEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, auth, "math") + t.Log( + "entrySizeEstimateBytes, ", + "fallible:", fallibleEntrySize, + "keccak:", keccakEntrySize, + "math:", mathEntrySize, + ) + if fallibleEntrySize == keccakEntrySize || fallibleEntrySize == mathEntrySize || keccakEntrySize == mathEntrySize { + Fatal(t, "at least two programs have the same entry size") + } + + programs.ClearWasmLruCache() + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + programs.SetWasmLruCacheCapacity(fallibleEntrySize - 1) + // fallible wasm program will not be cached since its size is greater than lru cache capacity + tx := l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + programs.SetWasmLruCacheCapacity( + fallibleEntrySize + keccakEntrySize + mathEntrySize - 1, + ) + // fallible wasm program will be cached + tx = l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // keccak wasm program will be cached + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + + // math wasm program will be cached, but fallible will be evicted since (fallible + keccak + math) > lruCacheCapacity + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: keccakEntrySize + mathEntrySize, + }) +} + +func checkLongTermCacheMetrics(t *testing.T, expected programs.WasmLongTermCacheMetrics) { + t.Helper() + longTermMetrics := programs.GetWasmCacheMetrics().LongTerm + if longTermMetrics.Count != expected.Count { + t.Fatalf("longTermMetrics.Count, expected: %v, actual: %v", expected.Count, longTermMetrics.Count) + } + if longTermMetrics.SizeBytes != expected.SizeBytes { + t.Fatalf("longTermMetrics.SizeBytes, expected: %v, actual: %v", expected.SizeBytes, longTermMetrics.SizeBytes) + } +} + +func checkLruCacheMetrics(t *testing.T, expected programs.WasmLruCacheMetrics) { + t.Helper() + lruMetrics := programs.GetWasmCacheMetrics().Lru + if lruMetrics.Count != expected.Count { + t.Fatalf("lruMetrics.Count, expected: %v, actual: %v", expected.Count, lruMetrics.Count) + } + if lruMetrics.SizeBytes != expected.SizeBytes { + t.Fatalf("lruMetrics.SizeBytes, expected: %v, actual: %v", expected.SizeBytes, lruMetrics.SizeBytes) + } +} + +func TestWasmLongTermCache(t *testing.T) { + builder, ownerAuth, cleanup := setupProgramTest(t, true, func(builder *NodeBuilder) { + builder.WithStylusLongTermCache(true) + }) + ctx := builder.ctx + l2info := builder.L2Info + l2client := builder.L2.Client + defer cleanup() + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt + } + + arbWasmCache, err := pgen.NewArbWasmCache(types.ArbWasmCacheAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := pgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + ensure(arbOwner.SetInkPrice(&ownerAuth, 10_000)) + + ownerAuth.GasLimit = 32000000 + ownerAuth.Value = oneEth + + fallibleProgramAddress, fallibleEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "fallible") + keccakProgramAddress, keccakEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "keccak") + mathProgramAddress, mathEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "math") + t.Log( + "entrySizeEstimateBytes, ", + "fallible:", fallibleEntrySize, + "keccak:", keccakEntrySize, + "math:", mathEntrySize, + ) + if fallibleEntrySize == keccakEntrySize || fallibleEntrySize == mathEntrySize || keccakEntrySize == mathEntrySize { + Fatal(t, "at least two programs have the same entry size") + } + + ownerAuth.Value = common.Big0 + + programs.ClearWasmLongTermCache() + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + // fallible wasm program will not be cached since caching is not set for this program + tx := l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + ensure(arbWasmCache.CacheProgram(&ownerAuth, fallibleProgramAddress)) + // fallible wasm program will be cached + tx = l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // keccak wasm program will be cached + ensure(arbWasmCache.CacheProgram(&ownerAuth, keccakProgramAddress)) + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + + // math wasm program will not be cached + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + + // math wasm program will be cached + ensure(arbWasmCache.CacheProgram(&ownerAuth, mathProgramAddress)) + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 3, + SizeBytes: fallibleEntrySize + keccakEntrySize + mathEntrySize, + }) + + statedb, err := builder.L2.ExecNode.Backend.ArbInterface().BlockChain().State() + Require(t, err) + fallibleProgramHash := statedb.GetCodeHash(fallibleProgramAddress) + keccakProgramHash := statedb.GetCodeHash(keccakProgramAddress) + mathProgramHash := statedb.GetCodeHash(mathProgramAddress) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, keccakProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + mathEntrySize, + }) + + // keccak wasm program will not be cached + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + mathEntrySize, + }) + + // keccak wasm program will be cached + ensure(arbWasmCache.CacheProgram(&ownerAuth, keccakProgramAddress)) + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 3, + SizeBytes: fallibleEntrySize + keccakEntrySize + mathEntrySize, + }) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, fallibleProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 2, + SizeBytes: keccakEntrySize + mathEntrySize, + }) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, mathProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: keccakEntrySize, + }) + + ensure(arbWasmCache.EvictCodehash(&ownerAuth, keccakProgramHash)) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) +} + +func TestRepopulateWasmLongTermCacheFromLru(t *testing.T) { + builder, ownerAuth, cleanup := setupProgramTest(t, true, func(builder *NodeBuilder) { + builder.WithStylusLongTermCache(true) + }) + ctx := builder.ctx + l2info := builder.L2Info + l2client := builder.L2.Client + defer cleanup() + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt + } + + arbWasmCache, err := pgen.NewArbWasmCache(types.ArbWasmCacheAddress, builder.L2.Client) + Require(t, err) + arbOwner, err := pgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + Require(t, err) + ensure(arbOwner.SetInkPrice(&ownerAuth, 10_000)) + + ownerAuth.GasLimit = 32000000 + ownerAuth.Value = oneEth + + fallibleProgramAddress, fallibleEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "fallible") + keccakProgramAddress, keccakEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "keccak") + mathProgramAddress, mathEntrySize := deployWasmAndGetEntrySizeEstimateBytes(t, builder, ownerAuth, "math") + if fallibleEntrySize == keccakEntrySize || fallibleEntrySize == mathEntrySize || keccakEntrySize == mathEntrySize { + Fatal(t, "at least two programs have the same entry size") + } + + ownerAuth.Value = common.Big0 + + programs.ClearWasmLongTermCache() + programs.ClearWasmLruCache() + // only 2 out of 3 programs should fit lru + programs.SetWasmLruCacheCapacity( + fallibleEntrySize + keccakEntrySize + mathEntrySize - 1, + ) + + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + ensure(arbWasmCache.CacheProgram(&ownerAuth, fallibleProgramAddress)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // clear long term cache to emulate restart + programs.ClearWasmLongTermCache() + programs.ClearWasmLruCache() + + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + nonce := builder.L2Info.GetInfoWithPrivKey("Owner").Nonce.Load() + tx := l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + _, err = arbutil.SendTxAsCall(ctx, l2client, tx, l2info.GetAddress("Owner"), nil, true) + Require(t, err) + // restore nonce in L2Info + builder.L2Info.GetInfoWithPrivKey("Owner").Nonce.Store(nonce) + // fallibleProgram should be added only to lru cache as the api call should be processed with wasm cache tag = 0 + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + tx = l2info.PrepareTxTo("Owner", &keccakProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 0, + SizeBytes: 0, + }) + + tx = l2info.PrepareTxTo("Owner", &fallibleProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: fallibleEntrySize + keccakEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) + + // mathProgram should end up in lru cache and + // as result fallibleProgram should be evicted as least recently used item (tx that restores the program back to long term cache shouldn't promote the lru item); + // fallibleProgram should remain in long term cache + tx = l2info.PrepareTxTo("Owner", &mathProgramAddress, l2info.TransferGas, nil, []byte{0x01}) + ensure(tx, l2client.SendTransaction(ctx, tx)) + checkLruCacheMetrics(t, programs.WasmLruCacheMetrics{ + Count: 2, + SizeBytes: keccakEntrySize + mathEntrySize, + }) + checkLongTermCacheMetrics(t, programs.WasmLongTermCacheMetrics{ + Count: 1, + SizeBytes: fallibleEntrySize, + }) +} diff --git a/system_tests/pruning_test.go b/system_tests/pruning_test.go index 90ac3c6909..f49ed8ddcf 100644 --- a/system_tests/pruning_test.go +++ b/system_tests/pruning_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/trie" + "github.com/offchainlabs/nitro/cmd/conf" "github.com/offchainlabs/nitro/cmd/pruning" "github.com/offchainlabs/nitro/execution/gethexec" diff --git a/system_tests/recreatestate_rpc_test.go b/system_tests/recreatestate_rpc_test.go index 09d53669ee..dc1356347c 100644 --- a/system_tests/recreatestate_rpc_test.go +++ b/system_tests/recreatestate_rpc_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/big" + "runtime" "strings" "sync" "testing" @@ -20,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" + "github.com/offchainlabs/nitro/execution/gethexec" "github.com/offchainlabs/nitro/util" ) @@ -95,7 +97,7 @@ func removeStatesFromDb(t *testing.T, bc *core.BlockChain, db ethdb.Database, fr func TestRecreateStateForRPCNoDepthLimit(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = arbitrum.InfiniteMaxRecreateStateDepth execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -132,8 +134,9 @@ func TestRecreateStateForRPCNoDepthLimit(t *testing.T) { func TestRecreateStateForRPCBigEnoughDepthLimit(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // #nosec G115 depthGasLimit := int64(256 * util.NormalizeL2GasForL1GasInitial(800_000, params.GWei)) - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = depthGasLimit execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -170,7 +173,7 @@ func TestRecreateStateForRPCBigEnoughDepthLimit(t *testing.T) { func TestRecreateStateForRPCDepthLimitExceeded(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = int64(200) execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -207,7 +210,7 @@ func TestRecreateStateForRPCMissingBlockParent(t *testing.T) { var headerCacheLimit uint64 = 512 ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = arbitrum.InfiniteMaxRecreateStateDepth execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -255,7 +258,7 @@ func TestRecreateStateForRPCBeyondGenesis(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = arbitrum.InfiniteMaxRecreateStateDepth execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -293,7 +296,7 @@ func TestRecreateStateForRPCBlockNotFoundWhileRecreating(t *testing.T) { var blockCacheLimit uint64 = 256 ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = arbitrum.InfiniteMaxRecreateStateDepth execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -337,11 +340,12 @@ func TestRecreateStateForRPCBlockNotFoundWhileRecreating(t *testing.T) { } func testSkippingSavingStateAndRecreatingAfterRestart(t *testing.T, cacheConfig *gethexec.CachingConfig, txCount int) { + t.Parallel() maxRecreateStateDepth := int64(30 * 1000 * 1000) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() + execConfig := ExecConfigDefaultTest(t) execConfig.RPC.MaxRecreateStateDepth = maxRecreateStateDepth execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 @@ -361,12 +365,14 @@ func testSkippingSavingStateAndRecreatingAfterRestart(t *testing.T, cacheConfig Require(t, err) l2info.GenerateAccount("User2") + // #nosec G115 for i := genesis; i < uint64(txCount)+genesis; i++ { tx := l2info.PrepareTx("Owner", "User2", l2info.TransferGas, common.Big1, nil) err := client.SendTransaction(ctx, tx) Require(t, err) receipt, err := EnsureTxSucceeded(ctx, client, tx) Require(t, err) + // #nosec G115 if have, want := receipt.BlockNumber.Uint64(), uint64(i)+1; have != want { Fatal(t, "internal test error - tx got included in unexpected block number, have:", have, "want:", want) } @@ -377,6 +383,7 @@ func testSkippingSavingStateAndRecreatingAfterRestart(t *testing.T, cacheConfig Fatal(t, "missing current block") } lastBlock := currentHeader.Number.Uint64() + // #nosec G115 if want := genesis + uint64(txCount); lastBlock < want { Fatal(t, "internal test error - not enough blocks produced during preparation, want:", want, "have:", lastBlock) } @@ -390,6 +397,7 @@ func testSkippingSavingStateAndRecreatingAfterRestart(t *testing.T, cacheConfig bc = builder.L2.ExecNode.Backend.ArbInterface().BlockChain() gas := skipGas blocks := skipBlocks + // #nosec G115 for i := genesis; i <= genesis+uint64(txCount); i++ { block := bc.GetBlockByNumber(i) if block == nil { @@ -407,6 +415,7 @@ func testSkippingSavingStateAndRecreatingAfterRestart(t *testing.T, cacheConfig gas = 0 blocks = 0 } else { + // #nosec G115 if int(i) >= int(lastBlock)-int(cacheConfig.BlockCount) { // skipping nonexistence check - the state might have been saved on node shutdown continue @@ -421,6 +430,7 @@ func testSkippingSavingStateAndRecreatingAfterRestart(t *testing.T, cacheConfig } } } + // #nosec G115 for i := genesis + 1; i <= genesis+uint64(txCount); i += i % 10 { _, err = client.BalanceAt(ctx, GetTestAddressForAccountName(t, "User2"), new(big.Int).SetUint64(i)) if err != nil { @@ -444,20 +454,26 @@ func TestSkippingSavingStateAndRecreatingAfterRestart(t *testing.T) { cacheConfig.SnapshotCache = 0 // disable snapshots cacheConfig.BlockAge = 0 // use only Caching.BlockCount to keep only last N blocks in dirties cache, no matter how new they are + runTestCase := func(t *testing.T, cacheConfig gethexec.CachingConfig, txes int) { + t.Run(fmt.Sprintf("skip-blocks-%d-skip-gas-%d-txes-%d", cacheConfig.MaxNumberOfBlocksToSkipStateSaving, cacheConfig.MaxAmountOfGasToSkipStateSaving, txes), func(t *testing.T) { + testSkippingSavingStateAndRecreatingAfterRestart(t, &cacheConfig, txes) + }) + } + // test defaults - testSkippingSavingStateAndRecreatingAfterRestart(t, &cacheConfig, 512) + runTestCase(t, cacheConfig, 512) cacheConfig.MaxNumberOfBlocksToSkipStateSaving = 127 cacheConfig.MaxAmountOfGasToSkipStateSaving = 0 - testSkippingSavingStateAndRecreatingAfterRestart(t, &cacheConfig, 512) + runTestCase(t, cacheConfig, 512) cacheConfig.MaxNumberOfBlocksToSkipStateSaving = 0 cacheConfig.MaxAmountOfGasToSkipStateSaving = 15 * 1000 * 1000 - testSkippingSavingStateAndRecreatingAfterRestart(t, &cacheConfig, 512) + runTestCase(t, cacheConfig, 512) cacheConfig.MaxNumberOfBlocksToSkipStateSaving = 127 cacheConfig.MaxAmountOfGasToSkipStateSaving = 15 * 1000 * 1000 - testSkippingSavingStateAndRecreatingAfterRestart(t, &cacheConfig, 512) + runTestCase(t, cacheConfig, 512) // lower number of blocks in triegc below 100 blocks, to be able to check for nonexistence in testSkippingSavingStateAndRecreatingAfterRestart (it doesn't check last BlockCount blocks as some of them may be persisted on node shutdown) cacheConfig.BlockCount = 16 @@ -471,22 +487,18 @@ func TestSkippingSavingStateAndRecreatingAfterRestart(t *testing.T) { for _, skipGas := range skipGasValues { for _, skipBlocks := range skipBlockValues[:len(skipBlockValues)-2] { cacheConfig.MaxAmountOfGasToSkipStateSaving = skipGas + // #nosec G115 cacheConfig.MaxNumberOfBlocksToSkipStateSaving = uint32(skipBlocks) - testSkippingSavingStateAndRecreatingAfterRestart(t, &cacheConfig, 100) + runTestCase(t, cacheConfig, 100) } } } -func TestGettingStateForRPCFullNode(t *testing.T) { +func testGettingState(t *testing.T, execConfig *gethexec.Config) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - execConfig := ExecConfigDefaultTest() - execConfig.Caching.SnapshotCache = 0 // disable snapshots - execConfig.Caching.BlockAge = 0 // use only Caching.BlockCount to keep only last N blocks in dirties cache, no matter how new they are - execConfig.Sequencer.MaxBlockSpeed = 0 - execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 builder, cancelNode := prepareNodeWithHistory(t, ctx, execConfig, 16) - execNode, _ := builder.L2.ExecNode, builder.L2.Client + execNode := builder.L2.ExecNode defer cancelNode() bc := execNode.Backend.ArbInterface().BlockChain() api := execNode.Backend.APIBackend() @@ -495,6 +507,7 @@ func TestGettingStateForRPCFullNode(t *testing.T) { if header == nil { Fatal(t, "failed to get current block header") } + // #nosec G115 state, _, err := api.StateAndHeaderByNumber(ctx, rpc.BlockNumber(header.Number.Uint64())) Require(t, err) addr := builder.L2Info.GetAddress("User2") @@ -505,24 +518,47 @@ func TestGettingStateForRPCFullNode(t *testing.T) { Fatal(t, "User2 address does not exist in the state") } // Get the state again to avoid caching + // #nosec G115 state, _, err = api.StateAndHeaderByNumber(ctx, rpc.BlockNumber(header.Number.Uint64())) Require(t, err) blockCountRequiredToFlushDirties := builder.execConfig.Caching.BlockCount makeSomeTransfers(t, ctx, builder, blockCountRequiredToFlushDirties) + // force garbage collection to check if it won't break anything + runtime.GC() + exists = state.Exist(addr) err = state.Error() Require(t, err) if !exists { Fatal(t, "User2 address does not exist in the state") } + + // force garbage collection of StateDB object, what should cause the state finalizer to run + state = nil + runtime.GC() + _, err = bc.StateAt(header.Root) + if err == nil { + Fatal(t, "StateAndHeaderByNumber didn't failed as expected") + } + expectedErr := &trie.MissingNodeError{} + if !errors.As(err, &expectedErr) { + Fatal(t, "StateAndHeaderByNumber failed with unexpected error:", err) + } } -func TestGettingStateForRPCHybridArchiveNode(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - execConfig := ExecConfigDefaultTest() +func TestGettingState(t *testing.T) { + execConfig := ExecConfigDefaultTest(t) + execConfig.Caching.SnapshotCache = 0 // disable snapshots + execConfig.Caching.BlockAge = 0 // use only Caching.BlockCount to keep only last N blocks in dirties cache, no matter how new they are + execConfig.Sequencer.MaxBlockSpeed = 0 + execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 + t.Run("full-node", func(t *testing.T) { + testGettingState(t, execConfig) + }) + + execConfig = ExecConfigDefaultTest(t) execConfig.Caching.Archive = true // For now Archive node should use HashScheme execConfig.Caching.StateScheme = rawdb.HashScheme @@ -532,40 +568,13 @@ func TestGettingStateForRPCHybridArchiveNode(t *testing.T) { execConfig.Caching.BlockAge = 0 // use only Caching.BlockCount to keep only last N blocks in dirties cache, no matter how new they are execConfig.Sequencer.MaxBlockSpeed = 0 execConfig.Sequencer.MaxTxDataSize = 150 // 1 test tx ~= 110 - builder, cancelNode := prepareNodeWithHistory(t, ctx, execConfig, 16) - execNode, _ := builder.L2.ExecNode, builder.L2.Client - defer cancelNode() - bc := execNode.Backend.ArbInterface().BlockChain() - api := execNode.Backend.APIBackend() - - header := bc.CurrentBlock() - if header == nil { - Fatal(t, "failed to get current block header") - } - state, _, err := api.StateAndHeaderByNumber(ctx, rpc.BlockNumber(header.Number.Uint64())) - Require(t, err) - addr := builder.L2Info.GetAddress("User2") - exists := state.Exist(addr) - err = state.Error() - Require(t, err) - if !exists { - Fatal(t, "User2 address does not exist in the state") - } - // Get the state again to avoid caching - state, _, err = api.StateAndHeaderByNumber(ctx, rpc.BlockNumber(header.Number.Uint64())) - Require(t, err) - - blockCountRequiredToFlushDirties := builder.execConfig.Caching.BlockCount - makeSomeTransfers(t, ctx, builder, blockCountRequiredToFlushDirties) - - exists = state.Exist(addr) - err = state.Error() - Require(t, err) - if !exists { - Fatal(t, "User2 address does not exist in the state") - } + t.Run("archive-node", func(t *testing.T) { + testGettingState(t, execConfig) + }) } +// regression test for issue caused by accessing block state that has just been committed to TrieDB but not yet referenced in core.BlockChain.writeBlockWithState (here called state of "recent" block) +// before the corresponding fix, access to the recent block state caused premature garbage collection of the head block state func TestStateAndHeaderForRecentBlock(t *testing.T) { threads := 32 ctx, cancel := context.WithCancel(context.Background()) @@ -606,15 +615,22 @@ func TestStateAndHeaderForRecentBlock(t *testing.T) { }() api := builder.L2.ExecNode.Backend.APIBackend() db := builder.L2.ExecNode.Backend.ChainDb() - i := 1 + + recentBlock := 1 var mtx sync.RWMutex var wgCallers sync.WaitGroup for j := 0; j < threads && ctx.Err() == nil; j++ { wgCallers.Add(1) + // each thread attempts to get state for a block that is just being created (here called recent): + // 1. Before state trie node is referenced in core.BlockChain.writeBlockWithState, block body is written to database with key prefix `b` followed by block number and then block hash (see: rawdb.blockBodyKey) + // 2. Each thread tries to read the block body entry to: a. extract recent block hash b. congest resource usage to slow down execution of core.BlockChain.writeBlockWithState + // 3. After extracting the hash from block body entry key, StateAndHeaderByNumberOfHash is called for the hash. It is expected that it will: + // a. either fail with "ahead of current block" if we made it before rawdb.WriteCanonicalHash is called in core.BlockChain.writeHeadBlock, which is called after writeBlockWithState finishes, + // b. or it will succeed if the canonical hash was written for the block meaning that writeBlockWithState was fully executed (i.a. state root trie node correctly referenced) - then the recentBlock is advanced go func() { defer wgCallers.Done() mtx.RLock() - blockNumber := i + blockNumber := recentBlock mtx.RUnlock() for blockNumber < 300 && ctx.Err() == nil { prefix := make([]byte, 8) @@ -633,8 +649,8 @@ func TestStateAndHeaderForRecentBlock(t *testing.T) { _, _, err := api.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHash{BlockHash: &blockHash}) if err == nil { mtx.Lock() - if blockNumber == i { - i++ + if blockNumber == recentBlock { + recentBlock++ } mtx.Unlock() break @@ -654,7 +670,7 @@ func TestStateAndHeaderForRecentBlock(t *testing.T) { } it.Release() mtx.RLock() - blockNumber = i + blockNumber = recentBlock mtx.RUnlock() } }() diff --git a/system_tests/reorg_resequencing_test.go b/system_tests/reorg_resequencing_test.go index 70ab63bec1..8f452ce95d 100644 --- a/system_tests/reorg_resequencing_test.go +++ b/system_tests/reorg_resequencing_test.go @@ -88,7 +88,7 @@ func TestReorgResequencing(t *testing.T) { err = builder.L2.ConsensusNode.TxStreamer.AddMessages(startMsgCount, true, []arbostypes.MessageWithMetadata{{ Message: newMessage, DelayedMessagesRead: prevMessage.DelayedMessagesRead + 1, - }}) + }}, nil) Require(t, err) _, err = builder.L2.ExecNode.ExecEngine.HeadMessageNumberSync(t) diff --git a/system_tests/retryable_test.go b/system_tests/retryable_test.go index 106dfc6d46..49bba81374 100644 --- a/system_tests/retryable_test.go +++ b/system_tests/retryable_test.go @@ -16,13 +16,14 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/gasestimator" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/util" - + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/node_interfacegen" @@ -75,7 +76,7 @@ func retryableSetup(t *testing.T, modifyNodeConfig ...func(*NodeBuilder)) ( if !msgTypes[message.Message.Header.Kind] { continue } - txs, err := arbos.ParseL2Transactions(message.Message, params.ArbitrumDevTestChainConfig().ChainID) + txs, err := arbos.ParseL2Transactions(message.Message, chaininfo.ArbitrumDevTestChainConfig().ChainID) Require(t, err) for _, tx := range txs { if txTypes[tx.Type()] { @@ -315,7 +316,7 @@ func testSubmitRetryableEmptyEscrow(t *testing.T, arbosVersion uint64) { state, err := builder.L2.ExecNode.ArbInterface.BlockChain().State() Require(t, err) escrowExists := state.Exist(escrowAccount) - if escrowExists != (arbosVersion < 30) { + if escrowExists != (arbosVersion < params.ArbosVersion_30) { Fatal(t, "Escrow account existance", escrowExists, "doesn't correspond to ArbOS version", arbosVersion) } } @@ -423,6 +424,118 @@ func TestSubmitRetryableFailThenRetry(t *testing.T) { } } +func TestGetLifetime(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + callOpts := &bind.CallOpts{Context: ctx} + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(common.HexToAddress("6e"), builder.L2.Client) + Require(t, err) + + lifetime, err := arbRetryableTx.GetLifetime(callOpts) + Require(t, err) + if lifetime.Cmp(big.NewInt(retryables.RetryableLifetimeSeconds)) != 0 { + t.Fatal("Expected to be ", retryables.RetryableLifetimeSeconds, " but got ", lifetime) + } +} + +func TestKeepaliveAndCancelRetryable(t *testing.T) { + t.Parallel() + builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) + defer teardown() + + ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + usertxopts := builder.L1Info.GetDefaultTransactOpts("Faucet", ctx) + usertxopts.Value = arbmath.BigMul(big.NewInt(1e12), big.NewInt(1e12)) + + simpleAddr, _ := builder.L2.DeploySimple(t, ownerTxOpts) + simpleABI, err := mocksgen.SimpleMetaData.GetAbi() + Require(t, err) + + beneficiaryAddress := builder.L2Info.GetAddress("Beneficiary") + l1tx, err := delayedInbox.CreateRetryableTicket( + &usertxopts, + simpleAddr, + common.Big0, + big.NewInt(1e16), + beneficiaryAddress, + beneficiaryAddress, + // send enough L2 gas for intrinsic but not compute + big.NewInt(int64(params.TxGas+params.TxDataNonZeroGasEIP2028*4)), + big.NewInt(l2pricing.InitialBaseFeeWei*2), + simpleABI.Methods["incrementRedeem"].ID, + ) + Require(t, err) + + l1Receipt, err := builder.L1.EnsureTxSucceeded(l1tx) + Require(t, err) + if l1Receipt.Status != types.ReceiptStatusSuccessful { + Fatal(t, "l1Receipt indicated failure") + } + + waitForL1DelayBlocks(t, builder) + + receipt, err := builder.L2.EnsureTxSucceeded(lookupL2Tx(l1Receipt)) + Require(t, err) + if len(receipt.Logs) != 2 { + Fatal(t, len(receipt.Logs)) + } + ticketId := receipt.Logs[0].Topics[1] + firstRetryTxId := receipt.Logs[1].Topics[2] + + // make sure it failed + receipt, err = WaitForTx(ctx, builder.L2.Client, firstRetryTxId, time.Second*5) + Require(t, err) + if receipt.Status != types.ReceiptStatusFailed { + Fatal(t, receipt.GasUsed) + } + + arbRetryableTx, err := precompilesgen.NewArbRetryableTx(common.HexToAddress("6e"), builder.L2.Client) + Require(t, err) + + // checks that the ticket exists and gets current timeout + timeoutBeforeKeepalive, err := arbRetryableTx.GetTimeout(&bind.CallOpts{}, ticketId) + Require(t, err) + + // checks beneficiary + retrievedBeneficiaryAddress, err := arbRetryableTx.GetBeneficiary(&bind.CallOpts{}, ticketId) + Require(t, err) + if retrievedBeneficiaryAddress != beneficiaryAddress { + Fatal(t, "expected beneficiary to be", beneficiaryAddress, "but got", retrievedBeneficiaryAddress) + } + + // checks that keepalive increases the timeout as expected + _, err = arbRetryableTx.Keepalive(&ownerTxOpts, ticketId) + Require(t, err) + timeoutAfterKeepalive, err := arbRetryableTx.GetTimeout(&bind.CallOpts{}, ticketId) + Require(t, err) + expectedTimeoutAfterKeepAlive := timeoutBeforeKeepalive + expectedTimeoutAfterKeepAlive.Add(expectedTimeoutAfterKeepAlive, big.NewInt(retryables.RetryableLifetimeSeconds)) + if timeoutAfterKeepalive.Cmp(expectedTimeoutAfterKeepAlive) != 0 { + Fatal(t, "expected timeout after keepalive to be", expectedTimeoutAfterKeepAlive, "but got", timeoutAfterKeepalive) + } + + // cancel the ticket + beneficiaryTxOpts := builder.L2Info.GetDefaultTransactOpts("Beneficiary", ctx) + tx, err := arbRetryableTx.Cancel(&beneficiaryTxOpts, ticketId) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // checks that the ticket no longer exists + _, err = arbRetryableTx.GetTimeout(&bind.CallOpts{}, ticketId) + if (err == nil) || (err.Error() != "execution reverted: error NoTicketWithID(): NoTicketWithID()") { + Fatal(t, "didn't get expected NoTicketWithID error") + } +} + func TestSubmissionGasCosts(t *testing.T) { t.Parallel() builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) @@ -1042,7 +1155,7 @@ func elevateL2Basefee(t *testing.T, ctx context.Context, builder *NodeBuilder) { _, err = precompilesgen.NewArbosTest(common.HexToAddress("0x69"), builder.L2.Client) Require(t, err, "failed to deploy ArbosTest") - burnAmount := ExecConfigDefaultTest().RPC.RPCGasCap + burnAmount := ExecConfigDefaultTest(t).RPC.RPCGasCap burnTarget := uint64(5 * l2pricing.InitialSpeedLimitPerSecondV6 * l2pricing.InitialBacklogTolerance) for i := uint64(0); i < (burnTarget+burnAmount)/burnAmount; i++ { burnArbGas := arbosTestAbi.Methods["burnArbGas"] diff --git a/system_tests/seq_coordinator_test.go b/system_tests/seq_coordinator_test.go index 1b8926a1b9..f6de83b3d3 100644 --- a/system_tests/seq_coordinator_test.go +++ b/system_tests/seq_coordinator_test.go @@ -8,11 +8,10 @@ import ( "errors" "fmt" "math/big" - "net" "testing" "time" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -90,12 +89,12 @@ func TestRedisSeqCoordinatorPriorities(t *testing.T) { }, DelayedMessagesRead: 1, } - err = node.SeqCoordinator.SequencingMessage(curMsgs, &emptyMessage) + err = node.SeqCoordinator.SequencingMessage(curMsgs, &emptyMessage, nil) if errors.Is(err, execution.ErrRetrySequencer) { return false } Require(t, err) - Require(t, node.TxStreamer.AddMessages(curMsgs, false, []arbostypes.MessageWithMetadata{emptyMessage})) + Require(t, node.TxStreamer.AddMessages(curMsgs, false, []arbostypes.MessageWithMetadata{emptyMessage}, nil)) return true } @@ -153,7 +152,15 @@ func TestRedisSeqCoordinatorPriorities(t *testing.T) { nodeForwardTarget := func(nodeNum int) int { execNode := testNodes[nodeNum].ExecNode - fwTarget := execNode.TxPublisher.(*gethexec.TxPreChecker).TransactionPublisher.(*gethexec.Sequencer).ForwardTarget() + preChecker, ok := execNode.TxPublisher.(*gethexec.TxPreChecker) + if !ok { + return -1 + } + sequencer, ok := preChecker.TransactionPublisher.(*gethexec.Sequencer) + if !ok { + return -1 + } + fwTarget := sequencer.ForwardTarget() if fwTarget == "" { return -1 } @@ -323,7 +330,7 @@ func testCoordinatorMessageSync(t *testing.T, successCase bool) { // nodeB doesn't sequence transactions, but adds messages related to them to its output feed. // nodeBOutputFeedReader reads those messages from this feed and processes them. // nodeBOutputFeedReader doesn't read messages from L1 since none of the nodes posts to L1. - nodeBPort := testClientB.ConsensusNode.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + nodeBPort := testhelpers.AddrTCPPort(testClientB.ConsensusNode.BroadcastServer.ListenerAddr(), t) nodeConfigNodeBOutputFeedReader := arbnode.ConfigDefaultL1NonSequencerTest() nodeConfigNodeBOutputFeedReader.Feed.Input = *newBroadcastClientConfigTest(nodeBPort) testClientNodeBOutputFeedReader, cleanupNodeBOutputFeedReader := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: nodeConfigNodeBOutputFeedReader}) @@ -373,3 +380,77 @@ func TestRedisSeqCoordinatorMessageSync(t *testing.T) { func TestRedisSeqCoordinatorWrongKeyMessageSync(t *testing.T) { testCoordinatorMessageSync(t, false) } + +func TestRedisSwitchover(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + builder.nodeConfig.SeqCoordinator.Enable = true + builder.nodeConfig.SeqCoordinator.RedisUrl = redisutil.CreateTestRedis(ctx, t) + builder.nodeConfig.SeqCoordinator.NewRedisUrl = redisutil.CreateTestRedis(ctx, t) + builder.nodeConfig.BatchPoster.Enable = false + + nodeNames := []string{"stdio://A", "stdio://B"} + initRedisForTest(t, ctx, builder.nodeConfig.SeqCoordinator.RedisUrl, nodeNames) + initRedisForTest(t, ctx, builder.nodeConfig.SeqCoordinator.NewRedisUrl, nodeNames) + builder.nodeConfig.SeqCoordinator.MyUrl = nodeNames[0] + + cleanup := builder.Build(t) + defer cleanup() + + redisClient, err := redisutil.RedisClientFromURL(builder.nodeConfig.SeqCoordinator.RedisUrl) + Require(t, err) + + // wait for sequencerA to become master + for { + err := redisClient.Get(ctx, redisutil.CHOSENSEQ_KEY).Err() + if errors.Is(err, redis.Nil) { + time.Sleep(builder.nodeConfig.SeqCoordinator.UpdateInterval) + continue + } + Require(t, err) + break + } + + builder.L2Info.GenerateAccount("User2") + + nodeConfigDup := *builder.nodeConfig + builder.nodeConfig = &nodeConfigDup + builder.nodeConfig.Feed.Output = *newBroadcasterConfigTest() + builder.nodeConfig.SeqCoordinator.MyUrl = nodeNames[1] + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: builder.nodeConfig}) + defer cleanupB() + + verifyTxIsProcessed(t, ctx, builder, testClientB, 1e12) + + redisClient.Set(ctx, redisutil.CHOSENSEQ_KEY, redisutil.SWITCHED_REDIS, time.Duration(-1)) + + verifyTxIsProcessed(t, ctx, builder, testClientB, 1e12*2) + + // Wait for all messages to be processed, before closing old redisClient + time.Sleep(1 * time.Second) + err = redisClient.Close() + Require(t, err) + + verifyTxIsProcessed(t, ctx, builder, testClientB, 1e12*3) + +} + +func verifyTxIsProcessed(t *testing.T, ctx context.Context, builder *NodeBuilder, testClientB *TestClient, balance int64) { + tx := builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, big.NewInt(1e12), nil) + + err := builder.L2.Client.SendTransaction(ctx, tx) + Require(t, err) + + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + _, err = WaitForTx(ctx, testClientB.Client, tx.Hash(), time.Second*5) + Require(t, err) + l2balance, err := testClientB.Client.BalanceAt(ctx, builder.L2Info.GetAddress("User2"), nil) + Require(t, err) + if l2balance.Cmp(big.NewInt(balance)) != 0 { + t.Fatal("Unexpected balance:", l2balance) + } +} diff --git a/system_tests/seq_nonce_test.go b/system_tests/seq_nonce_test.go index 72629e1978..7486b8a4ae 100644 --- a/system_tests/seq_nonce_test.go +++ b/system_tests/seq_nonce_test.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/offchainlabs/nitro/util/arbmath" ) @@ -111,6 +112,7 @@ func TestSequencerNonceTooHighQueueFull(t *testing.T) { } for wait := 9; wait >= 0; wait-- { + // #nosec G115 got := int(completed.Load()) expected := count - builder.execConfig.Sequencer.NonceFailureCacheSize if got == expected { diff --git a/system_tests/seq_pause_test.go b/system_tests/seq_pause_test.go index 6ce464d8da..c867a98271 100644 --- a/system_tests/seq_pause_test.go +++ b/system_tests/seq_pause_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/execution/gethexec" ) diff --git a/system_tests/seq_reject_test.go b/system_tests/seq_reject_test.go index 2dbdba6804..be230e26f5 100644 --- a/system_tests/seq_reject_test.go +++ b/system_tests/seq_reject_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "math/big" - "net" "strings" "sync" "sync/atomic" @@ -17,9 +16,11 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/colors" + "github.com/offchainlabs/nitro/util/testhelpers" ) func TestSequencerRejection(t *testing.T) { @@ -35,7 +36,7 @@ func TestSequencerRejection(t *testing.T) { builder := NewNodeBuilder(ctx).DefaultConfig(t, false) builder.takeOwnership = false - port := builderSeq.L2.ConsensusNode.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + port := testhelpers.AddrTCPPort(builderSeq.L2.ConsensusNode.BroadcastServer.ListenerAddr(), t) builder.nodeConfig.Feed.Input = *newBroadcastClientConfigTest(port) cleanup := builder.Build(t) defer cleanup() diff --git a/system_tests/seqcompensation_test.go b/system_tests/seqcompensation_test.go index 156ced6bfc..41133b8a42 100644 --- a/system_tests/seqcompensation_test.go +++ b/system_tests/seqcompensation_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/arbos/l1pricing" ) diff --git a/system_tests/seqfeed_test.go b/system_tests/seqfeed_test.go index 5e70fdf098..b757291561 100644 --- a/system_tests/seqfeed_test.go +++ b/system_tests/seqfeed_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "math/big" - "net" "reflect" "testing" "time" @@ -15,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/l1pricing" @@ -61,7 +61,7 @@ func TestSequencerFeed(t *testing.T) { defer cleanupSeq() seqInfo, seqNode, seqClient := builderSeq.L2Info, builderSeq.L2.ConsensusNode, builderSeq.L2.Client - port := seqNode.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + port := testhelpers.AddrTCPPort(seqNode.BroadcastServer.ListenerAddr(), t) builder := NewNodeBuilder(ctx).DefaultConfig(t, false) builder.nodeConfig.Feed.Input = *newBroadcastClientConfigTest(port) builder.takeOwnership = false @@ -107,7 +107,7 @@ func TestRelayedSequencerFeed(t *testing.T) { Require(t, err) config := relay.ConfigDefault - port := seqNode.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + port := testhelpers.AddrTCPPort(seqNode.BroadcastServer.ListenerAddr(), t) config.Node.Feed.Input = *newBroadcastClientConfigTest(port) config.Node.Feed.Output = *newBroadcasterConfigTest() config.Chain.ID = bigChainId.Uint64() @@ -119,7 +119,7 @@ func TestRelayedSequencerFeed(t *testing.T) { Require(t, err) defer currentRelay.StopAndWait() - port = currentRelay.GetListenerAddr().(*net.TCPAddr).Port + port = testhelpers.AddrTCPPort(currentRelay.GetListenerAddr(), t) builder := NewNodeBuilder(ctx).DefaultConfig(t, false) builder.nodeConfig.Feed.Input = *newBroadcastClientConfigTest(port) builder.takeOwnership = false @@ -164,12 +164,12 @@ func compareAllMsgResultsFromConsensusAndExecution( } var lastResult *execution.MessageResult - for msgCount := 1; arbutil.MessageIndex(msgCount) <= consensusMsgCount; msgCount++ { + for msgCount := arbutil.MessageIndex(1); msgCount <= consensusMsgCount; msgCount++ { pos := msgCount - 1 resultExec, err := testClient.ExecNode.ResultAtPos(arbutil.MessageIndex(pos)) Require(t, err) - resultConsensus, err := testClient.ConsensusNode.TxStreamer.ResultAtCount(arbutil.MessageIndex(msgCount)) + resultConsensus, err := testClient.ConsensusNode.TxStreamer.ResultAtCount(msgCount) Require(t, err) if !reflect.DeepEqual(resultExec, resultConsensus) { @@ -219,7 +219,7 @@ func testLyingSequencer(t *testing.T, dasModeStr string) { defer cleanupC() l2clientC, nodeC := testClientC.Client, testClientC.ConsensusNode - port := nodeC.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + port := testhelpers.AddrTCPPort(nodeC.BroadcastServer.ListenerAddr(), t) // The client node, connects to lying sequencer's feed nodeConfigB := arbnode.ConfigDefaultL1NonSequencerTest() @@ -361,7 +361,7 @@ func testBlockHashComparison(t *testing.T, blockHash *common.Hash, mustMismatch } defer wsBroadcastServer.StopAndWait() - port := wsBroadcastServer.ListenerAddr().(*net.TCPAddr).Port + port := testhelpers.AddrTCPPort(wsBroadcastServer.ListenerAddr(), t) builder := NewNodeBuilder(ctx).DefaultConfig(t, true) builder.nodeConfig.Feed.Input = *newBroadcastClientConfigTest(port) @@ -468,7 +468,7 @@ func TestPopulateFeedBacklog(t *testing.T) { // Creates a sink node that will read from the output feed of the previous node. nodeConfigSink := builder.nodeConfig - port := builder.L2.ConsensusNode.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + port := testhelpers.AddrTCPPort(builder.L2.ConsensusNode.BroadcastServer.ListenerAddr(), t) nodeConfigSink.Feed.Input = *newBroadcastClientConfigTest(port) testClientSink, cleanupSink := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: nodeConfigSink}) defer cleanupSink() diff --git a/system_tests/seqinbox_test.go b/system_tests/seqinbox_test.go index 4dc8f4a664..a9f66b0e2f 100644 --- a/system_tests/seqinbox_test.go +++ b/system_tests/seqinbox_test.go @@ -229,6 +229,7 @@ func testSequencerInboxReaderImpl(t *testing.T, validator bool) { reorgTargetNumber := blockStates[reorgTo].l1BlockNumber currentHeader, err := builder.L1.Client.HeaderByNumber(ctx, nil) Require(t, err) + // #nosec G115 if currentHeader.Number.Int64()-int64(reorgTargetNumber) < 65 { Fatal(t, "Less than 65 blocks of difference between current block", currentHeader.Number, "and target", reorgTargetNumber) } @@ -264,6 +265,7 @@ func testSequencerInboxReaderImpl(t *testing.T, validator bool) { for j := 0; j < numMessages; j++ { sourceNum := rand.Int() % len(state.accounts) source := state.accounts[sourceNum] + // #nosec G115 amount := new(big.Int).SetUint64(uint64(rand.Int()) % state.balances[source].Uint64()) reserveAmount := new(big.Int).SetUint64(l2pricing.InitialBaseFeeWei * 100000000) if state.balances[source].Cmp(new(big.Int).Add(amount, reserveAmount)) < 0 { @@ -313,6 +315,7 @@ func testSequencerInboxReaderImpl(t *testing.T, validator bool) { for j := 0; ; j++ { haveNonce, err := builder.L1.Client.PendingNonceAt(ctx, seqOpts.From) Require(t, err) + // #nosec G115 if haveNonce == uint64(seqNonce) { break } @@ -346,7 +349,7 @@ func testSequencerInboxReaderImpl(t *testing.T, validator bool) { BridgeAddr: builder.L1Info.GetAddress("Bridge"), DataPosterAddr: seqOpts.From, GasRefunderAddr: gasRefunderAddr, - SequencerInboxAccs: len(blockStates), + SequencerInboxAccs: uint64(len(blockStates)), AfterDelayedMessagesRead: 1, }) if diff := diffAccessList(accessed, *wantAL); diff != "" { @@ -374,10 +377,12 @@ func testSequencerInboxReaderImpl(t *testing.T, validator bool) { t.Fatalf("BalanceAt(%v) unexpected error: %v", seqOpts.From, err) } txCost := txRes.EffectiveGasPrice.Uint64() * txRes.GasUsed + // #nosec G115 if diff := before.Int64() - after.Int64(); diff >= int64(txCost) { t.Errorf("Transaction: %v was not refunded, balance diff: %v, cost: %v", tx.Hash(), diff, txCost) } + // #nosec G115 state.l2BlockNumber += uint64(numMessages) state.l1BlockNumber = txRes.BlockNumber.Uint64() blockStates = append(blockStates, state) @@ -424,11 +429,13 @@ func testSequencerInboxReaderImpl(t *testing.T, validator bool) { } for _, state := range blockStates { + // #nosec G115 block, err := l2Backend.APIBackend().BlockByNumber(ctx, rpc.BlockNumber(state.l2BlockNumber)) Require(t, err) if block == nil { Fatal(t, "missing state block", state.l2BlockNumber) } + // #nosec G115 stateDb, _, err := l2Backend.APIBackend().StateAndHeaderByNumber(ctx, rpc.BlockNumber(state.l2BlockNumber)) Require(t, err) for acct, expectedBalance := range state.balances { diff --git a/system_tests/snap_sync_test.go b/system_tests/snap_sync_test.go index a04d9f5bf3..7462b5f5f0 100644 --- a/system_tests/snap_sync_test.go +++ b/system_tests/snap_sync_test.go @@ -92,8 +92,10 @@ func TestSnapSync(t *testing.T) { waitForBlockToCatchupToMessageCount(ctx, t, nodeC.Client, finalMessageCount) // Fetching message count - 1 instead on the latest block number as the latest block number might not be // present in the snap sync node since it does not have the sequencer feed. + // #nosec G115 header, err := builder.L2.Client.HeaderByNumber(ctx, big.NewInt(int64(finalMessageCount)-1)) Require(t, err) + // #nosec G115 headerNodeC, err := nodeC.Client.HeaderByNumber(ctx, big.NewInt(int64(finalMessageCount)-1)) Require(t, err) // Once the node is synced up, check if the block hash is the same for the last block diff --git a/system_tests/staker_test.go b/system_tests/staker_test.go index 03c9fd3628..69645d8878 100644 --- a/system_tests/staker_test.go +++ b/system_tests/staker_test.go @@ -33,10 +33,12 @@ import ( "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" "github.com/offchainlabs/nitro/staker" + legacystaker "github.com/offchainlabs/nitro/staker/legacy" "github.com/offchainlabs/nitro/staker/validatorwallet" "github.com/offchainlabs/nitro/util" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/colors" + "github.com/offchainlabs/nitro/util/testhelpers" "github.com/offchainlabs/nitro/validator/valnode" ) @@ -57,7 +59,8 @@ func makeBackgroundTxs(ctx context.Context, builder *NodeBuilder) error { } func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) { - t.Parallel() + logHandler := testhelpers.InitTestLog(t, log.LvlTrace) + ctx, cancelCtx := context.WithCancel(context.Background()) defer cancelCtx() srv := externalsignertest.NewServer(t) @@ -132,15 +135,6 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) builder.L1.TransferBalance(t, "Faucet", "ValidatorB", balance, builder.L1Info) l1authB := builder.L1Info.GetDefaultTransactOpts("ValidatorB", ctx) - valWalletAddrAPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, &l1authA, l2nodeA.L1Reader, true) - Require(t, err) - valWalletAddrA := *valWalletAddrAPtr - valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, &l1authA, l2nodeA.L1Reader, true) - Require(t, err) - if valWalletAddrA == *valWalletAddrCheck { - Require(t, err, "didn't cache validator wallet address", valWalletAddrA.String(), "vs", valWalletAddrCheck.String()) - } - rollup, err := rollupgen.NewRollupAdminLogic(l2nodeA.DeployInfo.Rollup, builder.L1.Client) Require(t, err) @@ -149,16 +143,9 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) Require(t, err, "unable to parse rollup ABI") - setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddrA, l1authB.From, srv.Address}, []bool{true, true, true}) - Require(t, err, "unable to generate setValidator calldata") - tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setValidatorCalldata) - Require(t, err, "unable to set validators") - _, err = builder.L1.EnsureTxSucceeded(tx) - Require(t, err) - setMinAssertPeriodCalldata, err := rollupABI.Pack("setMinimumAssertionPeriod", big.NewInt(1)) Require(t, err, "unable to generate setMinimumAssertionPeriod calldata") - tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setMinAssertPeriodCalldata) + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setMinAssertPeriodCalldata) Require(t, err, "unable to set minimum assertion period") _, err = builder.L1.EnsureTxSucceeded(tx) Require(t, err) @@ -166,7 +153,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) validatorUtils, err := rollupgen.NewValidatorUtils(l2nodeA.DeployInfo.ValidatorUtils, builder.L1.Client) Require(t, err) - valConfigA := staker.TestL1ValidatorConfig + valConfigA := legacystaker.TestL1ValidatorConfig parentChainID, err := builder.L1.Client.ChainID(ctx) if err != nil { t.Fatalf("Failed to get parent chain id: %v", err) @@ -190,6 +177,22 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) valConfigA.Strategy = "MakeNodes" } + valWalletAddrAPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, l2nodeA.L1Reader, true, valWalletA.DataPoster(), valWalletA.GetExtraGas()) + Require(t, err) + valWalletAddrA := *valWalletAddrAPtr + valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, l2nodeA.L1Reader, true, valWalletA.DataPoster(), valWalletA.GetExtraGas()) + Require(t, err) + if valWalletAddrA == *valWalletAddrCheck { + Require(t, err, "didn't cache validator wallet address", valWalletAddrA.String(), "vs", valWalletAddrCheck.String()) + } + + setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddrA, l1authB.From, srv.Address}, []bool{true, true, true}) + Require(t, err, "unable to generate setValidator calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setValidatorCalldata) + Require(t, err, "unable to set validators") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) blockValidatorConfig := staker.TestBlockValidatorConfig @@ -206,11 +209,11 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) Require(t, err) err = statelessA.Start(ctx) Require(t, err) - stakerA, err := staker.NewStaker( + stakerA, err := legacystaker.NewStaker( l2nodeA.L1Reader, valWalletA, bind.CallOpts{}, - func() *staker.L1ValidatorConfig { return &valConfigA }, + func() *legacystaker.L1ValidatorConfig { return &valConfigA }, nil, statelessA, nil, @@ -220,7 +223,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) ) Require(t, err) err = stakerA.Initialize(ctx) - if stakerA.Strategy() != staker.WatchtowerStrategy { + if stakerA.Strategy() != legacystaker.WatchtowerStrategy { err = valWalletA.Initialize(ctx) Require(t, err) } @@ -244,7 +247,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) } valWalletB, err := validatorwallet.NewEOA(dpB, l2nodeB.DeployInfo.Rollup, l2nodeB.L1Reader.Client(), func() uint64 { return 0 }) Require(t, err) - valConfigB := staker.TestL1ValidatorConfig + valConfigB := legacystaker.TestL1ValidatorConfig valConfigB.Strategy = "MakeNodes" statelessB, err := staker.NewStatelessBlockValidator( l2nodeB.InboxReader, @@ -259,11 +262,11 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) Require(t, err) err = statelessB.Start(ctx) Require(t, err) - stakerB, err := staker.NewStaker( + stakerB, err := legacystaker.NewStaker( l2nodeB.L1Reader, valWalletB, bind.CallOpts{}, - func() *staker.L1ValidatorConfig { return &valConfigB }, + func() *legacystaker.L1ValidatorConfig { return &valConfigB }, nil, statelessB, nil, @@ -274,18 +277,18 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) Require(t, err) err = stakerB.Initialize(ctx) Require(t, err) - if stakerB.Strategy() != staker.WatchtowerStrategy { + if stakerB.Strategy() != legacystaker.WatchtowerStrategy { err = valWalletB.Initialize(ctx) Require(t, err) } valWalletC := validatorwallet.NewNoOp(builder.L1.Client, l2nodeA.DeployInfo.Rollup) - valConfigC := staker.TestL1ValidatorConfig + valConfigC := legacystaker.TestL1ValidatorConfig valConfigC.Strategy = "Watchtower" - stakerC, err := staker.NewStaker( + stakerC, err := legacystaker.NewStaker( l2nodeA.L1Reader, valWalletC, bind.CallOpts{}, - func() *staker.L1ValidatorConfig { return &valConfigC }, + func() *legacystaker.L1ValidatorConfig { return &valConfigC }, nil, statelessA, nil, @@ -294,7 +297,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) nil, ) Require(t, err) - if stakerC.Strategy() != staker.WatchtowerStrategy { + if stakerC.Strategy() != legacystaker.WatchtowerStrategy { err = valWalletC.Initialize(ctx) Require(t, err) } @@ -407,7 +410,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) if faultyStaker { conflictInfo, err := validatorUtils.FindStakerConflict(&bind.CallOpts{}, l2nodeA.DeployInfo.Rollup, l1authA.From, srv.Address, big.NewInt(1024)) Require(t, err) - if staker.ConflictType(conflictInfo.Ty) == staker.CONFLICT_TYPE_FOUND { + if legacystaker.ConflictType(conflictInfo.Ty) == legacystaker.CONFLICT_TYPE_FOUND { cancelBackgroundTxs() } } @@ -464,8 +467,53 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) if !stakerBWasStaked { Fatal(t, "staker B was never staked") } + + if logHandler.WasLogged("data poster expected next transaction to have nonce \\d+ but was requested to post transaction with nonce \\d+") { + Fatal(t, "Staker's DataPoster inferred nonce incorrectly") + } } func TestStakersCooperative(t *testing.T) { stakerTestImpl(t, false, false) } + +func TestGetValidatorWalletContractWithDataposterOnlyUsedToCreateValidatorWalletContract(t *testing.T) { + t.Parallel() + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanup := builder.Build(t) + defer cleanup() + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + builder.L1Info.GenerateAccount("ValidatorA") + builder.L1.TransferBalance(t, "Faucet", "ValidatorA", balance, builder.L1Info) + l1auth := builder.L1Info.GetDefaultTransactOpts("ValidatorA", ctx) + + parentChainID, err := builder.L1.Client.ChainID(ctx) + Require(t, err) + + dataPoster, err := arbnode.DataposterOnlyUsedToCreateValidatorWalletContract( + ctx, + builder.L2.ConsensusNode.L1Reader, + &l1auth, + &builder.nodeConfig.Staker.DataPoster, + parentChainID, + ) + if err != nil { + log.Crit("error creating data poster to create validator wallet contract", "err", err) + } + getExtraGas := func() uint64 { return builder.nodeConfig.Staker.ExtraGas } + + valWalletAddrAPtr, err := validatorwallet.GetValidatorWalletContract(ctx, builder.L2.ConsensusNode.DeployInfo.ValidatorWalletCreator, 0, builder.L2.ConsensusNode.L1Reader, true, dataPoster, getExtraGas) + Require(t, err) + valWalletAddrA := *valWalletAddrAPtr + valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, builder.L2.ConsensusNode.DeployInfo.ValidatorWalletCreator, 0, builder.L2.ConsensusNode.L1Reader, true, dataPoster, getExtraGas) + Require(t, err) + if valWalletAddrA == *valWalletAddrCheck { + Require(t, err, "didn't cache validator wallet address", valWalletAddrA.String(), "vs", valWalletAddrCheck.String()) + } +} diff --git a/system_tests/state_fuzz_test.go b/system_tests/state_fuzz_test.go index 24140e480d..8388e8417c 100644 --- a/system_tests/state_fuzz_test.go +++ b/system_tests/state_fuzz_test.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" @@ -27,6 +28,7 @@ import ( "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbstate" "github.com/offchainlabs/nitro/arbstate/daprovider" + "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/statetransfer" "github.com/offchainlabs/nitro/util/testhelpers/env" ) @@ -38,6 +40,7 @@ func BuildBlock( chainConfig *params.ChainConfig, inbox arbstate.InboxBackend, seqBatch []byte, + runMode core.MessageRunMode, ) (*types.Block, error) { var delayedMessagesRead uint64 if lastBlockHeader != nil { @@ -59,11 +62,13 @@ func BuildBlock( } err = l1Message.FillInBatchGasCost(batchFetcher) if err != nil { - return nil, err + // skip malformed batch posting report + // nolint:nilerr + return nil, nil } block, _, err := arbos.ProduceBlock( - l1Message, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false, + l1Message, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, false, runMode, ) return block, err } @@ -127,12 +132,12 @@ func (c noopChainContext) GetHeader(common.Hash, uint64) *types.Header { } func FuzzStateTransition(f *testing.F) { - f.Fuzz(func(t *testing.T, compressSeqMsg bool, seqMsg []byte, delayedMsg []byte) { + f.Fuzz(func(t *testing.T, compressSeqMsg bool, seqMsg []byte, delayedMsg []byte, runModeSeed uint8) { if len(seqMsg) > 0 && daprovider.IsL1AuthenticatedMessageHeaderByte(seqMsg[0]) { return } chainDb := rawdb.NewMemoryDatabase() - chainConfig := params.ArbitrumRollupGoerliTestnetChainConfig() + chainConfig := chaininfo.ArbitrumRollupGoerliTestnetChainConfig() serializedChainConfig, err := json.Marshal(chainConfig) if err != nil { panic(err) @@ -201,7 +206,9 @@ func FuzzStateTransition(f *testing.F) { positionWithinMessage: 0, delayedMessages: delayedMessages, } - _, err = BuildBlock(statedb, genesis, noopChainContext{}, params.ArbitrumOneChainConfig(), inbox, seqBatch) + numberOfMessageRunModes := uint8(core.MessageReplayMode) + 1 // TODO update number of run modes when new mode is added + runMode := core.MessageRunMode(runModeSeed % numberOfMessageRunModes) + _, err = BuildBlock(statedb, genesis, noopChainContext{}, chaininfo.ArbitrumOneChainConfig(), inbox, seqBatch, runMode) if err != nil { // With the fixed header it shouldn't be possible to read a delayed message, // and no other type of error should be possible. diff --git a/system_tests/staterecovery_test.go b/system_tests/staterecovery_test.go index 42faee7e0d..d5ed3fcd33 100644 --- a/system_tests/staterecovery_test.go +++ b/system_tests/staterecovery_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/trie" + "github.com/offchainlabs/nitro/cmd/conf" "github.com/offchainlabs/nitro/cmd/staterecovery" "github.com/offchainlabs/nitro/execution/gethexec" diff --git a/system_tests/stylus_trace_test.go b/system_tests/stylus_trace_test.go index cb303874d6..fe5de21dad 100644 --- a/system_tests/stylus_trace_test.go +++ b/system_tests/stylus_trace_test.go @@ -6,15 +6,18 @@ package arbtest import ( "bytes" "encoding/binary" + "math" "math/big" "testing" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/tracers/logger" - "github.com/holiman/uint256" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" @@ -76,6 +79,7 @@ func sendAndTraceTransaction( } func intToBytes(v int) []byte { + // #nosec G115 return binary.BigEndian.AppendUint64(nil, uint64(v)) } @@ -477,3 +481,17 @@ func TestStylusOpcodeTraceEquivalence(t *testing.T) { checkOpcode(t, wasmResult, 12, vm.RETURN, offset, returnLen) checkOpcode(t, evmResult, 5078, vm.RETURN, offset, returnLen) } + +func TestStylusHugeWriteResultTrace(t *testing.T) { + const jit = false + builder, auth, cleanup := setupProgramTest(t, jit) + ctx := builder.ctx + l2client := builder.L2.Client + defer cleanup() + + program := deployWasm(t, ctx, auth, l2client, watFile("write-result-len")) + const returnLen = math.MaxUint16 + 1 + args := binary.LittleEndian.AppendUint32(nil, returnLen) + result := sendAndTraceTransaction(t, builder, program, nil, args) + checkOpcode(t, result, 3, vm.RETURN, nil, intToBe32(returnLen)) +} diff --git a/system_tests/stylus_tracer_test.go b/system_tests/stylus_tracer_test.go new file mode 100644 index 0000000000..b833d7df17 --- /dev/null +++ b/system_tests/stylus_tracer_test.go @@ -0,0 +1,246 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package arbtest + +import ( + "encoding/binary" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" + "github.com/offchainlabs/nitro/util/containers" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func TestStylusTracer(t *testing.T) { + const jit = false + builder, auth, cleanup := setupProgramTest(t, jit) + ctx := builder.ctx + l2client := builder.L2.Client + l2info := builder.L2Info + rpcClient := builder.L2.Client.Client() + defer cleanup() + + traceTransaction := func(tx common.Hash, tracer string) []gethexec.HostioTraceInfo { + traceOpts := struct { + Tracer string `json:"tracer"` + }{ + Tracer: tracer, + } + var result []gethexec.HostioTraceInfo + err := rpcClient.CallContext(ctx, &result, "debug_traceTransaction", tx, traceOpts) + Require(t, err, "trace transaction") + return result + } + + // Deploy contracts + stylusMulticall := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) + evmMulticall, tx, _, err := mocksgen.DeployMultiCallTest(&auth, builder.L2.Client) + Require(t, err, "deploy evm multicall") + _, err = EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err, "ensure evm multicall deployment") + + // Args for tests + key := testhelpers.RandomHash() + value := testhelpers.RandomHash() + loadStoreArgs := multicallEmptyArgs() + loadStoreArgs = multicallAppendStore(loadStoreArgs, key, value, false) + loadStoreArgs = multicallAppendLoad(loadStoreArgs, key, false) + callArgs := argsForMulticall(vm.CALL, stylusMulticall, nil, []byte{0}) + evmCall := argsForMulticall(vm.CALL, evmMulticall, nil, []byte{0}) + + for _, testCase := range []struct { + name string + contract common.Address + args []byte + want []gethexec.HostioTraceInfo + }{ + { + name: "non-recursive hostios", + contract: stylusMulticall, + args: loadStoreArgs, + want: []gethexec.HostioTraceInfo{ + {Name: "user_entrypoint", Args: intToBe32(len(loadStoreArgs))}, + {Name: "pay_for_memory_grow", Args: []byte{0x00, 0x01}}, + {Name: "read_args", Outs: loadStoreArgs}, + {Name: "storage_cache_bytes32", Args: append(key.Bytes(), value.Bytes()...)}, + {Name: "storage_flush_cache", Args: []byte{0x00}}, + {Name: "storage_load_bytes32", Args: key.Bytes(), Outs: value.Bytes()}, + {Name: "storage_flush_cache", Args: []byte{0x00}}, + {Name: "write_result", Args: value.Bytes()}, + {Name: "user_returned", Outs: intToBe32(0)}, + }, + }, + + { + name: "call stylus contract", + contract: stylusMulticall, + args: callArgs, + want: []gethexec.HostioTraceInfo{ + {Name: "user_entrypoint", Args: intToBe32(len(callArgs))}, + {Name: "pay_for_memory_grow", Args: []byte{0x00, 0x01}}, + {Name: "read_args", Outs: callArgs}, + { + Name: "call_contract", + Args: append(stylusMulticall.Bytes(), common.Hex2Bytes("ffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000")...), + Outs: common.Hex2Bytes("0000000000"), + Address: &stylusMulticall, + Steps: (*containers.Stack[gethexec.HostioTraceInfo])(&[]gethexec.HostioTraceInfo{ + {Name: "user_entrypoint", Args: intToBe32(1)}, + {Name: "pay_for_memory_grow", Args: []byte{0x00, 0x01}}, + {Name: "read_args", Outs: []byte{0x00}}, + {Name: "storage_flush_cache", Args: []byte{0x00}}, + {Name: "write_result"}, + {Name: "user_returned", Outs: intToBe32(0)}, + }), + }, + {Name: "storage_flush_cache", Args: []byte{0x00}}, + {Name: "write_result"}, + {Name: "user_returned", Outs: intToBe32(0)}, + }, + }, + + { + name: "call evm contract", + contract: stylusMulticall, + args: evmCall, + want: []gethexec.HostioTraceInfo{ + {Name: "user_entrypoint", Args: intToBe32(len(callArgs))}, + {Name: "pay_for_memory_grow", Args: []byte{0x00, 0x01}}, + {Name: "read_args", Outs: evmCall}, + { + Name: "call_contract", + Args: append(evmMulticall.Bytes(), common.Hex2Bytes("ffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000")...), + Outs: common.Hex2Bytes("0000000000"), + Address: &evmMulticall, + Steps: containers.NewStack[gethexec.HostioTraceInfo](), + }, + {Name: "storage_flush_cache", Args: []byte{0x00}}, + {Name: "write_result"}, + {Name: "user_returned", Outs: intToBe32(0)}, + }, + }, + + { + name: "evm contract calling wasm", + contract: evmMulticall, + args: callArgs, + want: []gethexec.HostioTraceInfo{ + { + Name: "evm_call_contract", + Address: &stylusMulticall, + Steps: (*containers.Stack[gethexec.HostioTraceInfo])(&[]gethexec.HostioTraceInfo{ + {Name: "user_entrypoint", Args: intToBe32(1)}, + {Name: "pay_for_memory_grow", Args: []byte{0x00, 0x01}}, + {Name: "read_args", Outs: []byte{0x00}}, + {Name: "storage_flush_cache", Args: []byte{0x00}}, + {Name: "write_result"}, + {Name: "user_returned", Outs: intToBe32(0)}, + }), + }, + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + to := testCase.contract + tx := l2info.PrepareTxTo("Owner", &to, l2info.TransferGas, nil, testCase.args) + err := l2client.SendTransaction(ctx, tx) + Require(t, err, "send transaction") + + nativeResult := traceTransaction(tx.Hash(), "stylusTracer") + normalizeHostioTrace(nativeResult) + if diff := cmp.Diff(testCase.want, nativeResult); diff != "" { + Fatal(t, "native tracer don't match wanted result", diff) + } + + jsResult := traceTransaction(tx.Hash(), jsStylusTracer) + normalizeHostioTrace(jsResult) + if diff := cmp.Diff(jsResult, nativeResult); diff != "" { + Fatal(t, "native tracer don't match js trace", diff) + } + }) + } +} + +func intToBe32(v int) []byte { + // #nosec G115 + return binary.BigEndian.AppendUint32(nil, uint32(v)) +} + +// normalize removes the start and end ink values from the trace so we can compare them. +// In Arbitrum, the gas used by the transaction varies depending on the L1 fees, so the trace +// returns different gas values and we can't hardcode them. +func normalizeHostioTrace(trace []gethexec.HostioTraceInfo) { + for i := range trace { + trace[i].StartInk = 0 + trace[i].EndInk = 0 + if len(trace[i].Args) == 0 { + trace[i].Args = nil + } + if len(trace[i].Outs) == 0 { + trace[i].Outs = nil + } + if trace[i].Steps != nil { + normalizeHostioTrace(*trace[i].Steps) + } + } +} + +var jsStylusTracer = ` +{ + "hostio": function(info) { + info.args = toHex(info.args); + info.outs = toHex(info.outs); + if (this.nests.includes(info.name)) { + Object.assign(info, this.open.pop()); + info.name = info.name.substring(4) // remove evm_ + } + this.open.push(info); + }, + "enter": function(frame) { + let inner = []; + let name = ""; + switch (frame.getType()) { + case "CALL": + name = "evm_call_contract"; + break; + case "DELEGATECALL": + name = "evm_delegate_call_contract"; + break; + case "STATICCALL": + name = "evm_static_call_contract"; + break; + case "CREATE": + name = "evm_create1"; + break; + case "CREATE2": + name = "evm_create2"; + break; + case "SELFDESTRUCT": + name = "evm_self_destruct"; + break; + } + this.open.push({ + address: toHex(frame.getTo()), + steps: inner, + name: name, + }); + this.stack.push(this.open); // save where we were + this.open = inner; + }, + "exit": function(result) { + this.open = this.stack.pop(); + }, + "result": function() { return this.open; }, + "fault": function() { return this.open; }, + stack: [], + open: [], + nests: ["call_contract", "delegate_call_contract", "static_call_contract"] +} +` diff --git a/system_tests/test_info.go b/system_tests/test_info.go index 6313e392ca..105d491006 100644 --- a/system_tests/test_info.go +++ b/system_tests/test_info.go @@ -11,16 +11,16 @@ import ( "sync/atomic" "testing" - "github.com/offchainlabs/nitro/arbos/l2pricing" - "github.com/offchainlabs/nitro/util" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/statetransfer" + "github.com/offchainlabs/nitro/util" ) var simulatedChainID = big.NewInt(1337) diff --git a/system_tests/timeboost_test.go b/system_tests/timeboost_test.go index 1e7ca47db4..56b9623e5d 100644 --- a/system_tests/timeboost_test.go +++ b/system_tests/timeboost_test.go @@ -15,6 +15,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -26,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" @@ -42,7 +45,6 @@ import ( "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/stretchr/testify/require" ) func TestTimeboostBulkBlockMetadataAPI(t *testing.T) { @@ -159,10 +161,8 @@ func TestTimeboostBulkBlockMetadataAPI(t *testing.T) { } } -func TestSequencerFeed_ExpressLaneAuction_ExpressLaneTxsHaveAdvantage_TimeboostedFieldIsCorrect(t *testing.T) { +func TestExpressLaneControlTransfer(t *testing.T) { t.Parallel() - - // logHandler := testhelpers.InitTestLog(t, log.LevelInfo) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -173,138 +173,144 @@ func TestSequencerFeed_ExpressLaneAuction_ExpressLaneTxsHaveAdvantage_Timebooste }) jwtSecretPath := filepath.Join(tmpDir, "sequencer.jwt") - seq, seqClient, seqInfo, auctionContractAddr, cleanupSeq, feedListener, cleanupFeedListener := setupExpressLaneAuction(t, tmpDir, ctx, jwtSecretPath) + seq, seqClient, seqInfo, auctionContractAddr, aliceBidderClient, bobBidderClient, roundDuration, cleanupSeq, _, cleanupFeedListener := setupExpressLaneAuction(t, tmpDir, ctx, jwtSecretPath) defer cleanupSeq() defer cleanupFeedListener() - chainId, err := seqClient.ChainID(ctx) - Require(t, err) - auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient) Require(t, err) - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) Require(t, err) - bobPriv := seqInfo.Accounts["Bob"].PrivateKey - // Prepare a client that can submit txs to the sequencer via the express lane. + // Prepare clients that can submit txs to the sequencer via the express lane. + chainId, err := seqClient.ChainID(ctx) + Require(t, err) seqDial, err := rpc.Dial(seq.Stack.HTTPEndpoint()) Require(t, err) - expressLaneClient := newExpressLaneClient( - bobPriv, - chainId, - time.Unix(int64(info.OffsetTimestamp), 0), - time.Duration(info.RoundDurationSeconds)*time.Second, - auctionContractAddr, - seqDial, - ) - expressLaneClient.Start(ctx) - - // During the express lane around, Bob sends txs always 150ms later than Alice, but Alice's - // txs end up getting delayed by 200ms as she is not the express lane controller. - // In the end, Bob's txs should be ordered before Alice's during the round. - var wg sync.WaitGroup - wg.Add(2) - ownerAddr := seqInfo.GetAddress("Owner") - aliceData := &types.DynamicFeeTx{ - To: &ownerAddr, - Gas: seqInfo.TransferGas, - GasFeeCap: new(big.Int).Set(seqInfo.GasPrice), - Value: big.NewInt(1e12), - Nonce: 3, - Data: nil, + createExpressLaneClientFor := func(name string) (*expressLaneClient, bind.TransactOpts) { + priv := seqInfo.Accounts[name].PrivateKey + expressLaneClient := newExpressLaneClient( + priv, + chainId, + *roundTimingInfo, + auctionContractAddr, + seqDial, + ) + expressLaneClient.Start(ctx) + transacOpts := seqInfo.GetDefaultTransactOpts(name, ctx) + transacOpts.NoSend = true + return expressLaneClient, transacOpts } - aliceTx := seqInfo.SignTxAs("Alice", aliceData) - go func(w *sync.WaitGroup) { - defer w.Done() - err = seqClient.SendTransaction(ctx, aliceTx) - Require(t, err) - }(&wg) + bobExpressLaneClient, bobOpts := createExpressLaneClientFor("Bob") + aliceExpressLaneClient, aliceOpts := createExpressLaneClientFor("Alice") - bobData := &types.DynamicFeeTx{ - To: &ownerAddr, - Gas: seqInfo.TransferGas, - GasFeeCap: new(big.Int).Set(seqInfo.GasPrice), - Value: big.NewInt(1e12), - Nonce: 3, - Data: nil, - } - bobBoostableTx := seqInfo.SignTxAs("Bob", bobData) - go func(w *sync.WaitGroup) { - defer w.Done() - time.Sleep(time.Millisecond * 10) - err = expressLaneClient.SendTransaction(ctx, bobBoostableTx) - Require(t, err) - }(&wg) - wg.Wait() + // Bob will win the auction and become controller for next round + placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Bob", "Alice", bobBidderClient, aliceBidderClient, roundDuration) + time.Sleep(roundTimingInfo.TimeTilNextRound()) - // After round is done, verify that Bob beats Alice in the final sequence. - aliceReceipt, err := seqClient.TransactionReceipt(ctx, aliceTx.Hash()) + // Check that Bob's tx gets priority since he's the controller + verifyControllerAdvantage(t, ctx, seqClient, bobExpressLaneClient, seqInfo, "Bob", "Alice") + + // Transfer express lane control from Bob to Alice + currRound := roundTimingInfo.RoundNumber() + duringRoundTransferTx, err := auctionContract.ExpressLaneAuctionTransactor.TransferExpressLaneController(&bobOpts, currRound, seqInfo.Accounts["Alice"].Address) Require(t, err) - aliceBlock := aliceReceipt.BlockNumber.Uint64() - bobReceipt, err := seqClient.TransactionReceipt(ctx, bobBoostableTx.Hash()) + err = bobExpressLaneClient.SendTransaction(ctx, duringRoundTransferTx) Require(t, err) - bobBlock := bobReceipt.BlockNumber.Uint64() - if aliceBlock < bobBlock { - t.Fatal("Alice's tx should not have been sequenced before Bob's in different blocks") - } else if aliceBlock == bobBlock { - if aliceReceipt.TransactionIndex < bobReceipt.TransactionIndex { - t.Fatal("Bob should have been sequenced before Alice with express lane") - } + time.Sleep(time.Second) // Wait for controller to change on the sequencer side + // Check that now Alice's tx gets priority since she's the controller after bob transfered it + verifyControllerAdvantage(t, ctx, seqClient, aliceExpressLaneClient, seqInfo, "Alice", "Bob") + + // Alice and Bob submit bids and Alice wins for the next round + placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Alice", "Bob", aliceBidderClient, bobBidderClient, roundDuration) + t.Log("Alice won the express lane auction for upcoming round, now try to transfer control before the next round begins...") + + // Alice now transfers control to bob before her round begins + winnerRound := currRound + 1 + currRound = roundTimingInfo.RoundNumber() + if currRound >= winnerRound { + t.Fatalf("next round already began, try running the test again. Current round: %d, Winner Round: %d", currRound, winnerRound) } - // verifyTimeboostedCorrectness is used to check if the timeboosted byte array in both the sequencer's tx streamer and the client node's tx streamer (which is connected - // to the sequencer feed) is accurate, i.e it represents correctly whether a tx is timeboosted or not - verifyTimeboostedCorrectness := func(user string, tNode *arbnode.Node, tClient *ethclient.Client, isTimeboosted bool, userTx *types.Transaction, userTxBlockNum uint64) { - blockMetadataOfBlock, err := tNode.TxStreamer.BlockMetadataAtCount(arbutil.MessageIndex(userTxBlockNum) + 1) - Require(t, err) - if len(blockMetadataOfBlock) == 0 { - t.Fatal("got empty blockMetadata byte array") - } - if blockMetadataOfBlock[0] != message.TimeboostedVersion { - t.Fatalf("blockMetadata byte array has invalid version. Want: %d, Got: %d", message.TimeboostedVersion, blockMetadataOfBlock[0]) + beforeRoundTransferTx, err := auctionContract.ExpressLaneAuctionTransactor.TransferExpressLaneController(&aliceOpts, winnerRound, seqInfo.Accounts["Bob"].Address) + Require(t, err) + err = aliceExpressLaneClient.SendTransaction(ctx, beforeRoundTransferTx) + Require(t, err) + + setExpressLaneIterator, err := auctionContract.FilterSetExpressLaneController(&bind.FilterOpts{Context: ctx}, nil, nil, nil) + Require(t, err) + verifyControllerChange := func(round uint64, prev, new common.Address) { + setExpressLaneIterator.Next() + if setExpressLaneIterator.Event.Round != round { + t.Fatalf("unexpected round number. Want: %d, Got: %d", round, setExpressLaneIterator.Event.Round) } - userTxBlock, err := tClient.BlockByNumber(ctx, new(big.Int).SetUint64(userTxBlockNum)) - Require(t, err) - var foundUserTx bool - for txIndex, tx := range userTxBlock.Transactions() { - if tx.Hash() == userTx.Hash() { - foundUserTx = true - if !isTimeboosted && blockMetadataOfBlock.IsTxTimeboosted(txIndex) { - t.Fatalf("incorrect timeboosted bit for %s's tx, it shouldn't be timeboosted", user) - } else if isTimeboosted && !blockMetadataOfBlock.IsTxTimeboosted(txIndex) { - t.Fatalf("incorrect timeboosted bit for %s's tx, it should be timeboosted", user) - } - } else if blockMetadataOfBlock.IsTxTimeboosted(txIndex) { - // Other tx's right now shouln't be timeboosted - t.Fatalf("incorrect timeboosted bit for nonspecified tx with index: %d, it shouldn't be timeboosted", txIndex) - } + if setExpressLaneIterator.Event.PreviousExpressLaneController != prev { + t.Fatalf("unexpected previous express lane controller. Want: %v, Got: %v", prev, setExpressLaneIterator.Event.PreviousExpressLaneController) } - if !foundUserTx { - t.Fatalf("%s's tx wasn't found in the block with blockNum retrieved from its receipt", user) + if setExpressLaneIterator.Event.NewExpressLaneController != new { + t.Fatalf("unexpected new express lane controller. Want: %v, Got: %v", new, setExpressLaneIterator.Event.NewExpressLaneController) } } + // Verify during round control change + verifyControllerChange(currRound, common.Address{}, bobOpts.From) // Bob wins auction + verifyControllerChange(currRound, bobOpts.From, aliceOpts.From) // Bob transfers control to Alice + // Verify before round control change + verifyControllerChange(winnerRound, common.Address{}, aliceOpts.From) // Alice wins auction + verifyControllerChange(winnerRound, aliceOpts.From, bobOpts.From) // Alice transfers control to Bob before the round begins +} - // First test that timeboosted byte array is correct on sequencer side - verifyTimeboostedCorrectness("alice", seq, seqClient, false, aliceTx, aliceBlock) - verifyTimeboostedCorrectness("bob", seq, seqClient, true, bobBoostableTx, bobBlock) +func TestSequencerFeed_ExpressLaneAuction_ExpressLaneTxsHaveAdvantage(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Verify that timeboosted byte array receieved via sequencer feed is correct - _, err = WaitForTx(ctx, feedListener.Client, bobBoostableTx.Hash(), time.Second*5) + tmpDir, err := os.MkdirTemp("", "*") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(tmpDir)) + }) + jwtSecretPath := filepath.Join(tmpDir, "sequencer.jwt") + + seq, seqClient, seqInfo, auctionContractAddr, aliceBidderClient, bobBidderClient, roundDuration, cleanupSeq, _, cleanupFeedListener := setupExpressLaneAuction(t, tmpDir, ctx, jwtSecretPath) + defer cleanupSeq() + defer cleanupFeedListener() + + auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient) Require(t, err) - _, err = WaitForTx(ctx, feedListener.Client, aliceTx.Hash(), time.Second*5) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) Require(t, err) - verifyTimeboostedCorrectness("alice", feedListener.ConsensusNode, feedListener.Client, false, aliceTx, aliceBlock) - verifyTimeboostedCorrectness("bob", feedListener.ConsensusNode, feedListener.Client, true, bobBoostableTx, bobBlock) - // arbnode.BlockHashMismatchLogMsg has been randomly appearing and disappearing when running this test, not sure why that might be happening - // if logHandler.WasLogged(arbnode.BlockHashMismatchLogMsg) { - // t.Fatal("BlockHashMismatchLogMsg was logged unexpectedly") - // } + placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Bob", "Alice", bobBidderClient, aliceBidderClient, roundDuration) + time.Sleep(roundTimingInfo.TimeTilNextRound()) + + chainId, err := seqClient.ChainID(ctx) + Require(t, err) + + // Prepare a client that can submit txs to the sequencer via the express lane. + bobPriv := seqInfo.Accounts["Bob"].PrivateKey + seqDial, err := rpc.Dial(seq.Stack.HTTPEndpoint()) + Require(t, err) + expressLaneClient := newExpressLaneClient( + bobPriv, + chainId, + *roundTimingInfo, + auctionContractAddr, + seqDial, + ) + expressLaneClient.Start(ctx) + + verifyControllerAdvantage(t, ctx, seqClient, expressLaneClient, seqInfo, "Bob", "Alice") } -func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *testing.T) { +func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected_TimeboostedFieldIsCorrect(t *testing.T) { t.Parallel() + + // logHandler := testhelpers.InitTestLog(t, log.LevelInfo) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -314,25 +320,32 @@ func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *test require.NoError(t, os.RemoveAll(tmpDir)) }) jwtSecretPath := filepath.Join(tmpDir, "sequencer.jwt") - seq, seqClient, seqInfo, auctionContractAddr, cleanupSeq, _, _ := setupExpressLaneAuction(t, tmpDir, ctx, jwtSecretPath) + seq, seqClient, seqInfo, auctionContractAddr, aliceBidderClient, bobBidderClient, roundDuration, cleanupSeq, feedListener, cleanupFeedListener := setupExpressLaneAuction(t, tmpDir, ctx, jwtSecretPath) defer cleanupSeq() - chainId, err := seqClient.ChainID(ctx) - Require(t, err) + defer cleanupFeedListener() auctionContract, err := express_lane_auctiongen.NewExpressLaneAuction(auctionContractAddr, seqClient) Require(t, err) - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) + Require(t, err) + Require(t, err) - bobPriv := seqInfo.Accounts["Bob"].PrivateKey + + placeBidsAndDecideWinner(t, ctx, seqClient, seqInfo, auctionContract, "Bob", "Alice", bobBidderClient, aliceBidderClient, roundDuration) + time.Sleep(roundTimingInfo.TimeTilNextRound()) // Prepare a client that can submit txs to the sequencer via the express lane. + bobPriv := seqInfo.Accounts["Bob"].PrivateKey + chainId, err := seqClient.ChainID(ctx) + Require(t, err) seqDial, err := rpc.Dial(seq.Stack.HTTPEndpoint()) Require(t, err) expressLaneClient := newExpressLaneClient( bobPriv, chainId, - time.Unix(int64(info.OffsetTimestamp), 0), - time.Duration(info.RoundDurationSeconds)*time.Second, + *roundTimingInfo, auctionContractAddr, seqDial, ) @@ -348,12 +361,14 @@ func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *test var wg sync.WaitGroup wg.Add(2) ownerAddr := seqInfo.GetAddress("Owner") + aliceNonce, err := seqClient.PendingNonceAt(ctx, seqInfo.GetAddress("Alice")) + Require(t, err) aliceData := &types.DynamicFeeTx{ To: &ownerAddr, Gas: seqInfo.TransferGas, GasFeeCap: new(big.Int).Set(seqInfo.GasPrice), Value: big.NewInt(1e12), - Nonce: 3, + Nonce: aliceNonce, Data: nil, } aliceTx := seqInfo.SignTxAs("Alice", aliceData) @@ -410,6 +425,161 @@ func TestSequencerFeed_ExpressLaneAuction_InnerPayloadNoncesAreRespected(t *test t.Fatal("Charlie should have been sequenced before Alice with express lane") } } + + // First test that timeboosted byte array is correct on sequencer side + verifyTimeboostedCorrectness(t, ctx, "Alice", seq, seqClient, false, aliceTx, aliceBlock) + verifyTimeboostedCorrectness(t, ctx, "Charlie", seq, seqClient, true, charlie0, charlieBlock) + + // Verify that timeboosted byte array receieved via sequencer feed is correct + _, err = WaitForTx(ctx, feedListener.Client, charlie0.Hash(), time.Second*5) + Require(t, err) + _, err = WaitForTx(ctx, feedListener.Client, aliceTx.Hash(), time.Second*5) + Require(t, err) + verifyTimeboostedCorrectness(t, ctx, "Alice", feedListener.ConsensusNode, feedListener.Client, false, aliceTx, aliceBlock) + verifyTimeboostedCorrectness(t, ctx, "Charlie", feedListener.ConsensusNode, feedListener.Client, true, charlie0, charlieBlock) + + // arbnode.BlockHashMismatchLogMsg has been randomly appearing and disappearing when running this test, not sure why that might be happening + // if logHandler.WasLogged(arbnode.BlockHashMismatchLogMsg) { + // t.Fatal("BlockHashMismatchLogMsg was logged unexpectedly") + // } +} + +// verifyTimeboostedCorrectness is used to check if the timeboosted byte array in both the sequencer's tx streamer and the client node's tx streamer (which is connected +// to the sequencer feed) is accurate, i.e it represents correctly whether a tx is timeboosted or not +func verifyTimeboostedCorrectness(t *testing.T, ctx context.Context, user string, tNode *arbnode.Node, tClient *ethclient.Client, isTimeboosted bool, userTx *types.Transaction, userTxBlockNum uint64) { + blockMetadataOfBlock, err := tNode.TxStreamer.BlockMetadataAtCount(arbutil.MessageIndex(userTxBlockNum) + 1) + Require(t, err) + if len(blockMetadataOfBlock) == 0 { + t.Fatal("got empty blockMetadata byte array") + } + if blockMetadataOfBlock[0] != message.TimeboostedVersion { + t.Fatalf("blockMetadata byte array has invalid version. Want: %d, Got: %d", message.TimeboostedVersion, blockMetadataOfBlock[0]) + } + userTxBlock, err := tClient.BlockByNumber(ctx, new(big.Int).SetUint64(userTxBlockNum)) + Require(t, err) + var foundUserTx bool + for txIndex, tx := range userTxBlock.Transactions() { + got, err := blockMetadataOfBlock.IsTxTimeboosted(txIndex) + Require(t, err) + if tx.Hash() == userTx.Hash() { + foundUserTx = true + if !isTimeboosted && got { + t.Fatalf("incorrect timeboosted bit for %s's tx, it shouldn't be timeboosted", user) + } else if isTimeboosted && !got { + t.Fatalf("incorrect timeboosted bit for %s's tx, it should be timeboosted", user) + } + } else if got { + // Other tx's right now shouln't be timeboosted + t.Fatalf("incorrect timeboosted bit for nonspecified tx with index: %d, it shouldn't be timeboosted", txIndex) + } + } + if !foundUserTx { + t.Fatalf("%s's tx wasn't found in the block with blockNum retrieved from its receipt", user) + } +} + +func placeBidsAndDecideWinner(t *testing.T, ctx context.Context, seqClient *ethclient.Client, seqInfo *BlockchainTestInfo, auctionContract *express_lane_auctiongen.ExpressLaneAuction, winner, loser string, winnerBidderClient, loserBidderClient *timeboost.BidderClient, roundDuration time.Duration) { + t.Helper() + + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + Require(t, err) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) + Require(t, err) + currRound := roundTimingInfo.RoundNumber() + + // We are now in the bidding round, both issue their bids. winner will win + t.Logf("%s and %s now submitting their bids at %v", winner, loser, time.Now()) + winnerBid, err := winnerBidderClient.Bid(ctx, big.NewInt(2), seqInfo.GetAddress(winner)) + Require(t, err) + loserBid, err := loserBidderClient.Bid(ctx, big.NewInt(1), seqInfo.GetAddress(loser)) + Require(t, err) + t.Logf("%s bid %+v", winner, winnerBid) + t.Logf("%s bid %+v", loser, loserBid) + + // Subscribe to auction resolutions and wait for a winner + winnerAddr, winnerRound := awaitAuctionResolved(t, ctx, seqClient, auctionContract) + + // Verify winner wins the auction + if winnerAddr != seqInfo.GetAddress(winner) { + t.Fatalf("%s should have won the express lane auction", winner) + } + t.Logf("%s won the auction for the round: %d", winner, winnerRound) + if winnerRound != currRound+1 { + t.Fatalf("unexpected winner round: Want:%d Got:%d", currRound+1, winnerRound) + } + + it, err := auctionContract.FilterAuctionResolved(&bind.FilterOpts{Context: ctx}, nil, nil, nil) + Require(t, err) + winnerWon := false + for it.Next() { + if it.Event.FirstPriceBidder == seqInfo.GetAddress(winner) && it.Event.Round == winnerRound { + winnerWon = true + } + } + if !winnerWon { + t.Fatalf("%s should have won the auction", winner) + } +} + +func verifyControllerAdvantage(t *testing.T, ctx context.Context, seqClient *ethclient.Client, controllerClient *expressLaneClient, seqInfo *BlockchainTestInfo, controller, otherUser string) { + t.Helper() + + // During the express lane around, controller sends txs always 150ms later than otherUser, but otherUser's + // txs end up getting delayed by 200ms as they are not the express lane controller. + // In the end, controller's txs should be ordered before otherUser's during the round. + var wg sync.WaitGroup + wg.Add(2) + ownerAddr := seqInfo.GetAddress("Owner") + + otherUserNonce, err := seqClient.PendingNonceAt(ctx, seqInfo.GetAddress(otherUser)) + Require(t, err) + otherUserData := &types.DynamicFeeTx{ + To: &ownerAddr, + Gas: seqInfo.TransferGas, + GasFeeCap: new(big.Int).Set(seqInfo.GasPrice), + Value: big.NewInt(1e12), + Nonce: otherUserNonce, + Data: nil, + } + otherUserTx := seqInfo.SignTxAs(otherUser, otherUserData) + go func(w *sync.WaitGroup) { + defer w.Done() + Require(t, seqClient.SendTransaction(ctx, otherUserTx)) + }(&wg) + + controllerNonce, err := seqClient.PendingNonceAt(ctx, seqInfo.GetAddress(controller)) + Require(t, err) + controllerData := &types.DynamicFeeTx{ + To: &ownerAddr, + Gas: seqInfo.TransferGas, + GasFeeCap: new(big.Int).Set(seqInfo.GasPrice), + Value: big.NewInt(1e12), + Nonce: controllerNonce, + Data: nil, + } + controllerBoostableTx := seqInfo.SignTxAs(controller, controllerData) + go func(w *sync.WaitGroup) { + defer w.Done() + time.Sleep(time.Millisecond * 10) + Require(t, controllerClient.SendTransaction(ctx, controllerBoostableTx)) + }(&wg) + wg.Wait() + + // After round is done, verify that controller beats otherUser in the final sequence. + otherUserTxReceipt, err := seqClient.TransactionReceipt(ctx, otherUserTx.Hash()) + Require(t, err) + otherUserBlock := otherUserTxReceipt.BlockNumber.Uint64() + controllerBoostableTxReceipt, err := seqClient.TransactionReceipt(ctx, controllerBoostableTx.Hash()) + Require(t, err) + controllerBlock := controllerBoostableTxReceipt.BlockNumber.Uint64() + + if otherUserBlock < controllerBlock { + t.Fatalf("%s's tx should not have been sequenced before %s's in different blocks", otherUser, controller) + } else if otherUserBlock == controllerBlock { + if otherUserTxReceipt.TransactionIndex < controllerBoostableTxReceipt.TransactionIndex { + t.Fatalf("%s should have been sequenced before %s with express lane", controller, otherUser) + } + } } func setupExpressLaneAuction( @@ -417,7 +587,7 @@ func setupExpressLaneAuction( dbDirPath string, ctx context.Context, jwtSecretPath string, -) (*arbnode.Node, *ethclient.Client, *BlockchainTestInfo, common.Address, func(), *TestClient, func()) { +) (*arbnode.Node, *ethclient.Client, *BlockchainTestInfo, common.Address, *timeboost.BidderClient, *timeboost.BidderClient, time.Duration, func(), *TestClient, func()) { builderSeq := NewNodeBuilder(ctx).DefaultConfig(t, true) @@ -432,14 +602,17 @@ func setupExpressLaneAuction( builderSeq.nodeConfig.Feed.Output = *newBroadcasterConfigTest() builderSeq.execConfig.Sequencer.Enable = true builderSeq.execConfig.Sequencer.Timeboost = gethexec.TimeboostConfig{ - Enable: false, // We need to start without timeboost initially to create the auction contract - ExpressLaneAdvantage: time.Second * 5, - SequencerHTTPEndpoint: fmt.Sprintf("http://localhost:%d", seqPort), + Enable: false, // We need to start without timeboost initially to create the auction contract + ExpressLaneAdvantage: time.Second * 5, } cleanupSeq := builderSeq.Build(t) seqInfo, seqNode, seqClient := builderSeq.L2Info, builderSeq.L2.ConsensusNode, builderSeq.L2.Client - port := seqNode.BroadcastServer.ListenerAddr().(*net.TCPAddr).Port + tcpAddr, ok := seqNode.BroadcastServer.ListenerAddr().(*net.TCPAddr) + if !ok { + t.Fatalf("failed to cast listener address to *net.TCPAddr") + } + port := tcpAddr.Port builderFeedListener := NewNodeBuilder(ctx).DefaultConfig(t, true) builderFeedListener.isSequencer = false builderFeedListener.nodeConfig.Feed.Input = *newBroadcastClientConfigTest(port) @@ -538,7 +711,7 @@ func setupExpressLaneAuction( BiddingToken: biddingToken, Beneficiary: beneficiary, RoundTimingInfo: express_lane_auctiongen.RoundTimingInfo{ - OffsetTimestamp: initialTimestamp.Uint64(), + OffsetTimestamp: initialTimestamp.Int64(), RoundDurationSeconds: bidRoundSeconds, AuctionClosingSeconds: auctionClosingSeconds, ReserveSubmissionSeconds: reserveSubmissionSeconds, @@ -595,7 +768,7 @@ func setupExpressLaneAuction( // This is hacky- we are manually starting the ExpressLaneService here instead of letting it be started // by the sequencer. This is due to needing to deploy the auction contract first. builderSeq.execConfig.Sequencer.Timeboost.Enable = true - builderSeq.L2.ExecNode.Sequencer.StartExpressLane(ctx, proxyAddr, seqInfo.GetAddress("AuctionContract")) + builderSeq.L2.ExecNode.Sequencer.StartExpressLane(ctx, builderSeq.L2.ExecNode.Backend.APIBackend(), builderSeq.L2.ExecNode.FilterSystem, proxyAddr, seqInfo.GetAddress("AuctionContract"), gethexec.DefaultTimeboostConfig.EarlySubmissionGrace) t.Log("Started express lane service in sequencer") // Set up an autonomous auction contract service that runs in the background in this test. @@ -700,72 +873,24 @@ func setupExpressLaneAuction( bob.Start(ctx) // Wait until the initial round. - info, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) - Require(t, err) timeToWait := time.Until(initialTimestampUnix) t.Logf("Waiting until the initial round %v and %v, current time %v", timeToWait, initialTimestampUnix, time.Now()) <-time.After(timeToWait) t.Log("Started auction master stack and bid clients") - Require(t, alice.Deposit(ctx, big.NewInt(5))) - Require(t, bob.Deposit(ctx, big.NewInt(5))) + Require(t, alice.Deposit(ctx, big.NewInt(30))) + Require(t, bob.Deposit(ctx, big.NewInt(30))) // Wait until the next timeboost round + a few milliseconds. - now = time.Now() - waitTime = roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) t.Logf("Alice and Bob are now deposited into the autonomous auction contract, waiting %v for bidding round..., timestamp %v", waitTime, time.Now()) - time.Sleep(waitTime) - t.Logf("Reached the bidding round at %v", time.Now()) - time.Sleep(time.Second * 5) - - // We are now in the bidding round, both issue their bids. Bob will win. - t.Logf("Alice and Bob now submitting their bids at %v", time.Now()) - aliceBid, err := alice.Bid(ctx, big.NewInt(1), aliceOpts.From) - Require(t, err) - bobBid, err := bob.Bid(ctx, big.NewInt(2), bobOpts.From) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) Require(t, err) - t.Logf("Alice bid %+v", aliceBid) - t.Logf("Bob bid %+v", bobBid) - - // Subscribe to auction resolutions and wait for Bob to win the auction. - winner, winnerRound := awaitAuctionResolved(t, ctx, seqClient, auctionContract) - - // Verify Bob owns the express lane this round. - if winner != bobOpts.From { - t.Fatal("Bob should have won the express lane auction") - } - t.Log("Bob won the express lane auction for upcoming round, now waiting for that round to start...") - - // Wait until the round that Bob owns the express lane for. - now = time.Now() - waitTime = roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) - time.Sleep(waitTime) - - currRound := timeboost.CurrentRound(time.Unix(int64(info.OffsetTimestamp), 0), roundDuration) - t.Log("curr round", currRound) - if currRound != winnerRound { - now = time.Now() - waitTime = roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) - t.Log("Not express lane round yet, waiting for next round", waitTime) - time.Sleep(waitTime) - } - filterOpts := &bind.FilterOpts{ - Context: ctx, - Start: 0, - End: nil, - } - it, err := auctionContract.FilterAuctionResolved(filterOpts, nil, nil, nil) + roundTimingInfo, err := timeboost.NewRoundTimingInfo(rawRoundTimingInfo) Require(t, err) - bobWon := false - for it.Next() { - if it.Event.FirstPriceBidder == bobOpts.From { - bobWon = true - } - } - if !bobWon { - t.Fatal("Bob should have won the auction") - } - return seqNode, seqClient, seqInfo, proxyAddr, cleanupSeq, builderFeedListener.L2, cleanupFeedListener + time.Sleep(roundTimingInfo.TimeTilNextRound()) + t.Logf("Reached the bidding round at %v", time.Now()) + time.Sleep(time.Second * 5) + return seqNode, seqClient, seqInfo, proxyAddr, alice, bob, roundDuration, cleanupSeq, builderFeedListener.L2, cleanupFeedListener } func awaitAuctionResolved( @@ -813,31 +938,28 @@ func awaitAuctionResolved( type expressLaneClient struct { stopwaiter.StopWaiter sync.Mutex - privKey *ecdsa.PrivateKey - chainId *big.Int - initialRoundTimestamp time.Time - roundDuration time.Duration - auctionContractAddr common.Address - client *rpc.Client - sequence uint64 + privKey *ecdsa.PrivateKey + chainId *big.Int + roundTimingInfo timeboost.RoundTimingInfo + auctionContractAddr common.Address + client *rpc.Client + sequence uint64 } func newExpressLaneClient( privKey *ecdsa.PrivateKey, chainId *big.Int, - initialRoundTimestamp time.Time, - roundDuration time.Duration, + roundTimingInfo timeboost.RoundTimingInfo, auctionContractAddr common.Address, client *rpc.Client, ) *expressLaneClient { return &expressLaneClient{ - privKey: privKey, - chainId: chainId, - initialRoundTimestamp: initialRoundTimestamp, - roundDuration: roundDuration, - auctionContractAddr: auctionContractAddr, - client: client, - sequence: 0, + privKey: privKey, + chainId: chainId, + roundTimingInfo: roundTimingInfo, + auctionContractAddr: auctionContractAddr, + client: client, + sequence: 0, } } @@ -854,10 +976,10 @@ func (elc *expressLaneClient) SendTransaction(ctx context.Context, transaction * } msg := &timeboost.JsonExpressLaneSubmission{ ChainId: (*hexutil.Big)(elc.chainId), - Round: hexutil.Uint64(timeboost.CurrentRound(elc.initialRoundTimestamp, elc.roundDuration)), + Round: hexutil.Uint64(elc.roundTimingInfo.RoundNumber()), AuctionContractAddress: elc.auctionContractAddr, Transaction: encodedTx, - Sequence: hexutil.Uint64(elc.sequence), + SequenceNumber: hexutil.Uint64(elc.sequence), Signature: hexutil.Bytes{}, } msgGo, err := timeboost.JsonSubmissionToGo(msg) @@ -902,5 +1024,9 @@ func getRandomPort(t testing.TB) int { listener, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) defer listener.Close() - return listener.Addr().(*net.TCPAddr).Port + tcpAddr, ok := listener.Addr().(*net.TCPAddr) + if !ok { + t.Fatalf("failed to cast listener address to *net.TCPAddr") + } + return tcpAddr.Port } diff --git a/system_tests/transfer_test.go b/system_tests/transfer_test.go index a49e059351..c221ecc137 100644 --- a/system_tests/transfer_test.go +++ b/system_tests/transfer_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" ) func TestTransfer(t *testing.T) { @@ -51,12 +52,12 @@ func TestP256Verify(t *testing.T) { }{ { desc: "p256 should not be enabled on arbOS 20", - initialVersion: 20, + initialVersion: params.ArbosVersion_20, want: nil, }, { desc: "p256 should be enabled on arbOS 20", - initialVersion: 30, + initialVersion: params.ArbosVersion_30, want: common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001"), }, } { diff --git a/system_tests/triedb_race_test.go b/system_tests/triedb_race_test.go index 7828cf386d..78a7258aea 100644 --- a/system_tests/triedb_race_test.go +++ b/system_tests/triedb_race_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/util/testhelpers" ) diff --git a/system_tests/twonodeslong_test.go b/system_tests/twonodeslong_test.go index 83cd975dd8..5791661b16 100644 --- a/system_tests/twonodeslong_test.go +++ b/system_tests/twonodeslong_test.go @@ -14,10 +14,10 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/core/types" + "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbutil" - - "github.com/ethereum/go-ethereum/core/types" ) func testTwoNodesLong(t *testing.T, dasModeStr string) { @@ -63,6 +63,7 @@ func testTwoNodesLong(t *testing.T, dasModeStr string) { builder.L2Info.GenerateAccount("ErrorTxSender") builder.L2.SendWaitTestTransactions(t, []*types.Transaction{ + // #nosec G115 builder.L2Info.PrepareTx("Faucet", "ErrorTxSender", builder.L2Info.TransferGas, big.NewInt(l2pricing.InitialBaseFeeWei*int64(builder.L2Info.TransferGas)), nil), }) diff --git a/system_tests/unsupported_txtypes_test.go b/system_tests/unsupported_txtypes_test.go index 4c3c8661c8..6e92243c85 100644 --- a/system_tests/unsupported_txtypes_test.go +++ b/system_tests/unsupported_txtypes_test.go @@ -13,10 +13,11 @@ import ( "math/big" "testing" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" ) func TestBlobAndInternalTxsReject(t *testing.T) { @@ -112,8 +113,8 @@ func TestBlobAndInternalTxsAsDelayedMsgReject(t *testing.T) { blocknum, err := builder.L2.Client.BlockNumber(ctx) Require(t, err) - for i := int64(0); i <= int64(blocknum); i++ { - block, err := builder.L2.Client.BlockByNumber(ctx, big.NewInt(i)) + for i := uint64(0); i <= blocknum; i++ { + block, err := builder.L2.Client.BlockByNumber(ctx, new(big.Int).SetUint64(i)) Require(t, err) for _, tx := range block.Transactions() { if _, ok := txAcceptStatus[tx.Hash()]; ok { diff --git a/system_tests/validation_mock_test.go b/system_tests/validation_mock_test.go index 88421e4c4b..98dab7ad39 100644 --- a/system_tests/validation_mock_test.go +++ b/system_tests/validation_mock_test.go @@ -8,11 +8,12 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" @@ -21,11 +22,10 @@ import ( "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/validator" + validatorclient "github.com/offchainlabs/nitro/validator/client" "github.com/offchainlabs/nitro/validator/server_api" "github.com/offchainlabs/nitro/validator/server_arb" "github.com/offchainlabs/nitro/validator/valnode" - - validatorclient "github.com/offchainlabs/nitro/validator/client" ) type mockSpawner struct { @@ -61,8 +61,8 @@ func (s *mockSpawner) WasmModuleRoots() ([]common.Hash, error) { return mockWasmModuleRoots, nil } -func (s *mockSpawner) StylusArchs() []rawdb.Target { - return []rawdb.Target{"mock"} +func (s *mockSpawner) StylusArchs() []ethdb.WasmTarget { + return []ethdb.WasmTarget{"mock"} } func (s *mockSpawner) Launch(entry *validator.ValidationInput, moduleRoot common.Hash) validator.ValidationRun { @@ -84,7 +84,7 @@ func (s *mockSpawner) Stop() {} func (s *mockSpawner) Name() string { return "mock" } func (s *mockSpawner) Room() int { return 4 } -func (s *mockSpawner) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { +func (s *mockSpawner) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput, _ bool) containers.PromiseInterface[validator.ExecutionRun] { s.ExecSpawned = append(s.ExecSpawned, input.Id) return containers.NewReadyPromise[validator.ExecutionRun](&mockExecRun{ startState: input.StartState, @@ -96,10 +96,6 @@ func (s *mockSpawner) LatestWasmModuleRoot() containers.PromiseInterface[common. return containers.NewReadyPromise[common.Hash](mockWasmModuleRoots[0], nil) } -func (s *mockSpawner) WriteToFile(input *validator.ValidationInput, expOut validator.GoGlobalState, moduleRoot common.Hash) containers.PromiseInterface[struct{}] { - return containers.NewReadyPromise[struct{}](struct{}{}, nil) -} - type mockValRun struct { containers.Promise[validator.GoGlobalState] root common.Hash @@ -159,6 +155,10 @@ func (r *mockExecRun) PrepareRange(uint64, uint64) containers.PromiseInterface[s return containers.NewReadyPromise[struct{}](struct{}{}, nil) } +func (r *mockExecRun) CheckAlive(ctx context.Context) error { + return nil +} + func (r *mockExecRun) Close() {} func createMockValidationNode(t *testing.T, ctx context.Context, config *server_arb.ArbitratorSpawnerConfig) (*mockSpawner, *node.Node) { @@ -260,7 +260,7 @@ func TestValidationServerAPI(t *testing.T) { if res != endState { t.Error("unexpected mock validation run") } - execRun, err := client.CreateExecutionRun(wasmRoot, &valInput).Await(ctx) + execRun, err := client.CreateExecutionRun(wasmRoot, &valInput, false).Await(ctx) Require(t, err) step0 := execRun.GetStepAt(0) step0Res, err := step0.Await(ctx) @@ -385,9 +385,9 @@ func TestExecutionKeepAlive(t *testing.T) { Require(t, err) valInput := validator.ValidationInput{} - runDefault, err := clientDefault.CreateExecutionRun(wasmRoot, &valInput).Await(ctx) + runDefault, err := clientDefault.CreateExecutionRun(wasmRoot, &valInput, false).Await(ctx) Require(t, err) - runShortTO, err := clientShortTO.CreateExecutionRun(wasmRoot, &valInput).Await(ctx) + runShortTO, err := clientShortTO.CreateExecutionRun(wasmRoot, &valInput, false).Await(ctx) Require(t, err) <-time.After(time.Second * 10) stepDefault := runDefault.GetStepAt(0) diff --git a/system_tests/wrap_transaction_test.go b/system_tests/wrap_transaction_test.go index bd561ad5e5..dd68c25d6a 100644 --- a/system_tests/wrap_transaction_test.go +++ b/system_tests/wrap_transaction_test.go @@ -15,14 +15,16 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/headerreader" ) -func GetPendingBlockNumber(ctx context.Context, client arbutil.L1Interface) (*big.Int, error) { +func GetPendingBlockNumber(ctx context.Context, client *ethclient.Client) (*big.Int, error) { // Attempt to get the block number from ArbSys, if it exists arbSys, err := precompilesgen.NewArbSys(common.BigToAddress(big.NewInt(100)), client) if err != nil { @@ -37,7 +39,7 @@ func GetPendingBlockNumber(ctx context.Context, client arbutil.L1Interface) (*bi } // Will wait until txhash is in the blockchain and return its receipt -func WaitForTx(ctxinput context.Context, client arbutil.L1Interface, txhash common.Hash, timeout time.Duration) (*types.Receipt, error) { +func WaitForTx(ctxinput context.Context, client *ethclient.Client, txhash common.Hash, timeout time.Duration) (*types.Receipt, error) { ctx, cancel := context.WithTimeout(ctxinput, timeout) defer cancel() @@ -75,11 +77,11 @@ func WaitForTx(ctxinput context.Context, client arbutil.L1Interface, txhash comm } } -func EnsureTxSucceeded(ctx context.Context, client arbutil.L1Interface, tx *types.Transaction) (*types.Receipt, error) { +func EnsureTxSucceeded(ctx context.Context, client *ethclient.Client, tx *types.Transaction) (*types.Receipt, error) { return EnsureTxSucceededWithTimeout(ctx, client, tx, time.Second*5) } -func EnsureTxSucceededWithTimeout(ctx context.Context, client arbutil.L1Interface, tx *types.Transaction, timeout time.Duration) (*types.Receipt, error) { +func EnsureTxSucceededWithTimeout(ctx context.Context, client *ethclient.Client, tx *types.Transaction, timeout time.Duration) (*types.Receipt, error) { receipt, err := WaitForTx(ctx, client, tx.Hash(), timeout) if err != nil { return nil, fmt.Errorf("waitFoxTx (tx=%s) got: %w", tx.Hash().Hex(), err) @@ -103,12 +105,12 @@ func EnsureTxSucceededWithTimeout(ctx context.Context, client arbutil.L1Interfac return receipt, arbutil.DetailTxError(ctx, client, tx, receipt) } -func EnsureTxFailed(t *testing.T, ctx context.Context, client arbutil.L1Interface, tx *types.Transaction) *types.Receipt { +func EnsureTxFailed(t *testing.T, ctx context.Context, client *ethclient.Client, tx *types.Transaction) *types.Receipt { t.Helper() return EnsureTxFailedWithTimeout(t, ctx, client, tx, time.Second*5) } -func EnsureTxFailedWithTimeout(t *testing.T, ctx context.Context, client arbutil.L1Interface, tx *types.Transaction, timeout time.Duration) *types.Receipt { +func EnsureTxFailedWithTimeout(t *testing.T, ctx context.Context, client *ethclient.Client, tx *types.Transaction, timeout time.Duration) *types.Receipt { t.Helper() receipt, err := WaitForTx(ctx, client, tx.Hash(), timeout) Require(t, err) diff --git a/timeboost/auctioneer.go b/timeboost/auctioneer.go index 86225ff2f5..5c40505a6b 100644 --- a/timeboost/auctioneer.go +++ b/timeboost/auctioneer.go @@ -11,6 +11,11 @@ import ( "os" "time" + "github.com/golang-jwt/jwt/v4" + "github.com/pkg/errors" + "github.com/spf13/pflag" + "golang.org/x/crypto/sha3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -19,16 +24,13 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rpc" - "github.com/golang-jwt/jwt/v4" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util" "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/pkg/errors" - "github.com/spf13/pflag" - "golang.org/x/crypto/sha3" ) // domainValue holds the Keccak256 hash of the string "TIMEBOOST_BID". @@ -60,26 +62,31 @@ type AuctioneerServerConfig struct { RedisURL string `koanf:"redis-url"` ConsumerConfig pubsub.ConsumerConfig `koanf:"consumer-config"` // Timeout on polling for existence of each redis stream. - StreamTimeout time.Duration `koanf:"stream-timeout"` - Wallet genericconf.WalletConfig `koanf:"wallet"` - SequencerEndpoint string `koanf:"sequencer-endpoint"` - SequencerJWTPath string `koanf:"sequencer-jwt-path"` - AuctionContractAddress string `koanf:"auction-contract-address"` - DbDirectory string `koanf:"db-directory"` + StreamTimeout time.Duration `koanf:"stream-timeout"` + Wallet genericconf.WalletConfig `koanf:"wallet"` + SequencerEndpoint string `koanf:"sequencer-endpoint"` + SequencerJWTPath string `koanf:"sequencer-jwt-path"` + AuctionContractAddress string `koanf:"auction-contract-address"` + DbDirectory string `koanf:"db-directory"` + AuctionResolutionWaitTime time.Duration `koanf:"auction-resolution-wait-time"` + S3Storage S3StorageServiceConfig `koanf:"s3-storage"` } var DefaultAuctioneerServerConfig = AuctioneerServerConfig{ - Enable: true, - RedisURL: "", - ConsumerConfig: pubsub.DefaultConsumerConfig, - StreamTimeout: 10 * time.Minute, + Enable: true, + RedisURL: "", + ConsumerConfig: pubsub.DefaultConsumerConfig, + StreamTimeout: 10 * time.Minute, + AuctionResolutionWaitTime: 2 * time.Second, + S3Storage: DefaultS3StorageServiceConfig, } var TestAuctioneerServerConfig = AuctioneerServerConfig{ - Enable: true, - RedisURL: "", - ConsumerConfig: pubsub.TestConsumerConfig, - StreamTimeout: time.Minute, + Enable: true, + RedisURL: "", + ConsumerConfig: pubsub.TestConsumerConfig, + StreamTimeout: time.Minute, + AuctionResolutionWaitTime: 2 * time.Second, } func AuctioneerServerConfigAddOptions(prefix string, f *pflag.FlagSet) { @@ -92,26 +99,28 @@ func AuctioneerServerConfigAddOptions(prefix string, f *pflag.FlagSet) { f.String(prefix+".sequencer-jwt-path", DefaultAuctioneerServerConfig.SequencerJWTPath, "sequencer jwt file path") f.String(prefix+".auction-contract-address", DefaultAuctioneerServerConfig.AuctionContractAddress, "express lane auction contract address") f.String(prefix+".db-directory", DefaultAuctioneerServerConfig.DbDirectory, "path to database directory for persisting validated bids in a sqlite file") + f.Duration(prefix+".auction-resolution-wait-time", DefaultAuctioneerServerConfig.AuctionResolutionWaitTime, "wait time after auction closing before resolving the auction") + S3StorageServiceConfigAddOptions(prefix+".s3-storage", f) } // AuctioneerServer is a struct that represents an autonomous auctioneer. // It is responsible for receiving bids, validating them, and resolving auctions. type AuctioneerServer struct { stopwaiter.StopWaiter - consumer *pubsub.Consumer[*JsonValidatedBid, error] - txOpts *bind.TransactOpts - chainId *big.Int - sequencerRpc *rpc.Client - client *ethclient.Client - auctionContract *express_lane_auctiongen.ExpressLaneAuction - auctionContractAddr common.Address - bidsReceiver chan *JsonValidatedBid - bidCache *bidCache - initialRoundTimestamp time.Time - auctionClosingDuration time.Duration - roundDuration time.Duration - streamTimeout time.Duration - database *SqliteDatabase + consumer *pubsub.Consumer[*JsonValidatedBid, error] + txOpts *bind.TransactOpts + chainId *big.Int + sequencerRpc *rpc.Client + client *ethclient.Client + auctionContract *express_lane_auctiongen.ExpressLaneAuction + auctionContractAddr common.Address + bidsReceiver chan *JsonValidatedBid + bidCache *bidCache + roundTimingInfo RoundTimingInfo + streamTimeout time.Duration + auctionResolutionWaitTime time.Duration + database *SqliteDatabase + s3StorageService *S3StorageService } // NewAuctioneerServer creates a new autonomous auctioneer struct. @@ -133,6 +142,13 @@ func NewAuctioneerServer(ctx context.Context, configFetcher AuctioneerServerConf if err != nil { return nil, err } + var s3StorageService *S3StorageService + if cfg.S3Storage.Enable { + s3StorageService, err = NewS3StorageService(&cfg.S3Storage, database) + if err != nil { + return nil, err + } + } auctionContractAddr := common.HexToAddress(cfg.AuctionContractAddress) redisClient, err := redisutil.RedisClientFromURL(cfg.RedisURL) if err != nil { @@ -181,32 +197,40 @@ func NewAuctioneerServer(ctx context.Context, configFetcher AuctioneerServerConf if err != nil { return nil, err } - roundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) if err != nil { return nil, err } - auctionClosingDuration := time.Duration(roundTimingInfo.AuctionClosingSeconds) * time.Second - initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0) - roundDuration := time.Duration(roundTimingInfo.RoundDurationSeconds) * time.Second + roundTimingInfo, err := NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { + return nil, err + } + if err = roundTimingInfo.ValidateResolutionWaitTime(cfg.AuctionResolutionWaitTime); err != nil { + return nil, err + } return &AuctioneerServer{ - txOpts: txOpts, - sequencerRpc: client, - chainId: chainId, - client: sequencerClient, - database: database, - consumer: c, - auctionContract: auctionContract, - auctionContractAddr: auctionContractAddr, - bidsReceiver: make(chan *JsonValidatedBid, 100_000), // TODO(Terence): Is 100k enough? Make this configurable? - bidCache: newBidCache(), - initialRoundTimestamp: initialTimestamp, - auctionClosingDuration: auctionClosingDuration, - roundDuration: roundDuration, + txOpts: txOpts, + sequencerRpc: client, + chainId: chainId, + client: sequencerClient, + database: database, + s3StorageService: s3StorageService, + consumer: c, + auctionContract: auctionContract, + auctionContractAddr: auctionContractAddr, + bidsReceiver: make(chan *JsonValidatedBid, 100_000), // TODO(Terence): Is 100k enough? Make this configurable? + bidCache: newBidCache(), + roundTimingInfo: *roundTimingInfo, + auctionResolutionWaitTime: cfg.AuctionResolutionWaitTime, }, nil } func (a *AuctioneerServer) Start(ctx_in context.Context) { a.StopWaiter.Start(ctx_in, a) + // Start S3 storage service to persist validated bids to s3 + if a.s3StorageService != nil { + a.s3StorageService.Start(ctx_in) + } // Channel that consumer uses to indicate its readiness. readyStream := make(chan struct{}, 1) a.consumer.Start(ctx_in) @@ -255,6 +279,7 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) { log.Error("Error setting result for request", "id", req.ID, "result", nil, "error", err) return 0 } + req.Ack() return 0 }) }) @@ -292,8 +317,8 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) { // Auction resolution thread. a.StopWaiter.LaunchThread(func(ctx context.Context) { - ticker := newAuctionCloseTicker(a.roundDuration, a.auctionClosingDuration) - go ticker.start() + ticker := newRoundTicker(a.roundTimingInfo) + go ticker.tickAtAuctionClose() for { select { case <-ctx.Done(): @@ -301,8 +326,7 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) { return case auctionClosingTime := <-ticker.c: log.Info("New auction closing time reached", "closingTime", auctionClosingTime, "totalBids", a.bidCache.size()) - // Wait for two seconds, just to give some leeway for latency of bids received last minute. - time.Sleep(2 * time.Second) + time.Sleep(a.auctionResolutionWaitTime) if err := a.resolveAuction(ctx); err != nil { log.Error("Could not resolve auction for round", "error", err) } @@ -315,7 +339,7 @@ func (a *AuctioneerServer) Start(ctx_in context.Context) { // Resolves the auction by calling the smart contract with the top two bids. func (a *AuctioneerServer) resolveAuction(ctx context.Context) error { - upcomingRound := CurrentRound(a.initialRoundTimestamp, a.roundDuration) + 1 + upcomingRound := a.roundTimingInfo.RoundNumber() + 1 result := a.bidCache.topTwoBids() first := result.firstPlace second := result.secondPlace @@ -363,8 +387,7 @@ func (a *AuctioneerServer) resolveAuction(ctx context.Context) error { return err } - currentRound := CurrentRound(a.initialRoundTimestamp, a.roundDuration) - roundEndTime := a.initialRoundTimestamp.Add(time.Duration(currentRound) * a.roundDuration) + roundEndTime := a.roundTimingInfo.TimeOfNextRound() retryInterval := 1 * time.Second if err := retryUntil(ctx, func() error { diff --git a/timeboost/auctioneer_test.go b/timeboost/auctioneer_test.go index 951dee8845..855ec53687 100644 --- a/timeboost/auctioneer_test.go +++ b/timeboost/auctioneer_test.go @@ -10,16 +10,18 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/util/redisutil" - "github.com/stretchr/testify/require" ) func TestBidValidatorAuctioneerRedisStream(t *testing.T) { @@ -134,7 +136,7 @@ func TestBidValidatorAuctioneerRedisStream(t *testing.T) { require.NoError(t, err) _, err = bob.Bid(ctx, big.NewInt(int64(i)+1), bobAddr) // Bob bids 1 wei higher than Alice. require.NoError(t, err) - _, err = charlie.Bid(ctx, big.NewInt(int64(i)+2), charlieAddr) // Charlie bids 2 wei higher than the Bob. + _, err = charlie.Bid(ctx, big.NewInt(int64(i)+2), charlieAddr) // Charlie bids 2 wei higher than the Alice. require.NoError(t, err) } @@ -151,12 +153,14 @@ func TestBidValidatorAuctioneerRedisStream(t *testing.T) { // We verify that the auctioneer has consumed all validated bids from the single Redis stream. // We also verify the top two bids are those we expect. + am.bidCache.Lock() require.Equal(t, 3, len(am.bidCache.bidsByExpressLaneControllerAddr)) + am.bidCache.Unlock() result := am.bidCache.topTwoBids() - require.Equal(t, result.firstPlace.Amount, big.NewInt(6)) - require.Equal(t, result.firstPlace.Bidder, charlieAddr) - require.Equal(t, result.secondPlace.Amount, big.NewInt(5)) - require.Equal(t, result.secondPlace.Bidder, bobAddr) + require.Equal(t, big.NewInt(7), result.firstPlace.Amount) // Best bid should be Charlie's last bid 7 + require.Equal(t, charlieAddr, result.firstPlace.Bidder) + require.Equal(t, big.NewInt(6), result.secondPlace.Amount) // Second best bid should be Bob's last bid of 6 + require.Equal(t, bobAddr, result.secondPlace.Bidder) } func TestRetryUntil(t *testing.T) { diff --git a/timeboost/bid_cache.go b/timeboost/bid_cache.go index 4031ab9a0b..3c0a31e553 100644 --- a/timeboost/bid_cache.go +++ b/timeboost/bid_cache.go @@ -50,16 +50,16 @@ func (bc *bidCache) topTwoBids() *auctionResult { result.secondPlace = result.firstPlace result.firstPlace = bid } else if bid.Amount.Cmp(result.firstPlace.Amount) == 0 { - if bid.BigIntHash().Cmp(result.firstPlace.BigIntHash()) > 0 { + if bid.bigIntHash().Cmp(result.firstPlace.bigIntHash()) > 0 { result.secondPlace = result.firstPlace result.firstPlace = bid - } else if result.secondPlace == nil || bid.BigIntHash().Cmp(result.secondPlace.BigIntHash()) > 0 { + } else if result.secondPlace == nil || bid.bigIntHash().Cmp(result.secondPlace.bigIntHash()) > 0 { result.secondPlace = bid } } else if result.secondPlace == nil || bid.Amount.Cmp(result.secondPlace.Amount) > 0 { result.secondPlace = bid } else if bid.Amount.Cmp(result.secondPlace.Amount) == 0 { - if bid.BigIntHash().Cmp(result.secondPlace.BigIntHash()) > 0 { + if bid.bigIntHash().Cmp(result.secondPlace.bigIntHash()) > 0 { result.secondPlace = bid } } diff --git a/timeboost/bid_cache_test.go b/timeboost/bid_cache_test.go index c0aa7eafd5..b28d69dd1c 100644 --- a/timeboost/bid_cache_test.go +++ b/timeboost/bid_cache_test.go @@ -7,13 +7,15 @@ import ( "net" "testing" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/util/redisutil" - "github.com/stretchr/testify/require" ) func TestTopTwoBids(t *testing.T) { @@ -208,5 +210,9 @@ func getRandomPort(t testing.TB) int { listener, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) defer listener.Close() - return listener.Addr().(*net.TCPAddr).Port + tcpAddr, ok := listener.Addr().(*net.TCPAddr) + if !ok { + t.Fatalf("failed to cast listener address to *net.TCPAddr") + } + return tcpAddr.Port } diff --git a/timeboost/bid_validator.go b/timeboost/bid_validator.go index 61360d0d8d..fda5bcf58d 100644 --- a/timeboost/bid_validator.go +++ b/timeboost/bid_validator.go @@ -7,6 +7,10 @@ import ( "sync" "time" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -14,13 +18,11 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" - "github.com/go-redis/redis/v8" + "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/pkg/errors" - "github.com/spf13/pflag" ) type BidValidatorConfigFetcher func() *BidValidatorConfig @@ -57,24 +59,22 @@ func BidValidatorConfigAddOptions(prefix string, f *pflag.FlagSet) { type BidValidator struct { stopwaiter.StopWaiter sync.RWMutex - chainId *big.Int - stack *node.Node - producerCfg *pubsub.ProducerConfig - producer *pubsub.Producer[*JsonValidatedBid, error] - redisClient redis.UniversalClient - domainValue []byte - client *ethclient.Client - auctionContract *express_lane_auctiongen.ExpressLaneAuction - auctionContractAddr common.Address - bidsReceiver chan *Bid - initialRoundTimestamp time.Time - roundDuration time.Duration - auctionClosingDuration time.Duration - reserveSubmissionDuration time.Duration - reservePriceLock sync.RWMutex - reservePrice *big.Int - bidsPerSenderInRound map[common.Address]uint8 - maxBidsPerSenderInRound uint8 + chainId *big.Int + stack *node.Node + producerCfg *pubsub.ProducerConfig + producer *pubsub.Producer[*JsonValidatedBid, error] + redisClient redis.UniversalClient + domainValue []byte + client *ethclient.Client + auctionContract *express_lane_auctiongen.ExpressLaneAuction + auctionContractAddr common.Address + auctionContractDomainSeparator [32]byte + bidsReceiver chan *Bid + roundTimingInfo RoundTimingInfo + reservePriceLock sync.RWMutex + reservePrice *big.Int + bidsPerSenderInRound map[common.Address]uint8 + maxBidsPerSenderInRound uint8 } func NewBidValidator( @@ -108,36 +108,42 @@ func NewBidValidator( if err != nil { return nil, err } - roundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{}) + if err != nil { + return nil, err + } + roundTimingInfo, err := NewRoundTimingInfo(rawRoundTimingInfo) if err != nil { return nil, err } - initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0) - roundDuration := time.Duration(roundTimingInfo.RoundDurationSeconds) * time.Second - auctionClosingDuration := time.Duration(roundTimingInfo.AuctionClosingSeconds) * time.Second - reserveSubmissionDuration := time.Duration(roundTimingInfo.ReserveSubmissionSeconds) * time.Second reservePrice, err := auctionContract.ReservePrice(&bind.CallOpts{}) if err != nil { return nil, err } + + domainSeparator, err := auctionContract.DomainSeparator(&bind.CallOpts{ + Context: ctx, + }) + if err != nil { + return nil, err + } + bidValidator := &BidValidator{ - chainId: chainId, - client: sequencerClient, - redisClient: redisClient, - stack: stack, - auctionContract: auctionContract, - auctionContractAddr: auctionContractAddr, - bidsReceiver: make(chan *Bid, 10_000), - initialRoundTimestamp: initialTimestamp, - roundDuration: roundDuration, - auctionClosingDuration: auctionClosingDuration, - reserveSubmissionDuration: reserveSubmissionDuration, - reservePrice: reservePrice, - domainValue: domainValue, - bidsPerSenderInRound: make(map[common.Address]uint8), - maxBidsPerSenderInRound: 5, // 5 max bids per sender address in a round. - producerCfg: &cfg.ProducerConfig, + chainId: chainId, + client: sequencerClient, + redisClient: redisClient, + stack: stack, + auctionContract: auctionContract, + auctionContractAddr: auctionContractAddr, + auctionContractDomainSeparator: domainSeparator, + bidsReceiver: make(chan *Bid, 10_000), + roundTimingInfo: *roundTimingInfo, + reservePrice: reservePrice, + domainValue: domainValue, + bidsPerSenderInRound: make(map[common.Address]uint8), + maxBidsPerSenderInRound: 5, // 5 max bids per sender address in a round. + producerCfg: &cfg.ProducerConfig, } api := &BidValidatorAPI{bidValidator} valAPIs := []rpc.API{{ @@ -188,16 +194,19 @@ func (bv *BidValidator) Start(ctx_in context.Context) { } bv.producer.Start(ctx_in) - // Set reserve price thread. + // Thread to set reserve price and clear per-round map of bid count per account. bv.StopWaiter.LaunchThread(func(ctx context.Context) { - ticker := newAuctionCloseTicker(bv.roundDuration, bv.auctionClosingDuration+bv.reserveSubmissionDuration) - go ticker.start() + reservePriceTicker := newRoundTicker(bv.roundTimingInfo) + go reservePriceTicker.tickAtReserveSubmissionDeadline() + auctionCloseTicker := newRoundTicker(bv.roundTimingInfo) + go auctionCloseTicker.tickAtAuctionClose() + for { select { case <-ctx.Done(): log.Error("Context closed, autonomous auctioneer shutting down") return - case <-ticker.c: + case <-reservePriceTicker.c: rp, err := bv.auctionContract.ReservePrice(&bind.CallOpts{}) if err != nil { log.Error("Could not get reserve price", "error", err) @@ -212,6 +221,7 @@ func (bv *BidValidator) Start(ctx_in context.Context) { log.Info("Reserve price updated", "old", currentReservePrice.String(), "new", rp.String()) bv.setReservePrice(rp) + case <-auctionCloseTicker.c: bv.Lock() bv.bidsPerSenderInRound = make(map[common.Address]uint8) bv.Unlock() @@ -285,18 +295,13 @@ func (bv *BidValidator) validateBid( } // Check if the bid is intended for upcoming round. - upcomingRound := CurrentRound(bv.initialRoundTimestamp, bv.roundDuration) + 1 + upcomingRound := bv.roundTimingInfo.RoundNumber() + 1 if bid.Round != upcomingRound { return nil, errors.Wrapf(ErrBadRoundNumber, "wanted %d, got %d", upcomingRound, bid.Round) } // Check if the auction is closed. - if isAuctionRoundClosed( - time.Now(), - bv.initialRoundTimestamp, - bv.roundDuration, - bv.auctionClosingDuration, - ) { + if bv.roundTimingInfo.isAuctionRoundClosed() { return nil, errors.Wrap(ErrBadRoundNumber, "auction is closed") } @@ -306,10 +311,10 @@ func (bv *BidValidator) validateBid( } // Validate the signature. - packedBidBytes := bid.ToMessageBytes() if len(bid.Signature) != 65 { return nil, errors.Wrap(ErrMalformedData, "signature length is not 65") } + // Recover the public key. sigItem := make([]byte, len(bid.Signature)) copy(sigItem, bid.Signature) @@ -320,7 +325,12 @@ func (bv *BidValidator) validateBid( if sigItem[len(sigItem)-1] >= 27 { sigItem[len(sigItem)-1] -= 27 } - pubkey, err := crypto.SigToPub(buildEthereumSignedMessage(packedBidBytes), sigItem) + + bidHash, err := bid.ToEIP712Hash(bv.auctionContractDomainSeparator) + if err != nil { + return nil, err + } + pubkey, err := crypto.SigToPub(bidHash[:], sigItem) if err != nil { return nil, ErrMalformedData } @@ -329,7 +339,7 @@ func (bv *BidValidator) validateBid( bv.Lock() numBids, ok := bv.bidsPerSenderInRound[bidder] if !ok { - bv.bidsPerSenderInRound[bidder] = 1 + bv.bidsPerSenderInRound[bidder] = 0 } if numBids >= bv.maxBidsPerSenderInRound { bv.Unlock() @@ -343,10 +353,10 @@ func (bv *BidValidator) validateBid( return nil, err } if depositBal.Cmp(new(big.Int)) == 0 { - return nil, ErrNotDepositor + return nil, errors.Wrapf(ErrNotDepositor, "bidder %s", bidder.Hex()) } if depositBal.Cmp(bid.Amount) < 0 { - return nil, errors.Wrapf(ErrInsufficientBalance, "onchain balance %#x, bid amount %#x", depositBal, bid.Amount) + return nil, errors.Wrapf(ErrInsufficientBalance, "bidder %s, onchain balance %#x, bid amount %#x", bidder.Hex(), depositBal, bid.Amount) } vb := &ValidatedBid{ ExpressLaneController: bid.ExpressLaneController, diff --git a/timeboost/bid_validator_test.go b/timeboost/bid_validator_test.go index 6532596ab3..80ddc481a5 100644 --- a/timeboost/bid_validator_test.go +++ b/timeboost/bid_validator_test.go @@ -2,16 +2,15 @@ package timeboost import ( "context" - "crypto/ecdsa" - "fmt" "math/big" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" ) func TestBidValidator_validateBid(t *testing.T) { @@ -102,20 +101,22 @@ func TestBidValidator_validateBid(t *testing.T) { for _, tt := range tests { bv := BidValidator{ - chainId: big.NewInt(1), - initialRoundTimestamp: time.Now().Add(-time.Second), + chainId: big.NewInt(1), + roundTimingInfo: RoundTimingInfo{ + Offset: time.Now().Add(-time.Second * 3), + Round: 10 * time.Second, + AuctionClosing: 5 * time.Second, + }, reservePrice: big.NewInt(2), - roundDuration: time.Minute, - auctionClosingDuration: 45 * time.Second, auctionContract: setup.expressLaneAuction, auctionContractAddr: setup.expressLaneAuctionAddr, bidsPerSenderInRound: make(map[common.Address]uint8), maxBidsPerSenderInRound: 5, } - if tt.auctionClosed { - bv.roundDuration = 0 - } t.Run(tt.name, func(t *testing.T) { + if tt.auctionClosed { + time.Sleep(time.Second * 3) + } _, err := bv.validateBid(tt.bid, setup.expressLaneAuction.BalanceOf) require.ErrorIs(t, err, tt.expectedErr) require.Contains(t, err.Error(), tt.errMsg) @@ -130,14 +131,17 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) { } auctionContractAddr := common.Address{'a'} bv := BidValidator{ - chainId: big.NewInt(1), - initialRoundTimestamp: time.Now().Add(-time.Second), - reservePrice: big.NewInt(2), - roundDuration: time.Minute, - auctionClosingDuration: 45 * time.Second, - bidsPerSenderInRound: make(map[common.Address]uint8), - maxBidsPerSenderInRound: 5, - auctionContractAddr: auctionContractAddr, + chainId: big.NewInt(1), + roundTimingInfo: RoundTimingInfo{ + Offset: time.Now().Add(-time.Second), + Round: time.Minute, + AuctionClosing: 45 * time.Second, + }, + reservePrice: big.NewInt(2), + bidsPerSenderInRound: make(map[common.Address]uint8), + maxBidsPerSenderInRound: 5, + auctionContractAddr: auctionContractAddr, + auctionContractDomainSeparator: common.Hash{}, } privateKey, err := crypto.GenerateKey() require.NoError(t, err) @@ -149,7 +153,11 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) { Amount: big.NewInt(3), Signature: []byte{'a'}, } - signature, err := buildSignature(privateKey, bid.ToMessageBytes()) + + bidHash, err := bid.ToEIP712Hash(bv.auctionContractDomainSeparator) + require.NoError(t, err) + + signature, err := crypto.Sign(bidHash[:], privateKey) require.NoError(t, err) bid.Signature = signature @@ -162,15 +170,6 @@ func TestBidValidator_validateBid_perRoundBidLimitReached(t *testing.T) { } -func buildSignature(privateKey *ecdsa.PrivateKey, data []byte) ([]byte, error) { - prefixedData := crypto.Keccak256(append([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(data))), data...)) - signature, err := crypto.Sign(prefixedData, privateKey) - if err != nil { - return nil, err - } - return signature, nil -} - func buildValidBid(t *testing.T, auctionContractAddr common.Address) *Bid { privateKey, err := crypto.GenerateKey() require.NoError(t, err) @@ -183,7 +182,10 @@ func buildValidBid(t *testing.T, auctionContractAddr common.Address) *Bid { Signature: []byte{'a'}, } - signature, err := buildSignature(privateKey, bid.ToMessageBytes()) + bidHash, err := bid.ToEIP712Hash(common.Hash{}) + require.NoError(t, err) + + signature, err := crypto.Sign(bidHash[:], privateKey) require.NoError(t, err) bid.Signature = signature diff --git a/timeboost/bidder_client.go b/timeboost/bidder_client.go index 884cfe8acc..66c69991f4 100644 --- a/timeboost/bidder_client.go +++ b/timeboost/bidder_client.go @@ -4,15 +4,17 @@ import ( "context" "fmt" "math/big" - "time" + + "github.com/pkg/errors" + "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/cmd/util" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" @@ -20,8 +22,6 @@ import ( "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/signature" "github.com/offchainlabs/nitro/util/stopwaiter" - "github.com/pkg/errors" - "github.com/spf13/pflag" ) type BidderClientConfigFetcher func() *BidderClientConfig @@ -65,8 +65,7 @@ type BidderClient struct { auctionContract *express_lane_auctiongen.ExpressLaneAuction biddingTokenContract *bindings.MockERC20 auctioneerClient *rpc.Client - initialRoundTimestamp time.Time - roundDuration time.Duration + roundTimingInfo RoundTimingInfo domainValue []byte } @@ -75,6 +74,8 @@ func NewBidderClient( configFetcher BidderClientConfigFetcher, ) (*BidderClient, error) { cfg := configFetcher() + _ = cfg.BidGwei // These fields are used from cmd/bidder-client + _ = cfg.DepositGwei // this marks them as used for the linter. if cfg.AuctionContractAddress == "" { return nil, fmt.Errorf("auction contract address cannot be empty") } @@ -92,14 +93,16 @@ func NewBidderClient( if err != nil { return nil, err } - roundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{ + rawRoundTimingInfo, err := auctionContract.RoundTimingInfo(&bind.CallOpts{ Context: ctx, }) if err != nil { return nil, err } - initialTimestamp := time.Unix(int64(roundTimingInfo.OffsetTimestamp), 0) - roundDuration := time.Duration(roundTimingInfo.RoundDurationSeconds) * time.Second + roundTimingInfo, err := NewRoundTimingInfo(rawRoundTimingInfo) + if err != nil { + return nil, err + } txOpts, signer, err := util.OpenWallet("bidder-client", &cfg.Wallet, chainId) if err != nil { return nil, errors.Wrap(err, "opening wallet") @@ -130,8 +133,7 @@ func NewBidderClient( auctionContract: auctionContract, biddingTokenContract: biddingTokenContract, auctioneerClient: bidValidatorClient, - initialRoundTimestamp: initialTimestamp, - roundDuration: roundDuration, + roundTimingInfo: *roundTimingInfo, domainValue: domainValue, }, nil } @@ -186,20 +188,33 @@ func (bd *BidderClient) Bid( if (expressLaneController == common.Address{}) { expressLaneController = bd.txOpts.From } + + domainSeparator, err := bd.auctionContract.DomainSeparator(&bind.CallOpts{ + Context: ctx, + }) + if err != nil { + return nil, err + } newBid := &Bid{ ChainId: bd.chainId, ExpressLaneController: expressLaneController, AuctionContractAddress: bd.auctionContractAddress, - Round: CurrentRound(bd.initialRoundTimestamp, bd.roundDuration) + 1, + Round: bd.roundTimingInfo.RoundNumber() + 1, Amount: amount, - Signature: nil, } - sig, err := bd.signer(buildEthereumSignedMessage(newBid.ToMessageBytes())) + bidHash, err := newBid.ToEIP712Hash(domainSeparator) + if err != nil { + return nil, err + } + + sig, err := bd.signer(bidHash.Bytes()) if err != nil { return nil, err } sig[64] += 27 + newBid.Signature = sig + promise := bd.submitBid(newBid) if _, err := promise.Await(ctx); err != nil { return nil, err @@ -213,7 +228,3 @@ func (bd *BidderClient) submitBid(bid *Bid) containers.PromiseInterface[struct{} return struct{}{}, err }) } - -func buildEthereumSignedMessage(msg []byte) []byte { - return crypto.Keccak256(append([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(msg))), msg...)) -} diff --git a/timeboost/db.go b/timeboost/db.go index d5825166d6..8d71a510a8 100644 --- a/timeboost/db.go +++ b/timeboost/db.go @@ -129,6 +129,36 @@ func (d *SqliteDatabase) InsertBid(b *ValidatedBid) error { return nil } +func (d *SqliteDatabase) GetBids(maxDbRows int) ([]*SqliteDatabaseBid, uint64, error) { + d.lock.Lock() + defer d.lock.Unlock() + var maxRound uint64 + query := `SELECT MAX(Round) FROM Bids` + err := d.sqlDB.Get(&maxRound, query) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch maxRound from bids: %w", err) + } + var sqlDBbids []*SqliteDatabaseBid + if maxDbRows == 0 { + if err := d.sqlDB.Select(&sqlDBbids, "SELECT * FROM Bids WHERE Round < ? ORDER BY Round ASC", maxRound); err != nil { + return nil, 0, err + } + return sqlDBbids, maxRound, nil + } + if err := d.sqlDB.Select(&sqlDBbids, "SELECT * FROM Bids WHERE Round < ? ORDER BY Round ASC LIMIT ?", maxRound, maxDbRows); err != nil { + return nil, 0, err + } + // We should return contiguous set of bids + for i := len(sqlDBbids) - 1; i > 0; i-- { + if sqlDBbids[i].Round != sqlDBbids[i-1].Round { + return sqlDBbids[:i], sqlDBbids[i].Round, nil + } + } + // If we can't determine a contiguous set of bids, we abort and retry again. + // Saves us from cases where we sometime push same batch data twice + return nil, 0, nil +} + func (d *SqliteDatabase) DeleteBids(round uint64) error { d.lock.Lock() defer d.lock.Unlock() diff --git a/timeboost/db_test.go b/timeboost/db_test.go index a193cdaf8f..7bfae9c61a 100644 --- a/timeboost/db_test.go +++ b/timeboost/db_test.go @@ -6,24 +6,15 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/ethereum/go-ethereum/common" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" ) func TestInsertAndFetchBids(t *testing.T) { t.Parallel() - type DatabaseBid struct { - Id uint64 `db:"Id"` - ChainId string `db:"ChainId"` - Bidder string `db:"Bidder"` - ExpressLaneController string `db:"ExpressLaneController"` - AuctionContractAddress string `db:"AuctionContractAddress"` - Round uint64 `db:"Round"` - Amount string `db:"Amount"` - Signature string `db:"Signature"` - } tmpDir, err := os.MkdirTemp("", "*") require.NoError(t, err) @@ -56,7 +47,7 @@ func TestInsertAndFetchBids(t *testing.T) { for _, bid := range bids { require.NoError(t, db.InsertBid(bid)) } - gotBids := make([]*DatabaseBid, 2) + gotBids := make([]*SqliteDatabaseBid, 2) err = db.sqlDB.Select(&gotBids, "SELECT * FROM Bids ORDER BY Id") require.NoError(t, err) require.Equal(t, bids[0].Amount.String(), gotBids[0].Amount) diff --git a/timeboost/roundtiminginfo.go b/timeboost/roundtiminginfo.go new file mode 100644 index 0000000000..037b44e4cf --- /dev/null +++ b/timeboost/roundtiminginfo.go @@ -0,0 +1,133 @@ +// Copyright 2024-2025, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package timeboost + +import ( + "fmt" + "time" + + "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" + "github.com/offchainlabs/nitro/util/arbmath" +) + +// Validate the express_lane_auctiongen.RoundTimingInfo fields. +// Returns errors in terms of the solidity field names to ease debugging. +func validateRoundTimingInfo(c *express_lane_auctiongen.RoundTimingInfo) error { + roundDuration := arbmath.SaturatingCast[time.Duration](c.RoundDurationSeconds) * time.Second + auctionClosing := arbmath.SaturatingCast[time.Duration](c.AuctionClosingSeconds) * time.Second + reserveSubmission := arbmath.SaturatingCast[time.Duration](c.ReserveSubmissionSeconds) * time.Second + + // Validate minimum durations + if roundDuration < time.Second*10 { + return fmt.Errorf("RoundDurationSeconds (%d) must be at least 10 seconds", c.RoundDurationSeconds) + } + + if auctionClosing < time.Second*5 { + return fmt.Errorf("AuctionClosingSeconds (%d) must be at least 5 seconds", c.AuctionClosingSeconds) + } + + if reserveSubmission < time.Second { + return fmt.Errorf("ReserveSubmissionSeconds (%d) must be at least 1 second", c.ReserveSubmissionSeconds) + } + + // Validate combined auction closing and reserve submission against round duration + combinedClosingTime := auctionClosing + reserveSubmission + if roundDuration <= combinedClosingTime { + return fmt.Errorf("RoundDurationSeconds (%d) must be greater than AuctionClosingSeconds (%d) + ReserveSubmissionSeconds (%d) = %d", + c.RoundDurationSeconds, + c.AuctionClosingSeconds, + c.ReserveSubmissionSeconds, + combinedClosingTime/time.Second) + } + + return nil +} + +// RoundTimingInfo holds the information from the Solidity type of the same name, +// validated and converted into higher level time types, with helpful methods +// for calculating round number, if a round is closed, and time til close. +type RoundTimingInfo struct { + Offset time.Time + Round time.Duration + AuctionClosing time.Duration + ReserveSubmission time.Duration +} + +// Convert from solgen bindings to domain type +func NewRoundTimingInfo(c express_lane_auctiongen.RoundTimingInfo) (*RoundTimingInfo, error) { + if err := validateRoundTimingInfo(&c); err != nil { + return nil, err + } + + return &RoundTimingInfo{ + Offset: time.Unix(c.OffsetTimestamp, 0), + Round: arbmath.SaturatingCast[time.Duration](c.RoundDurationSeconds) * time.Second, + AuctionClosing: arbmath.SaturatingCast[time.Duration](c.AuctionClosingSeconds) * time.Second, + ReserveSubmission: arbmath.SaturatingCast[time.Duration](c.ReserveSubmissionSeconds) * time.Second, + }, nil +} + +// resolutionWaitTime is an additional parameter that the Auctioneer +// needs to validate against other timing fields. +func (info *RoundTimingInfo) ValidateResolutionWaitTime(resolutionWaitTime time.Duration) error { + // Resolution wait time shouldn't be more than 50% of auction closing time + if resolutionWaitTime > info.AuctionClosing/2 { + return fmt.Errorf("resolution wait time (%v) must not exceed 50%% of auction closing time (%v)", + resolutionWaitTime, info.AuctionClosing) + } + return nil +} + +// RoundNumber returns the round number as of now. +func (info *RoundTimingInfo) RoundNumber() uint64 { + return info.RoundNumberAt(time.Now()) +} + +// RoundNumberAt returns the round number as of some timestamp. +func (info *RoundTimingInfo) RoundNumberAt(currentTime time.Time) uint64 { + return arbmath.SaturatingUCast[uint64](currentTime.Sub(info.Offset) / info.Round) + // info.Round has already been validated to be nonzero during construction. +} + +// TimeTilNextRound returns the time til the next round as of now. +func (info *RoundTimingInfo) TimeTilNextRound() time.Duration { + return info.TimeTilNextRoundAt(time.Now()) +} + +// TimeTilNextRoundAt returns the time til the next round, +// where the next round is determined from the timestamp passed in. +func (info *RoundTimingInfo) TimeTilNextRoundAt(currentTime time.Time) time.Duration { + return info.TimeOfNextRoundAt(currentTime).Sub(currentTime) +} + +func (info *RoundTimingInfo) TimeOfNextRound() time.Time { + return info.TimeOfNextRoundAt(time.Now()) +} + +func (info *RoundTimingInfo) TimeOfNextRoundAt(currentTime time.Time) time.Time { + roundNum := info.RoundNumberAt(currentTime) + return info.Offset.Add(info.Round * arbmath.SaturatingCast[time.Duration](roundNum+1)) +} + +func (info *RoundTimingInfo) durationIntoRound(timestamp time.Time) time.Duration { + secondsSinceOffset := uint64(timestamp.Sub(info.Offset).Seconds()) + roundDurationSeconds := uint64(info.Round.Seconds()) + return arbmath.SaturatingCast[time.Duration](secondsSinceOffset % roundDurationSeconds) +} + +func (info *RoundTimingInfo) isAuctionRoundClosed() bool { + return info.isAuctionRoundClosedAt(time.Now()) +} + +func (info *RoundTimingInfo) isAuctionRoundClosedAt(currentTime time.Time) bool { + if currentTime.Before(info.Offset) { + return false + } + + return info.durationIntoRound(currentTime)*time.Second >= info.Round-info.AuctionClosing +} + +func (info *RoundTimingInfo) IsWithinAuctionCloseWindow(timestamp time.Time) bool { + return info.TimeTilNextRoundAt(timestamp) <= info.AuctionClosing +} diff --git a/timeboost/s3_storage.go b/timeboost/s3_storage.go new file mode 100644 index 0000000000..3235fa844b --- /dev/null +++ b/timeboost/s3_storage.go @@ -0,0 +1,247 @@ +package timeboost + +import ( + "bytes" + "context" + "encoding/csv" + "fmt" + "strconv" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/util/gzip" + "github.com/offchainlabs/nitro/util/s3client" + "github.com/offchainlabs/nitro/util/stopwaiter" +) + +type S3StorageServiceConfig struct { + Enable bool `koanf:"enable"` + AccessKey string `koanf:"access-key"` + Bucket string `koanf:"bucket"` + ObjectPrefix string `koanf:"object-prefix"` + Region string `koanf:"region"` + SecretKey string `koanf:"secret-key"` + UploadInterval time.Duration `koanf:"upload-interval"` + MaxBatchSize int `koanf:"max-batch-size"` + MaxDbRows int `koanf:"max-db-rows"` +} + +func (c *S3StorageServiceConfig) Validate() error { + if !c.Enable { + return nil + } + if c.MaxBatchSize < 0 { + return fmt.Errorf("invalid max-batch-size value for auctioneer's s3-storage config, it should be non-negative, got: %d", c.MaxBatchSize) + } + if c.MaxDbRows < 0 { + return fmt.Errorf("invalid max-db-rows value for auctioneer's s3-storage config, it should be non-negative, got: %d", c.MaxDbRows) + } + return nil +} + +var DefaultS3StorageServiceConfig = S3StorageServiceConfig{ + Enable: false, + UploadInterval: 15 * time.Minute, + MaxBatchSize: 100000000, + MaxDbRows: 0, // Disabled by default +} + +func S3StorageServiceConfigAddOptions(prefix string, f *pflag.FlagSet) { + f.Bool(prefix+".enable", DefaultS3StorageServiceConfig.Enable, "enable persisting of valdiated bids to AWS S3 bucket") + f.String(prefix+".access-key", DefaultS3StorageServiceConfig.AccessKey, "S3 access key") + f.String(prefix+".bucket", DefaultS3StorageServiceConfig.Bucket, "S3 bucket") + f.String(prefix+".object-prefix", DefaultS3StorageServiceConfig.ObjectPrefix, "prefix to add to S3 objects") + f.String(prefix+".region", DefaultS3StorageServiceConfig.Region, "S3 region") + f.String(prefix+".secret-key", DefaultS3StorageServiceConfig.SecretKey, "S3 secret key") + f.Duration(prefix+".upload-interval", DefaultS3StorageServiceConfig.UploadInterval, "frequency at which batches are uploaded to S3") + f.Int(prefix+".max-batch-size", DefaultS3StorageServiceConfig.MaxBatchSize, "max size of uncompressed batch in bytes to be uploaded to S3") + f.Int(prefix+".max-db-rows", DefaultS3StorageServiceConfig.MaxDbRows, "when the sql db is very large, this enables reading of db in chunks instead of all at once which might cause OOM") +} + +type S3StorageService struct { + stopwaiter.StopWaiter + config *S3StorageServiceConfig + client s3client.FullClient + sqlDB *SqliteDatabase + bucket string + objectPrefix string + lastFailedDeleteRound uint64 +} + +func NewS3StorageService(config *S3StorageServiceConfig, sqlDB *SqliteDatabase) (*S3StorageService, error) { + client, err := s3client.NewS3FullClient(config.AccessKey, config.SecretKey, config.Region) + if err != nil { + return nil, err + } + return &S3StorageService{ + config: config, + client: client, + sqlDB: sqlDB, + bucket: config.Bucket, + objectPrefix: config.ObjectPrefix, + }, nil +} + +func (s *S3StorageService) Start(ctx context.Context) { + s.StopWaiter.Start(ctx, s) + if err := s.LaunchThreadSafe(func(ctx context.Context) { + ticker := time.NewTicker(s.config.UploadInterval) + defer ticker.Stop() + for { + interval := s.uploadBatches(ctx) + if ctx.Err() != nil { + return + } + if interval != s.config.UploadInterval { // Indicates error case, so we'll retry sooner than upload-interval + time.Sleep(interval) + continue + } + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } + }); err != nil { + log.Error("Failed to launch s3-storage service of auctioneer", "err", err) + } +} + +// Used in padding round numbers to a fixed length for naming the batch being uploaded to s3. - +const fixedRoundStrLen = 7 + +func (s *S3StorageService) getBatchName(firstRound, lastRound uint64) string { + padder := "%0" + strconv.Itoa(fixedRoundStrLen) + "d" + now := time.Now() + return fmt.Sprintf("%svalidated-timeboost-bids/%d/%02d/%02d/"+padder+"-"+padder+".csv.gzip", s.objectPrefix, now.Year(), now.Month(), now.Day(), firstRound, lastRound) +} +func (s *S3StorageService) uploadBatch(ctx context.Context, batch []byte, firstRound, lastRound uint64) error { + compressedData, err := gzip.CompressGzip(batch) + if err != nil { + return err + } + key := s.getBatchName(firstRound, lastRound) + putObjectInput := s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(key), + Body: bytes.NewReader(compressedData), + } + if _, err = s.client.Upload(ctx, &putObjectInput); err != nil { + return err + } + return nil +} + +// downloadBatch is only used for testing purposes +func (s *S3StorageService) downloadBatch(ctx context.Context, key string) ([]byte, error) { + buf := manager.NewWriteAtBuffer([]byte{}) + if _, err := s.client.Download(ctx, buf, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(key), + }); err != nil { + return nil, err + } + return gzip.DecompressGzip(buf.Bytes()) +} + +func csvRecordSize(record []string) int { + size := len(record) // comma between fields + newline + for _, entry := range record { + size += len(entry) + } + return size +} + +func (s *S3StorageService) uploadBatches(ctx context.Context) time.Duration { + // Before doing anything first try to delete the previously uploaded bids that were not successfully erased from the sqlDB + if s.lastFailedDeleteRound != 0 { + if err := s.sqlDB.DeleteBids(s.lastFailedDeleteRound); err != nil { + log.Error("error deleting s3-persisted bids from sql db using lastFailedDeleteRound", "lastFailedDeleteRound", s.lastFailedDeleteRound, "err", err) + return 5 * time.Second + } + s.lastFailedDeleteRound = 0 + } + + bids, round, err := s.sqlDB.GetBids(s.config.MaxDbRows) + if err != nil { + log.Error("Error fetching validated bids from sql DB", "round", round, "err", err) + return 5 * time.Second + } + // Nothing to persist or a contiguous set of bids wasn't found, so exit early + if len(bids) == 0 { + return s.config.UploadInterval + } + + var csvBuffer bytes.Buffer + var size int + var firstBidId int + csvWriter := csv.NewWriter(&csvBuffer) + uploadAndDeleteBids := func(firstRound, lastRound, deletRound uint64) error { + // End current batch when size exceeds MaxBatchSize and the current round ends + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + log.Error("Error flushing csv writer", "err", err) + return err + } + if err := s.uploadBatch(ctx, csvBuffer.Bytes(), firstRound, lastRound); err != nil { + log.Error("Error uploading batch to s3", "firstRound", firstRound, "lastRound", lastRound, "err", err) + return err + } + // After successful upload we should go ahead and delete the uploaded bids from DB to prevent duplicate uploads + // If the delete fails, we track the deleteRound until a future delete succeeds. + if err := s.sqlDB.DeleteBids(deletRound); err != nil { + log.Error("error deleting s3-persisted bids from sql db", "round", deletRound, "err", err) + s.lastFailedDeleteRound = deletRound + } else { + // Previously failed deletes dont matter anymore as the recent one (larger round number) succeeded + s.lastFailedDeleteRound = 0 + } + return nil + } + + header := []string{"ChainID", "Bidder", "ExpressLaneController", "AuctionContractAddress", "Round", "Amount", "Signature"} + if err := csvWriter.Write(header); err != nil { + log.Error("Error writing to csv writer", "err", err) + return 5 * time.Second + } + for index, bid := range bids { + record := []string{bid.ChainId, bid.Bidder, bid.ExpressLaneController, bid.AuctionContractAddress, fmt.Sprintf("%d", bid.Round), bid.Amount, bid.Signature} + if err := csvWriter.Write(record); err != nil { + log.Error("Error writing to csv writer", "err", err) + return 5 * time.Second + } + if s.config.MaxBatchSize != 0 { + size += csvRecordSize(record) + if size >= s.config.MaxBatchSize && index < len(bids)-1 && bid.Round != bids[index+1].Round { + if uploadAndDeleteBids(bids[firstBidId].Round, bid.Round, bids[index+1].Round) != nil { + return 5 * time.Second + } + // Reset csv for next batch + csvBuffer.Reset() + if err := csvWriter.Write(header); err != nil { + log.Error("Error writing to csv writer", "err", err) + return 5 * time.Second + } + size = 0 + firstBidId = index + 1 + } + } + } + if s.config.MaxBatchSize == 0 || size > 0 { + if uploadAndDeleteBids(bids[firstBidId].Round, bids[len(bids)-1].Round, round) != nil { + return 5 * time.Second + } + } + + if s.lastFailedDeleteRound != 0 { + return 5 * time.Second + } + + return s.config.UploadInterval +} diff --git a/timeboost/s3_storage_test.go b/timeboost/s3_storage_test.go new file mode 100644 index 0000000000..ae2d9a0c19 --- /dev/null +++ b/timeboost/s3_storage_test.go @@ -0,0 +1,238 @@ +package timeboost + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "math/big" + "testing" + + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" +) + +type mockS3FullClient struct { + data map[string][]byte +} + +func newmockS3FullClient() *mockS3FullClient { + return &mockS3FullClient{make(map[string][]byte)} +} + +func (m *mockS3FullClient) clear() { + m.data = make(map[string][]byte) +} + +func (m *mockS3FullClient) Client() *s3.Client { + return nil +} + +func (m *mockS3FullClient) Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) { + buf := new(bytes.Buffer) + _, err := buf.ReadFrom(input.Body) + if err != nil { + return nil, err + } + m.data[*input.Key] = buf.Bytes() + return nil, nil +} + +func (m *mockS3FullClient) Download(ctx context.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*manager.Downloader)) (n int64, err error) { + if _, ok := m.data[*input.Key]; ok { + ret, err := w.WriteAt(m.data[*input.Key], 0) + if err != nil { + return 0, err + } + return int64(ret), nil + } + return 0, errors.New("key not found") +} + +func TestS3StorageServiceUploadAndDownload(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mockClient := newmockS3FullClient() + s3StorageService := &S3StorageService{ + client: mockClient, + config: &S3StorageServiceConfig{MaxBatchSize: 0}, + } + + // Test upload and download of data + testData := []byte{1, 2, 3, 4} + require.NoError(t, s3StorageService.uploadBatch(ctx, testData, 10, 11)) + key := s3StorageService.getBatchName(10, 11) + gotData, err := s3StorageService.downloadBatch(ctx, key) + require.NoError(t, err) + require.Equal(t, testData, gotData) + + // Test interaction with sqlDB and upload of multiple batches + mockClient.clear() + db, err := NewDatabase(t.TempDir()) + require.NoError(t, err) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000001"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000003"), + Round: 0, + Amount: big.NewInt(10), + Signature: []byte("signature0"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(1), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000001"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000003"), + Round: 1, + Amount: big.NewInt(100), + Signature: []byte("signature1"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000004"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000005"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000006"), + Round: 2, + Amount: big.NewInt(200), + Signature: []byte("signature2"), + })) + s3StorageService.sqlDB = db + + // Helper functions to verify correctness of batch uploads and + // Check if all the uploaded bids are removed from sql DB + verifyBatchUploadCorrectness := func(firstRound, lastRound uint64, wantBatch []byte) { + key = s3StorageService.getBatchName(firstRound, lastRound) + data, err := s3StorageService.downloadBatch(ctx, key) + require.NoError(t, err) + require.Equal(t, wantBatch, data) + } + var sqlDBbids []*SqliteDatabaseBid + checkUploadedBidsRemoval := func(remainingRound uint64) { + require.NoError(t, db.sqlDB.Select(&sqlDBbids, "SELECT * FROM Bids")) + require.Equal(t, 1, len(sqlDBbids)) + require.Equal(t, remainingRound, sqlDBbids[0].Round) + } + + // UploadBatches should upload only the first bid and only one bid (round = 2) should remain in the sql database + s3StorageService.uploadBatches(ctx) + verifyBatchUploadCorrectness(0, 1, []byte(`ChainID,Bidder,ExpressLaneController,AuctionContractAddress,Round,Amount,Signature +2,0x0000000000000000000000000000000000000003,0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002,0,10,signature0 +1,0x0000000000000000000000000000000000000003,0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002,1,100,signature1 +`)) + checkUploadedBidsRemoval(2) + + // UploadBatches should continue adding bids to the batch until round ends, even if its past MaxBatchSize + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(1), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000007"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000008"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000009"), + Round: 2, + Amount: big.NewInt(150), + Signature: []byte("signature3"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000001"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000003"), + Round: 3, + Amount: big.NewInt(250), + Signature: []byte("signature4"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000004"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000005"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000006"), + Round: 4, + Amount: big.NewInt(350), + Signature: []byte("signature5"), + })) + record := []string{sqlDBbids[0].ChainId, sqlDBbids[0].Bidder, sqlDBbids[0].ExpressLaneController, sqlDBbids[0].AuctionContractAddress, fmt.Sprintf("%d", sqlDBbids[0].Round), sqlDBbids[0].Amount, sqlDBbids[0].Signature} + s3StorageService.config.MaxBatchSize = csvRecordSize(record) + + // Round 2 bids should all be in the same batch even though the resulting batch exceeds MaxBatchSize + s3StorageService.uploadBatches(ctx) + verifyBatchUploadCorrectness(2, 2, []byte(`ChainID,Bidder,ExpressLaneController,AuctionContractAddress,Round,Amount,Signature +2,0x0000000000000000000000000000000000000006,0x0000000000000000000000000000000000000004,0x0000000000000000000000000000000000000005,2,200,signature2 +1,0x0000000000000000000000000000000000000009,0x0000000000000000000000000000000000000007,0x0000000000000000000000000000000000000008,2,150,signature3 +`)) + + // After Batching Round 2 bids we end that batch and create a new batch for Round 3 bids to adhere to MaxBatchSize rule + s3StorageService.uploadBatches(ctx) + verifyBatchUploadCorrectness(3, 3, []byte(`ChainID,Bidder,ExpressLaneController,AuctionContractAddress,Round,Amount,Signature +2,0x0000000000000000000000000000000000000003,0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002,3,250,signature4 +`)) + checkUploadedBidsRemoval(4) + + // Verify chunked reading of sql db + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(1), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000007"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000008"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000009"), + Round: 4, + Amount: big.NewInt(450), + Signature: []byte("signature6"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000001"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000003"), + Round: 5, + Amount: big.NewInt(550), + Signature: []byte("signature7"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000004"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000005"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000006"), + Round: 5, + Amount: big.NewInt(650), + Signature: []byte("signature8"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(2), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000004"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000005"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000006"), + Round: 6, + Amount: big.NewInt(750), + Signature: []byte("signature9"), + })) + require.NoError(t, db.InsertBid(&ValidatedBid{ + ChainId: big.NewInt(1), + ExpressLaneController: common.HexToAddress("0x0000000000000000000000000000000000000004"), + AuctionContractAddress: common.HexToAddress("0x0000000000000000000000000000000000000005"), + Bidder: common.HexToAddress("0x0000000000000000000000000000000000000006"), + Round: 7, + Amount: big.NewInt(850), + Signature: []byte("signature10"), + })) + s3StorageService.config.MaxDbRows = 5 + + // Since config.MaxBatchSize is kept same and config.MaxDbRows is 5, sqldb.GetBids would return all bids from round 4 and 5, with round used for DeletBids as 6 + // maxBatchSize would then batch bids from round 4 & 5 separately and uploads them to s3 + s3StorageService.uploadBatches(ctx) + verifyBatchUploadCorrectness(4, 4, []byte(`ChainID,Bidder,ExpressLaneController,AuctionContractAddress,Round,Amount,Signature +2,0x0000000000000000000000000000000000000006,0x0000000000000000000000000000000000000004,0x0000000000000000000000000000000000000005,4,350,signature5 +1,0x0000000000000000000000000000000000000009,0x0000000000000000000000000000000000000007,0x0000000000000000000000000000000000000008,4,450,signature6 +`)) + verifyBatchUploadCorrectness(5, 5, []byte(`ChainID,Bidder,ExpressLaneController,AuctionContractAddress,Round,Amount,Signature +2,0x0000000000000000000000000000000000000003,0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002,5,550,signature7 +2,0x0000000000000000000000000000000000000006,0x0000000000000000000000000000000000000004,0x0000000000000000000000000000000000000005,5,650,signature8 +`)) + require.NoError(t, db.sqlDB.Select(&sqlDBbids, "SELECT * FROM Bids ORDER BY Round ASC")) + require.Equal(t, 2, len(sqlDBbids)) + require.Equal(t, uint64(6), sqlDBbids[0].Round) + require.Equal(t, uint64(7), sqlDBbids[1].Round) +} diff --git a/timeboost/schema.go b/timeboost/schema.go index 94fc04d1f1..68a70aac63 100644 --- a/timeboost/schema.go +++ b/timeboost/schema.go @@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS Bids ( Amount TEXT NOT NULL, Signature TEXT NOT NULL ); +CREATE INDEX idx_bids_round ON Bids(Round); ` schemaList = []string{version1} ) diff --git a/timeboost/setup_test.go b/timeboost/setup_test.go index 9a603d4d7b..c093ab2d67 100644 --- a/timeboost/setup_test.go +++ b/timeboost/setup_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -15,11 +17,11 @@ import ( "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethclient/simulated" "github.com/ethereum/go-ethereum/node" + "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/solgen/go/express_lane_auctiongen" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/timeboost/bindings" - "github.com/stretchr/testify/require" ) type auctionSetup struct { @@ -120,7 +122,7 @@ func setupAuctionTest(t testing.TB, ctx context.Context) *auctionSetup { BiddingToken: biddingToken, Beneficiary: beneficiary, RoundTimingInfo: express_lane_auctiongen.RoundTimingInfo{ - OffsetTimestamp: initialTimestamp.Uint64(), + OffsetTimestamp: initialTimestamp.Int64(), RoundDurationSeconds: bidRoundSeconds, AuctionClosingSeconds: auctionClosingSeconds, ReserveSubmissionSeconds: reserveSubmissionSeconds, diff --git a/timeboost/ticker.go b/timeboost/ticker.go index 45e6ecef11..f9bfc18ed4 100644 --- a/timeboost/ticker.go +++ b/timeboost/ticker.go @@ -4,39 +4,37 @@ import ( "time" ) -type auctionCloseTicker struct { - c chan time.Time - done chan bool - roundDuration time.Duration - auctionClosingDuration time.Duration +type roundTicker struct { + c chan time.Time + done chan bool + roundTimingInfo RoundTimingInfo } -func newAuctionCloseTicker(roundDuration, auctionClosingDuration time.Duration) *auctionCloseTicker { - return &auctionCloseTicker{ - c: make(chan time.Time, 1), - done: make(chan bool), - roundDuration: roundDuration, - auctionClosingDuration: auctionClosingDuration, +func newRoundTicker(roundTimingInfo RoundTimingInfo) *roundTicker { + return &roundTicker{ + c: make(chan time.Time, 1), + done: make(chan bool), + roundTimingInfo: roundTimingInfo, } } -func (t *auctionCloseTicker) start() { +func (t *roundTicker) tickAtAuctionClose() { + t.start(t.roundTimingInfo.AuctionClosing) +} + +func (t *roundTicker) tickAtReserveSubmissionDeadline() { + t.start(t.roundTimingInfo.AuctionClosing + t.roundTimingInfo.ReserveSubmission) +} + +func (t *roundTicker) start(timeBeforeRoundStart time.Duration) { for { - now := time.Now() - // Calculate the start of the next round - startOfNextMinute := now.Truncate(t.roundDuration).Add(t.roundDuration) - // Subtract AUCTION_CLOSING_SECONDS seconds to get the tick time - nextTickTime := startOfNextMinute.Add(-t.auctionClosingDuration) - // Ensure we are not setting a past tick time - if nextTickTime.Before(now) { - // If the calculated tick time is in the past, move to the next interval - nextTickTime = nextTickTime.Add(t.roundDuration) + nextTick := t.roundTimingInfo.TimeTilNextRound() - timeBeforeRoundStart + if nextTick < 0 { + nextTick += t.roundTimingInfo.Round } - // Calculate how long to wait until the next tick - waitTime := nextTickTime.Sub(now) select { - case <-time.After(waitTime): + case <-time.After(nextTick): t.c <- time.Now() case <-t.done: close(t.c) @@ -44,34 +42,3 @@ func (t *auctionCloseTicker) start() { } } } - -// CurrentRound returns the current round number. -func CurrentRound(initialRoundTimestamp time.Time, roundDuration time.Duration) uint64 { - if roundDuration == 0 { - return 0 - } - return uint64(time.Since(initialRoundTimestamp) / roundDuration) -} - -func isAuctionRoundClosed( - timestamp time.Time, - initialTimestamp time.Time, - roundDuration time.Duration, - auctionClosingDuration time.Duration, -) bool { - if timestamp.Before(initialTimestamp) { - return false - } - timeInRound := timeIntoRound(timestamp, initialTimestamp, roundDuration) - return time.Duration(timeInRound)*time.Second >= roundDuration-auctionClosingDuration -} - -func timeIntoRound( - timestamp time.Time, - initialTimestamp time.Time, - roundDuration time.Duration, -) uint64 { - secondsSinceOffset := uint64(timestamp.Sub(initialTimestamp).Seconds()) - roundDurationSeconds := uint64(roundDuration.Seconds()) - return secondsSinceOffset % roundDurationSeconds -} diff --git a/timeboost/ticker_test.go b/timeboost/ticker_test.go index b1ee996bc0..f284ba56ae 100644 --- a/timeboost/ticker_test.go +++ b/timeboost/ticker_test.go @@ -9,32 +9,34 @@ import ( func Test_auctionClosed(t *testing.T) { t.Parallel() - roundDuration := time.Minute - auctionClosingDuration := time.Second * 15 - now := time.Now() - waitTime := roundDuration - time.Duration(now.Second())*time.Second - time.Duration(now.Nanosecond()) - initialTimestamp := now.Add(waitTime) + roundTimingInfo := RoundTimingInfo{ + Offset: time.Now(), + Round: time.Minute, + AuctionClosing: time.Second * 15, + } + + initialTimestamp := time.Now() // We should not have closed the round yet, and the time into the round should be less than a second. - isClosed := isAuctionRoundClosed(initialTimestamp, initialTimestamp, roundDuration, auctionClosingDuration) + isClosed := roundTimingInfo.isAuctionRoundClosedAt(initialTimestamp) require.False(t, isClosed) // Wait right before auction closure (before the 45 second mark). - timestamp := initialTimestamp.Add((roundDuration - auctionClosingDuration) - time.Second) - isClosed = isAuctionRoundClosed(timestamp, initialTimestamp, roundDuration, auctionClosingDuration) + timestamp := initialTimestamp.Add((roundTimingInfo.Round - roundTimingInfo.AuctionClosing) - time.Second) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(timestamp) require.False(t, isClosed) // Wait a second more and the auction should be closed. - timestamp = initialTimestamp.Add(roundDuration - auctionClosingDuration) - isClosed = isAuctionRoundClosed(timestamp, initialTimestamp, roundDuration, auctionClosingDuration) + timestamp = initialTimestamp.Add(roundTimingInfo.Round - roundTimingInfo.AuctionClosing) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(timestamp) require.True(t, isClosed) // Future timestamp should also be closed, until we reach the new round - for i := float64(0); i < auctionClosingDuration.Seconds(); i++ { - timestamp = initialTimestamp.Add((roundDuration - auctionClosingDuration) + time.Second*time.Duration(i)) - isClosed = isAuctionRoundClosed(timestamp, initialTimestamp, roundDuration, auctionClosingDuration) + for i := float64(0); i < roundTimingInfo.AuctionClosing.Seconds(); i++ { + timestamp = initialTimestamp.Add((roundTimingInfo.Round - roundTimingInfo.AuctionClosing) + time.Second*time.Duration(i)) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(timestamp) require.True(t, isClosed) } - isClosed = isAuctionRoundClosed(initialTimestamp.Add(roundDuration), initialTimestamp, roundDuration, auctionClosingDuration) + isClosed = roundTimingInfo.isAuctionRoundClosedAt(initialTimestamp.Add(roundTimingInfo.Round)) require.False(t, isClosed) } diff --git a/timeboost/types.go b/timeboost/types.go index 428027730a..73e2e0d2b6 100644 --- a/timeboost/types.go +++ b/timeboost/types.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" ) type Bid struct { @@ -33,19 +34,40 @@ func (b *Bid) ToJson() *JsonBid { } } -func (b *Bid) ToMessageBytes() []byte { - buf := new(bytes.Buffer) - // Encode uint256 values - each occupies 32 bytes - buf.Write(domainValue) - buf.Write(padBigInt(b.ChainId)) - buf.Write(b.AuctionContractAddress[:]) - roundBuf := make([]byte, 8) - binary.BigEndian.PutUint64(roundBuf, b.Round) - buf.Write(roundBuf) - buf.Write(padBigInt(b.Amount)) - buf.Write(b.ExpressLaneController[:]) +func (b *Bid) ToEIP712Hash(domainSeparator [32]byte) (common.Hash, error) { + types := apitypes.Types{ + "Bid": []apitypes.Type{ + {Name: "round", Type: "uint64"}, + {Name: "expressLaneController", Type: "address"}, + {Name: "amount", Type: "uint256"}, + }, + } + + message := apitypes.TypedDataMessage{ + "round": big.NewInt(0).SetUint64(b.Round), + "expressLaneController": [20]byte(b.ExpressLaneController), + "amount": b.Amount, + } + + typedData := apitypes.TypedData{ + Types: types, + PrimaryType: "Bid", + Message: message, + Domain: apitypes.TypedDataDomain{Salt: "Unused; domain separator fetched from method on contract. This must be nonempty for validation."}, + } + + messageHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return common.Hash{}, err + } + + bidHash := crypto.Keccak256Hash( + []byte("\x19\x01"), + domainSeparator[:], + messageHash, + ) - return buf.Bytes() + return bidHash, nil } type JsonBid struct { @@ -72,17 +94,20 @@ type ValidatedBid struct { // The hash is equivalent to the following Solidity implementation: // // uint256(keccak256(abi.encodePacked(bidder, bidBytes))) -func (v *ValidatedBid) BigIntHash() *big.Int { - bidBytes := v.BidBytes() +// +// This is only used for breaking ties amongst equivalent bids and not used for +// Bid signing, which uses EIP 712 as the hashing scheme. +func (v *ValidatedBid) bigIntHash() *big.Int { + bidBytes := v.bidBytes() bidder := v.Bidder.Bytes() return new(big.Int).SetBytes(crypto.Keccak256Hash(bidder, bidBytes).Bytes()) } -// BidBytes returns the byte representation equivalent to the Solidity implementation of +// bidBytes returns the byte representation equivalent to the Solidity implementation of // // abi.encodePacked(BID_DOMAIN, block.chainid, address(this), _round, _amount, _expressLaneController) -func (v *ValidatedBid) BidBytes() []byte { +func (v *ValidatedBid) bidBytes() []byte { var buffer bytes.Buffer buffer.Write(domainValue) @@ -139,7 +164,7 @@ type JsonExpressLaneSubmission struct { AuctionContractAddress common.Address `json:"auctionContractAddress"` Transaction hexutil.Bytes `json:"transaction"` Options *arbitrum_types.ConditionalOptions `json:"options"` - Sequence hexutil.Uint64 + SequenceNumber hexutil.Uint64 Signature hexutil.Bytes `json:"signature"` } @@ -149,7 +174,7 @@ type ExpressLaneSubmission struct { AuctionContractAddress common.Address Transaction *types.Transaction Options *arbitrum_types.ConditionalOptions `json:"options"` - Sequence uint64 + SequenceNumber uint64 Signature []byte } @@ -164,7 +189,7 @@ func JsonSubmissionToGo(submission *JsonExpressLaneSubmission) (*ExpressLaneSubm AuctionContractAddress: submission.AuctionContractAddress, Transaction: tx, Options: submission.Options, - Sequence: uint64(submission.Sequence), + SequenceNumber: uint64(submission.SequenceNumber), Signature: submission.Signature, }, nil } @@ -180,7 +205,7 @@ func (els *ExpressLaneSubmission) ToJson() (*JsonExpressLaneSubmission, error) { AuctionContractAddress: els.AuctionContractAddress, Transaction: encoded, Options: els.Options, - Sequence: hexutil.Uint64(els.Sequence), + SequenceNumber: hexutil.Uint64(els.SequenceNumber), Signature: els.Signature, }, nil } @@ -189,13 +214,13 @@ func (els *ExpressLaneSubmission) ToMessageBytes() ([]byte, error) { buf := new(bytes.Buffer) buf.Write(domainValue) buf.Write(padBigInt(els.ChainId)) - seqBuf := make([]byte, 8) - binary.BigEndian.PutUint64(seqBuf, els.Sequence) - buf.Write(seqBuf) buf.Write(els.AuctionContractAddress[:]) roundBuf := make([]byte, 8) binary.BigEndian.PutUint64(roundBuf, els.Round) buf.Write(roundBuf) + seqBuf := make([]byte, 8) + binary.BigEndian.PutUint64(seqBuf, els.SequenceNumber) + buf.Write(seqBuf) rlpTx, err := els.Transaction.MarshalBinary() if err != nil { return nil, err @@ -211,3 +236,14 @@ func padBigInt(bi *big.Int) []byte { padded = append(padded, bb...) return padded } + +type SqliteDatabaseBid struct { + Id uint64 `db:"Id"` + ChainId string `db:"ChainId"` + Bidder string `db:"Bidder"` + ExpressLaneController string `db:"ExpressLaneController"` + AuctionContractAddress string `db:"AuctionContractAddress"` + Round uint64 `db:"Round"` + Amount string `db:"Amount"` + Signature string `db:"Signature"` +} diff --git a/util/arbmath/bips.go b/util/arbmath/bips.go index 8b7c47d82b..39b014f3ac 100644 --- a/util/arbmath/bips.go +++ b/util/arbmath/bips.go @@ -20,36 +20,47 @@ func PercentToBips(percentage int64) Bips { } func BigToBips(natural *big.Int) Bips { - return Bips(natural.Uint64()) + return Bips(natural.Int64()) } func BigMulByBips(value *big.Int, bips Bips) *big.Int { return BigMulByFrac(value, int64(bips), int64(OneInBips)) } +func BigMulByUBips(value *big.Int, bips UBips) *big.Int { + return BigMulByUFrac(value, uint64(bips), uint64(OneInUBips)) +} + func IntMulByBips(value int64, bips Bips) int64 { return value * int64(bips) / int64(OneInBips) } +// UintMulByBips multiplies a uint value by a bips value +// bips must be positive and not cause an overflow func UintMulByBips(value uint64, bips Bips) uint64 { + // #nosec G115 return value * uint64(bips) / uint64(OneInBips) } -func SaturatingCastToBips(value uint64) Bips { - return Bips(SaturatingCast[int64](value)) -} - -func (bips UBips) Uint64() uint64 { - return uint64(bips) +// UintSaturatingMulByBips multiplies a uint value by a bips value, +// saturating at the maximum bips value (not the maximum uint64 result), +// then rounding down and returning a uint64. +// Returns 0 if bips is less than or equal to zero +func UintSaturatingMulByBips(value uint64, bips Bips) uint64 { + if bips <= 0 { + return 0 + } + // #nosec G115 + return SaturatingUMul(value, uint64(bips)) / uint64(OneInBips) } -func (bips Bips) Uint64() uint64 { - return uint64(bips) +func SaturatingCastToBips(value uint64) Bips { + return Bips(SaturatingCast[int64](value)) } // BigDivToBips returns dividend/divisor as bips, saturating if out of bounds func BigDivToBips(dividend, divisor *big.Int) Bips { value := BigMulByInt(dividend, int64(OneInBips)) value.Div(value, divisor) - return Bips(BigToUintSaturating(value)) + return Bips(BigToIntSaturating(value)) } diff --git a/util/arbmath/bits.go b/util/arbmath/bits.go index 1b91e2755a..501ef9787e 100644 --- a/util/arbmath/bits.go +++ b/util/arbmath/bits.go @@ -6,8 +6,9 @@ package arbmath import ( "encoding/binary" - "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" ) type bytes32 = common.Hash diff --git a/util/arbmath/math.go b/util/arbmath/math.go index 62af1e26e0..07a9941b65 100644 --- a/util/arbmath/math.go +++ b/util/arbmath/math.go @@ -29,6 +29,7 @@ func NextOrCurrentPowerOf2(value uint64) uint64 { // Log2ceil the log2 of the int, rounded up func Log2ceil(value uint64) uint64 { + // #nosec G115 return uint64(64 - bits.LeadingZeros64(value)) } @@ -117,6 +118,18 @@ func BigToUintSaturating(value *big.Int) uint64 { return value.Uint64() } +// BigToUintSaturating casts a huge to an int, saturating if out of bounds +func BigToIntSaturating(value *big.Int) int64 { + if !value.IsInt64() { + if value.Sign() < 0 { + return math.MinInt64 + } else { + return math.MaxInt64 + } + } + return value.Int64() +} + // BigToUintOrPanic casts a huge to a uint, panicking if out of bounds func BigToUintOrPanic(value *big.Int) uint64 { if value.Sign() < 0 { @@ -216,8 +229,8 @@ func BigMulByFrac(value *big.Int, numerator, denominator int64) *big.Int { return value } -// BigMulByUfrac multiply a huge by a rational whose components are non-negative -func BigMulByUfrac(value *big.Int, numerator, denominator uint64) *big.Int { +// BigMulByUFrac multiply a huge by a rational whose components are non-negative +func BigMulByUFrac(value *big.Int, numerator, denominator uint64) *big.Int { value = new(big.Int).Set(value) value.Mul(value, new(big.Int).SetUint64(numerator)) value.Div(value, new(big.Int).SetUint64(denominator)) @@ -260,10 +273,12 @@ func BigFloatMulByUint(multiplicand *big.Float, multiplier uint64) *big.Float { } func MaxSignedValue[T Signed]() T { + // #nosec G115 return T((uint64(1) << (8*unsafe.Sizeof(T(0)) - 1)) - 1) } func MinSignedValue[T Signed]() T { + // #nosec G115 return T(uint64(1) << ((8 * unsafe.Sizeof(T(0))) - 1)) } @@ -393,6 +408,8 @@ func ApproxExpBasisPoints(value Bips, accuracy uint64) Bips { if negative { input = -value } + // This cast is safe because input is always positive + // #nosec G115 x := uint64(input) bips := uint64(OneInBips) diff --git a/util/arbmath/math_test.go b/util/arbmath/math_test.go index 1be60dc58b..befa7813e1 100644 --- a/util/arbmath/math_test.go +++ b/util/arbmath/math_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -35,6 +36,7 @@ func TestMath(t *testing.T) { input := rand.Uint64() / 256 approx := ApproxSquareRoot(input) correct := math.Sqrt(float64(input)) + // #nosec G115 diff := int(approx) - int(correct) if diff < -1 || diff > 1 { Fail(t, "sqrt approximation off by too much", diff, input, approx, correct) @@ -43,9 +45,11 @@ func TestMath(t *testing.T) { // try the first million sqrts for i := 0; i < 1000000; i++ { + // #nosec G115 input := uint64(i) approx := ApproxSquareRoot(input) correct := math.Sqrt(float64(input)) + // #nosec G115 diff := int(approx) - int(correct) if diff < 0 || diff > 1 { Fail(t, "sqrt approximation off by too much", diff, input, approx, correct) @@ -57,6 +61,7 @@ func TestMath(t *testing.T) { input := uint64(1 << i) approx := ApproxSquareRoot(input) correct := math.Sqrt(float64(input)) + // #nosec G115 diff := int(approx) - int(correct) if diff != 0 { Fail(t, "incorrect", "2^", i, diff, approx, correct) diff --git a/util/arbmath/uint24.go b/util/arbmath/uint24.go index 818f871a23..a0c5aa27b7 100644 --- a/util/arbmath/uint24.go +++ b/util/arbmath/uint24.go @@ -9,10 +9,10 @@ import ( "math/big" ) -const MaxUint24 = 1<<24 - 1 // 16777215 - type Uint24 uint32 +const MaxUint24 = 1<<24 - 1 // 16777215 + func (value Uint24) ToBig() *big.Int { return UintToBig(uint64(value)) } @@ -26,8 +26,9 @@ func (value Uint24) ToUint64() uint64 { } func IntToUint24[T uint32 | uint64](value T) (Uint24, error) { + // #nosec G115 if value > T(MaxUint24) { - return Uint24(MaxUint24), errors.New("value out of range") + return MaxUint24, errors.New("value out of range") } return Uint24(value), nil } @@ -40,6 +41,7 @@ func BigToUint24OrPanic(value *big.Int) Uint24 { if !value.IsUint64() || value.Uint64() > MaxUint24 { panic("big.Int value exceeds the max Uint24") } + // #nosec G115 return Uint24(value.Uint64()) } diff --git a/util/blobs/blobs.go b/util/blobs/blobs.go index 405c776bad..cae9c7d30f 100644 --- a/util/blobs/blobs.go +++ b/util/blobs/blobs.go @@ -41,6 +41,7 @@ func fillBlobBits(blob []byte, data []byte) ([]byte, error) { accBits += 8 data = data[1:] } + // #nosec G115 blob[fieldElement*32] = uint8(acc & ((1 << spareBlobBits) - 1)) accBits -= spareBlobBits if accBits < 0 { @@ -88,6 +89,7 @@ func DecodeBlobs(blobs []kzg4844.Blob) ([]byte, error) { acc |= uint16(blob[fieldIndex*32]) << accBits accBits += spareBlobBits if accBits >= 8 { + // #nosec G115 rlpData = append(rlpData, uint8(acc)) acc >>= 8 accBits -= 8 @@ -116,7 +118,7 @@ func ComputeCommitmentsAndHashes(blobs []kzg4844.Blob) ([]kzg4844.Commitment, [] for i := range blobs { var err error - commitments[i], err = kzg4844.BlobToCommitment(blobs[i]) + commitments[i], err = kzg4844.BlobToCommitment(&blobs[i]) if err != nil { return nil, nil, err } @@ -133,7 +135,7 @@ func ComputeBlobProofs(blobs []kzg4844.Blob, commitments []kzg4844.Commitment) ( proofs := make([]kzg4844.Proof, len(blobs)) for i := range blobs { var err error - proofs[i], err = kzg4844.ComputeBlobProof(blobs[i], commitments[i]) + proofs[i], err = kzg4844.ComputeBlobProof(&blobs[i], commitments[i]) if err != nil { return nil, err } diff --git a/util/containers/stack.go b/util/containers/stack.go new file mode 100644 index 0000000000..ea7f31013b --- /dev/null +++ b/util/containers/stack.go @@ -0,0 +1,50 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package containers + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/log" +) + +type Stack[T any] []T + +func NewStack[T any]() *Stack[T] { + return &Stack[T]{} +} + +func (s *Stack[T]) Push(v T) { + if s == nil { + log.Warn("trying to push nil stack") + return + } + *s = append(*s, v) +} + +func (s *Stack[T]) Pop() (T, error) { + if s == nil { + var zeroVal T + return zeroVal, fmt.Errorf("trying to pop nil stack") + } + if s.Empty() { + var zeroVal T + return zeroVal, fmt.Errorf("trying to pop empty stack") + } + i := len(*s) - 1 + val := (*s)[i] + *s = (*s)[:i] + return val, nil +} + +func (s *Stack[T]) Empty() bool { + return s == nil || len(*s) == 0 +} + +func (s *Stack[T]) Len() int { + if s == nil { + return 0 + } + return len(*s) +} diff --git a/util/containers/syncmap.go b/util/containers/syncmap.go index 7952a32252..9190d81974 100644 --- a/util/containers/syncmap.go +++ b/util/containers/syncmap.go @@ -1,6 +1,9 @@ package containers -import "sync" +import ( + "fmt" + "sync" +) type SyncMap[K any, V any] struct { internal sync.Map @@ -12,7 +15,11 @@ func (m *SyncMap[K, V]) Load(key K) (V, bool) { var empty V return empty, false } - return val.(V), true + vVal, ok := val.(V) + if !ok { + panic(fmt.Sprintf("type assertion failed on %s", val)) + } + return vVal, true } func (m *SyncMap[K, V]) Store(key K, val V) { @@ -22,3 +29,17 @@ func (m *SyncMap[K, V]) Store(key K, val V) { func (m *SyncMap[K, V]) Delete(key K) { m.internal.Delete(key) } + +// Only used for testing +func (m *SyncMap[K, V]) Keys() []K { + s := make([]K, 0) + m.internal.Range(func(k, v interface{}) bool { + kKey, ok := k.(K) + if !ok { + panic(fmt.Sprintf("type assertion failed on %s", k)) + } + s = append(s, kKey) + return true + }) + return s +} diff --git a/util/contracts/address_verifier.go b/util/contracts/address_verifier.go index eb2f862210..66686e9dc8 100644 --- a/util/contracts/address_verifier.go +++ b/util/contracts/address_verifier.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" ) diff --git a/util/dbutil/dbutil.go b/util/dbutil/dbutil.go index ca0f5aaaeb..28a57025e9 100644 --- a/util/dbutil/dbutil.go +++ b/util/dbutil/dbutil.go @@ -6,13 +6,14 @@ package dbutil import ( "errors" "fmt" - "os" + "io/fs" "regexp" "github.com/cockroachdb/pebble" + "github.com/syndtr/goleveldb/leveldb" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/syndtr/goleveldb/leveldb" ) func IsErrNotFound(err error) bool { @@ -22,13 +23,15 @@ func IsErrNotFound(err error) bool { var pebbleNotExistErrorRegex = regexp.MustCompile("pebble: database .* does not exist") func isPebbleNotExistError(err error) bool { - return pebbleNotExistErrorRegex.MatchString(err.Error()) + return err != nil && pebbleNotExistErrorRegex.MatchString(err.Error()) } func isLeveldbNotExistError(err error) bool { - return os.IsNotExist(err) + return errors.Is(err, fs.ErrNotExist) } +// IsNotExistError returns true if the error is a "database not found" error. +// It must return false if err is nil. func IsNotExistError(err error) bool { return isLeveldbNotExistError(err) || isPebbleNotExistError(err) } diff --git a/util/dbutil/dbutil_test.go b/util/dbutil/dbutil_test.go index b28f8a2c23..b303bb56b6 100644 --- a/util/dbutil/dbutil_test.go +++ b/util/dbutil/dbutil_test.go @@ -28,6 +28,9 @@ func testIsNotExistError(t *testing.T, dbEngine string, isNotExist func(error) b if isNotExist(err) { t.Fatalf("Classified other error as not exist, err: %v", err) } + if isNotExist(nil) { + t.Fatal("Classified nil as not exist") + } } func TestIsNotExistError(t *testing.T) { diff --git a/util/gzip/gzip_compression.go b/util/gzip/gzip_compression.go new file mode 100644 index 0000000000..4ad069767c --- /dev/null +++ b/util/gzip/gzip_compression.go @@ -0,0 +1,34 @@ +package gzip + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" +) + +func CompressGzip(data []byte) ([]byte, error) { + var buffer bytes.Buffer + gzipWriter := gzip.NewWriter(&buffer) + if _, err := gzipWriter.Write(data); err != nil { + return nil, fmt.Errorf("failed to write to gzip writer: %w", err) + } + if err := gzipWriter.Close(); err != nil { + return nil, fmt.Errorf("failed to close gzip writer: %w", err) + } + return buffer.Bytes(), nil +} + +func DecompressGzip(data []byte) ([]byte, error) { + buffer := bytes.NewReader(data) + gzipReader, err := gzip.NewReader(buffer) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzipReader.Close() + decompressData, err := io.ReadAll(gzipReader) + if err != nil { + return nil, fmt.Errorf("failed to read decompressed data: %w", err) + } + return decompressData, nil +} diff --git a/util/gzip/gzip_compression_test.go b/util/gzip/gzip_compression_test.go new file mode 100644 index 0000000000..c55dfb68c0 --- /dev/null +++ b/util/gzip/gzip_compression_test.go @@ -0,0 +1,21 @@ +package gzip + +import ( + "bytes" + "testing" +) + +func TestCompressDecompress(t *testing.T) { + sampleData := []byte{1, 2, 3, 4} + compressedData, err := CompressGzip(sampleData) + if err != nil { + t.Fatalf("got error gzip-compressing data: %v", err) + } + gotData, err := DecompressGzip(compressedData) + if err != nil { + t.Fatalf("got error gzip-decompressing data: %v", err) + } + if !bytes.Equal(sampleData, gotData) { + t.Fatal("original data and decompression of its compression don't match") + } +} diff --git a/util/headerreader/blob_client.go b/util/headerreader/blob_client.go index 2b47a940c3..0c92ff2e85 100644 --- a/util/headerreader/blob_client.go +++ b/util/headerreader/blob_client.go @@ -15,20 +15,21 @@ import ( "path" "time" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" - "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/util/blobs" "github.com/offchainlabs/nitro/util/jsonapi" "github.com/offchainlabs/nitro/util/pretty" - - "github.com/spf13/pflag" ) type BlobClient struct { - ec arbutil.L1Interface + ec *ethclient.Client beaconUrl *url.URL secondaryBeaconUrl *url.URL httpClient *http.Client @@ -63,7 +64,7 @@ func BlobClientAddOptions(prefix string, f *pflag.FlagSet) { f.String(prefix+".authorization", DefaultBlobClientConfig.Authorization, "Value to send with the HTTP Authorization: header for Beacon REST requests, must include both scheme and scheme parameters") } -func NewBlobClient(config BlobClientConfig, ec arbutil.L1Interface) (*BlobClient, error) { +func NewBlobClient(config BlobClientConfig, ec *ethclient.Client) (*BlobClient, error) { beaconUrl, err := url.Parse(config.BeaconUrl) if err != nil { return nil, fmt.Errorf("failed to parse beacon chain URL: %w", err) @@ -191,6 +192,7 @@ func (b *BlobClient) blobSidecars(ctx context.Context, slot uint64, versionedHas rawData, err := beaconRequest[json.RawMessage](b, ctx, fmt.Sprintf("/eth/v1/beacon/blob_sidecars/%d", slot)) if err != nil || len(rawData) == 0 { // blobs are pruned after 4096 epochs (1 epoch = 32 slots), we determine if the requested slot were to be pruned by a non-archive endpoint + // #nosec G115 roughAgeOfSlot := uint64(time.Now().Unix()) - (b.genesisTime + slot*b.secondsPerSlot) if roughAgeOfSlot > b.secondsPerSlot*32*4096 { return nil, fmt.Errorf("beacon client in blobSidecars got error or empty response fetching older blobs in slot: %d, an archive endpoint is required, please refer to https://docs.arbitrum.io/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers, err: %w", slot, err) @@ -247,7 +249,7 @@ func (b *BlobClient) blobSidecars(ctx context.Context, slot uint64, versionedHas var proof kzg4844.Proof copy(proof[:], blobItem.KzgProof) - err = kzg4844.VerifyBlobProof(output[outputIdx], commitment, proof) + err = kzg4844.VerifyBlobProof(&output[outputIdx], commitment, proof) if err != nil { return nil, fmt.Errorf("failed to verify blob proof for blob at slot(%d) at index(%d), blob(%s)", slot, blobItem.Index, pretty.FirstFewChars(blobItem.Blob.String())) } diff --git a/util/headerreader/blob_client_test.go b/util/headerreader/blob_client_test.go index 9735899daa..52c22e434a 100644 --- a/util/headerreader/blob_client_test.go +++ b/util/headerreader/blob_client_test.go @@ -11,8 +11,9 @@ import ( "reflect" "testing" - "github.com/offchainlabs/nitro/util/testhelpers" "github.com/r3labs/diff/v3" + + "github.com/offchainlabs/nitro/util/testhelpers" ) func TestSaveBlobsToDisk(t *testing.T) { diff --git a/util/headerreader/header_reader.go b/util/headerreader/header_reader.go index 074d24338e..f8e3bc6cd6 100644 --- a/util/headerreader/header_reader.go +++ b/util/headerreader/header_reader.go @@ -12,15 +12,18 @@ import ( "sync" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/stopwaiter" - flag "github.com/spf13/pflag" ) // A regexp matching "execution reverted" errors returned from the parent chain RPC. @@ -33,7 +36,7 @@ type ArbSysInterface interface { type HeaderReader struct { stopwaiter.StopWaiter config ConfigFetcher - client arbutil.L1Interface + client *ethclient.Client isParentChainArbitrum bool arbSys ArbSysInterface @@ -120,7 +123,7 @@ var TestConfig = Config{ }, } -func New(ctx context.Context, client arbutil.L1Interface, config ConfigFetcher, arbSysPrecompile ArbSysInterface) (*HeaderReader, error) { +func New(ctx context.Context, client *ethclient.Client, config ConfigFetcher, arbSysPrecompile ArbSysInterface) (*HeaderReader, error) { isParentChainArbitrum := false var arbSys ArbSysInterface if arbSysPrecompile != nil { @@ -340,6 +343,7 @@ func (s *HeaderReader) logIfHeaderIsOld() { if storedHeader == nil { return } + // #nosec G115 l1Timetamp := time.Unix(int64(storedHeader.Time), 0) headerTime := time.Since(l1Timetamp) if headerTime >= s.config().OldHeaderTimeout { @@ -521,7 +525,7 @@ func (s *HeaderReader) LatestFinalizedBlockNr(ctx context.Context) (uint64, erro return header.Number.Uint64(), nil } -func (s *HeaderReader) Client() arbutil.L1Interface { +func (s *HeaderReader) Client() *ethclient.Client { return s.client } diff --git a/util/jsonapi/preimages_test.go b/util/jsonapi/preimages_test.go index 3074a1e698..5b699df5fe 100644 --- a/util/jsonapi/preimages_test.go +++ b/util/jsonapi/preimages_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/util/testhelpers" ) diff --git a/util/merkletree/merkleAccumulator_test.go b/util/merkletree/merkleAccumulator_test.go index d26f0244d3..95e1862b7a 100644 --- a/util/merkletree/merkleAccumulator_test.go +++ b/util/merkletree/merkleAccumulator_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/merkleAccumulator" ) diff --git a/util/merkletree/merkleEventProof.go b/util/merkletree/merkleEventProof.go index 130249cc3f..cab85dbefe 100644 --- a/util/merkletree/merkleEventProof.go +++ b/util/merkletree/merkleEventProof.go @@ -5,6 +5,7 @@ package merkletree import ( "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/merkleAccumulator" ) diff --git a/util/merkletree/merkleEventProof_test.go b/util/merkletree/merkleEventProof_test.go index b64cc88c2a..c0c8e777cb 100644 --- a/util/merkletree/merkleEventProof_test.go +++ b/util/merkletree/merkleEventProof_test.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/merkleAccumulator" "github.com/offchainlabs/nitro/arbos/storage" @@ -22,6 +23,7 @@ func initializedMerkleAccumulatorForTesting() *merkleAccumulator.MerkleAccumulat func TestProofForNext(t *testing.T) { leaves := make([]common.Hash, 13) for i := range leaves { + // #nosec G115 leaves[i] = pseudorandomForTesting(uint64(i)) } diff --git a/util/merkletree/merkleTree.go b/util/merkletree/merkleTree.go index 1b15d51d98..9cf7b485d9 100644 --- a/util/merkletree/merkleTree.go +++ b/util/merkletree/merkleTree.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/util" ) @@ -43,8 +44,8 @@ func NewLevelAndLeaf(level, leaf uint64) LevelAndLeaf { func (place LevelAndLeaf) ToBigInt() *big.Int { return new(big.Int).Add( - new(big.Int).Lsh(big.NewInt(int64(place.Level)), 192), - big.NewInt(int64(place.Leaf)), + new(big.Int).Lsh(new(big.Int).SetUint64(place.Level), 192), + new(big.Int).SetUint64(place.Leaf), ) } diff --git a/util/redisutil/redis_coordinator.go b/util/redisutil/redis_coordinator.go index 2c12ffec50..ed0c9f70c7 100644 --- a/util/redisutil/redis_coordinator.go +++ b/util/redisutil/redis_coordinator.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/ethereum/go-ethereum/log" @@ -20,7 +20,9 @@ const PRIORITIES_KEY string = "coordinator.priorities" // Read o const WANTS_LOCKOUT_KEY_PREFIX string = "coordinator.liveliness." // Per server. Only written by self const MESSAGE_KEY_PREFIX string = "coordinator.msg." // Per Message. Only written by sequencer holding CHOSEN const SIGNATURE_KEY_PREFIX string = "coordinator.msg.sig." // Per Message. Only written by sequencer holding CHOSEN +const BLOCKMETADATA_KEY_PREFIX string = "coordinator.blockMetadata." // Per Message. Only written by sequencer holding CHOSEN const WANTS_LOCKOUT_VAL string = "OK" +const SWITCHED_REDIS string = "SWITCHED_REDIS" const INVALID_VAL string = "INVALID" const INVALID_URL string = "" @@ -118,3 +120,7 @@ func MessageKeyFor(pos arbutil.MessageIndex) string { func MessageSigKeyFor(pos arbutil.MessageIndex) string { return fmt.Sprintf("%s%d", SIGNATURE_KEY_PREFIX, pos) } + +func BlockMetadataKeyFor(pos arbutil.MessageIndex) string { + return fmt.Sprintf("%s%d", BLOCKMETADATA_KEY_PREFIX, pos) +} diff --git a/util/redisutil/redisutil.go b/util/redisutil/redisutil.go index f89c250e9a..fafb816b8a 100644 --- a/util/redisutil/redisutil.go +++ b/util/redisutil/redisutil.go @@ -1,14 +1,231 @@ package redisutil -import "github.com/go-redis/redis/v8" +import ( + "fmt" + "net" + "net/url" + "sort" + "strconv" + "strings" + "time" -func RedisClientFromURL(url string) (redis.UniversalClient, error) { - if url == "" { + "github.com/redis/go-redis/v9" +) + +// RedisClientFromURL creates a new Redis client based on the provided URL. +// The URL scheme can be either `redis` or `redis+sentinel`. +func RedisClientFromURL(redisUrl string) (redis.UniversalClient, error) { + if redisUrl == "" { return nil, nil } - redisOptions, err := redis.ParseURL(url) + u, err := url.Parse(redisUrl) + if err != nil { + return nil, err + } + if u.Scheme == "redis+sentinel" { + redisOptions, err := parseFailoverRedisUrl(redisUrl) + if err != nil { + return nil, err + } + return redis.NewFailoverClient(redisOptions), nil + } + redisOptions, err := redis.ParseURL(redisUrl) if err != nil { return nil, err } return redis.NewClient(redisOptions), nil } + +// Designed using https://github.com/redis/go-redis/blob/a8590e987945b7ba050569cc3b94b8ece49e99e3/options.go#L283 as reference +// Example Usage : +// +// redis+sentinel://:@:,:,:/?dial_timeout=3&db=1&read_timeout=6s&max_retries=2 +func parseFailoverRedisUrl(redisUrl string) (*redis.FailoverOptions, error) { + u, err := url.Parse(redisUrl) + if err != nil { + return nil, err + } + o := &redis.FailoverOptions{} + o.SentinelUsername, o.SentinelPassword = getUserPassword(u) + o.SentinelAddrs = getAddressesWithDefaults(u) + f := strings.FieldsFunc(u.Path, func(r rune) bool { + return r == '/' + }) + switch len(f) { + case 0: + return nil, fmt.Errorf("redis: master name is required") + case 1: + o.DB = 0 + o.MasterName = f[0] + case 2: + o.MasterName = f[0] + var err error + if o.DB, err = strconv.Atoi(f[1]); err != nil { + return nil, fmt.Errorf("redis: invalid database number: %q", f[0]) + } + default: + return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path) + } + + return setupConnParams(u, o) +} + +func getUserPassword(u *url.URL) (string, string) { + var user, password string + if u.User != nil { + user = u.User.Username() + if p, ok := u.User.Password(); ok { + password = p + } + } + return user, password +} + +func getAddressesWithDefaults(u *url.URL) []string { + urlHosts := strings.Split(u.Host, ",") + var addresses []string + for _, urlHost := range urlHosts { + host, port, err := net.SplitHostPort(urlHost) + if err != nil { + host = u.Host + } + if host == "" { + host = "localhost" + } + if port == "" { + port = "6379" + } + addresses = append(addresses, net.JoinHostPort(host, port)) + } + return addresses +} + +type queryOptions struct { + q url.Values + err error +} + +func (o *queryOptions) has(name string) bool { + return len(o.q[name]) > 0 +} + +func (o *queryOptions) string(name string) string { + vs := o.q[name] + if len(vs) == 0 { + return "" + } + delete(o.q, name) // enable detection of unknown parameters + return vs[len(vs)-1] +} + +func (o *queryOptions) int(name string) int { + s := o.string(name) + if s == "" { + return 0 + } + i, err := strconv.Atoi(s) + if err == nil { + return i + } + if o.err == nil { + o.err = fmt.Errorf("redis: invalid %s number: %w", name, err) + } + return 0 +} + +func (o *queryOptions) duration(name string) time.Duration { + s := o.string(name) + if s == "" { + return 0 + } + // try plain number first + if i, err := strconv.Atoi(s); err == nil { + if i <= 0 { + // disable timeouts + return -1 + } + return time.Duration(i) * time.Second + } + dur, err := time.ParseDuration(s) + if err == nil { + return dur + } + if o.err == nil { + o.err = fmt.Errorf("redis: invalid %s duration: %w", name, err) + } + return 0 +} + +func (o *queryOptions) bool(name string) bool { + switch s := o.string(name); s { + case "true", "1": + return true + case "false", "0", "": + return false + default: + if o.err == nil { + o.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s) + } + return false + } +} + +func (o *queryOptions) remaining() []string { + if len(o.q) == 0 { + return nil + } + keys := make([]string, 0, len(o.q)) + for k := range o.q { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func setupConnParams(u *url.URL, o *redis.FailoverOptions) (*redis.FailoverOptions, error) { + q := queryOptions{q: u.Query()} + + // compat: a future major release may use q.int("db") + if tmp := q.string("db"); tmp != "" { + db, err := strconv.Atoi(tmp) + if err != nil { + return nil, fmt.Errorf("redis: invalid database number: %w", err) + } + o.DB = db + } + + o.Protocol = q.int("protocol") + o.ClientName = q.string("client_name") + o.MaxRetries = q.int("max_retries") + o.MinRetryBackoff = q.duration("min_retry_backoff") + o.MaxRetryBackoff = q.duration("max_retry_backoff") + o.DialTimeout = q.duration("dial_timeout") + o.ReadTimeout = q.duration("read_timeout") + o.WriteTimeout = q.duration("write_timeout") + o.PoolFIFO = q.bool("pool_fifo") + o.PoolSize = q.int("pool_size") + o.PoolTimeout = q.duration("pool_timeout") + o.MinIdleConns = q.int("min_idle_conns") + o.MaxIdleConns = q.int("max_idle_conns") + o.MaxActiveConns = q.int("max_active_conns") + if q.has("conn_max_idle_time") { + o.ConnMaxIdleTime = q.duration("conn_max_idle_time") + } else { + o.ConnMaxIdleTime = q.duration("idle_timeout") + } + if q.has("conn_max_lifetime") { + o.ConnMaxLifetime = q.duration("conn_max_lifetime") + } else { + o.ConnMaxLifetime = q.duration("max_conn_age") + } + if q.err != nil { + return nil, q.err + } + + // any parameters left? + if r := q.remaining(); len(r) > 0 { + return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", ")) + } + + return o, nil +} diff --git a/util/redisutil/test_redis.go b/util/redisutil/test_redis.go index b6d2dc8fa8..271b3b48af 100644 --- a/util/redisutil/test_redis.go +++ b/util/redisutil/test_redis.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/alicebob/miniredis/v2" + "github.com/offchainlabs/nitro/util/testhelpers" ) diff --git a/util/rpcclient/rpcclient.go b/util/rpcclient/rpcclient.go index be5825a28d..a35d4b6665 100644 --- a/util/rpcclient/rpcclient.go +++ b/util/rpcclient/rpcclient.go @@ -101,7 +101,7 @@ func (c *RpcClient) Close() { } type limitedMarshal struct { - limit int + limit uint value any } @@ -113,16 +113,18 @@ func (m limitedMarshal) String() string { } else { str = string(marshalled) } - if m.limit == 0 || len(str) <= m.limit { + // #nosec G115 + limit := int(m.limit) + if m.limit <= 0 || len(str) <= limit { return str } prefix := str[:m.limit/2-1] - postfix := str[len(str)-m.limit/2+1:] + postfix := str[len(str)-limit/2+1:] return fmt.Sprintf("%v..%v", prefix, postfix) } type limitedArgumentsMarshal struct { - limit int + limit uint args []any } @@ -162,9 +164,9 @@ func (c *RpcClient) CallContext(ctx_in context.Context, result interface{}, meth return errors.New("not connected") } logId := c.logId.Add(1) - log.Trace("sending RPC request", "method", method, "logId", logId, "args", limitedArgumentsMarshal{int(c.config().ArgLogLimit), args}) + log.Trace("sending RPC request", "method", method, "logId", logId, "args", limitedArgumentsMarshal{c.config().ArgLogLimit, args}) var err error - for i := 0; i < int(c.config().Retries)+1; i++ { + for i := uint(0); i < c.config().Retries+1; i++ { retryDelay := c.config().RetryDelay if i > 0 && retryDelay > 0 { select { @@ -188,7 +190,7 @@ func (c *RpcClient) CallContext(ctx_in context.Context, result interface{}, meth cancelCtx() logger := log.Trace - limit := int(c.config().ArgLogLimit) + limit := c.config().ArgLogLimit if err != nil && !IsAlreadyKnownError(err) { logger = log.Info } diff --git a/util/rpcclient/rpcclient_test.go b/util/rpcclient/rpcclient_test.go index 1a7da54774..f711747b70 100644 --- a/util/rpcclient/rpcclient_test.go +++ b/util/rpcclient/rpcclient_test.go @@ -9,10 +9,12 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/util/testhelpers" ) diff --git a/util/s3client/s3client.go b/util/s3client/s3client.go new file mode 100644 index 0000000000..623107ea14 --- /dev/null +++ b/util/s3client/s3client.go @@ -0,0 +1,62 @@ +package s3client + +import ( + "context" + "io" + + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type Uploader interface { + Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) +} + +type Downloader interface { + Download(ctx context.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*manager.Downloader)) (n int64, err error) +} + +type FullClient interface { + Uploader + Downloader + Client() *s3.Client +} + +type s3Client struct { + client *s3.Client + uploader Uploader + downloader Downloader +} + +func NewS3FullClient(accessKey, secretKey, region string) (FullClient, error) { + cfg, err := awsConfig.LoadDefaultConfig(context.TODO(), awsConfig.WithRegion(region), func(options *awsConfig.LoadOptions) error { + // remain backward compatible with accessKey and secretKey credentials provided via cli flags + if accessKey != "" && secretKey != "" { + options.Credentials = credentials.NewStaticCredentialsProvider(accessKey, secretKey, "") + } + return nil + }) + if err != nil { + return nil, err + } + client := s3.NewFromConfig(cfg) + return &s3Client{ + client: client, + uploader: manager.NewUploader(client), + downloader: manager.NewDownloader(client), + }, nil +} + +func (s *s3Client) Client() *s3.Client { + return s.client +} + +func (s *s3Client) Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) { + return s.uploader.Upload(ctx, input, opts...) +} + +func (s *s3Client) Download(ctx context.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*manager.Downloader)) (n int64, err error) { + return s.downloader.Download(ctx, w, input, options...) +} diff --git a/util/sharedmetrics/sharedmetrics.go b/util/sharedmetrics/sharedmetrics.go index 377eef5352..1df34d4d54 100644 --- a/util/sharedmetrics/sharedmetrics.go +++ b/util/sharedmetrics/sharedmetrics.go @@ -2,6 +2,7 @@ package sharedmetrics import ( "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/arbutil" ) @@ -11,8 +12,10 @@ var ( ) func UpdateSequenceNumberGauge(sequenceNumber arbutil.MessageIndex) { + // #nosec G115 latestSequenceNumberGauge.Update(int64(sequenceNumber)) } func UpdateSequenceNumberInBlockGauge(sequenceNumber arbutil.MessageIndex) { + // #nosec G115 sequenceNumberInBlockGauge.Update(int64(sequenceNumber)) } diff --git a/util/signature/sign_verify.go b/util/signature/sign_verify.go index 5ed852bfbc..f222860d8b 100644 --- a/util/signature/sign_verify.go +++ b/util/signature/sign_verify.go @@ -4,9 +4,11 @@ import ( "context" "errors" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/util/contracts" - flag "github.com/spf13/pflag" ) type SignVerify struct { diff --git a/util/stopwaiter/stopwaiter.go b/util/stopwaiter/stopwaiter.go index 23a0cc1000..ccfa676d49 100644 --- a/util/stopwaiter/stopwaiter.go +++ b/util/stopwaiter/stopwaiter.go @@ -13,6 +13,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/containers" ) @@ -95,20 +96,12 @@ func (s *StopWaiterSafe) Start(ctx context.Context, parent any) error { } func (s *StopWaiterSafe) StopOnly() { - _ = s.stopOnly() -} - -// returns true if stop function was called -func (s *StopWaiterSafe) stopOnly() bool { - stopWasCalled := false s.mutex.Lock() defer s.mutex.Unlock() if s.started && !s.stopped { s.stopFunc() - stopWasCalled = true } s.stopped = true - return stopWasCalled } // StopAndWait may be called multiple times, even before start. @@ -125,9 +118,15 @@ func getAllStackTraces() string { } func (s *StopWaiterSafe) stopAndWaitImpl(warningTimeout time.Duration) error { - if !s.stopOnly() { + s.StopOnly() + if !s.Started() { + // No need to wait, because nothing can be started if it's already stopped. return nil } + // Even if StopOnly has been previously called, make sure we wait for everything to shut down. + // Otherwise, a StopOnly call followed by StopAndWait might return early without waiting. + // At this point started must be true (because it was true above and cannot go back to false), + // so GetWaitChannel won't return an error. waitChan, err := s.GetWaitChannel() if err != nil { return err diff --git a/util/stopwaiter/stopwaiter_test.go b/util/stopwaiter/stopwaiter_test.go index 5319927887..68e49ac2be 100644 --- a/util/stopwaiter/stopwaiter_test.go +++ b/util/stopwaiter/stopwaiter_test.go @@ -5,10 +5,12 @@ package stopwaiter import ( "context" + "sync/atomic" "testing" "time" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/testhelpers" ) @@ -72,3 +74,19 @@ func TestStopWaiterStopAndWaitMultipleTimes(t *testing.T) { sw.StopAndWait() sw.StopAndWait() } + +func TestStopWaiterStopOnlyThenStopAndWait(t *testing.T) { + t.Parallel() + sw := StopWaiter{} + sw.Start(context.Background(), &TestStruct{}) + var threadStopping atomic.Bool + sw.LaunchThread(func(context.Context) { + time.Sleep(time.Second) + threadStopping.Store(true) + }) + sw.StopOnly() + sw.StopAndWait() + if !threadStopping.Load() { + t.Error("StopAndWait returned before background thread stopped") + } +} diff --git a/util/testhelpers/env/env.go b/util/testhelpers/env/env.go index 27d74465de..2a8090c212 100644 --- a/util/testhelpers/env/env.go +++ b/util/testhelpers/env/env.go @@ -14,7 +14,7 @@ import ( // An environment variable controls that behavior. func GetTestStateScheme() string { envTestStateScheme := os.Getenv("TEST_STATE_SCHEME") - stateScheme := rawdb.PathScheme + stateScheme := rawdb.HashScheme if envTestStateScheme == rawdb.PathScheme || envTestStateScheme == rawdb.HashScheme { stateScheme = envTestStateScheme } diff --git a/util/testhelpers/port.go b/util/testhelpers/port.go index d31fa41cdc..c17e9d9ec2 100644 --- a/util/testhelpers/port.go +++ b/util/testhelpers/port.go @@ -2,6 +2,7 @@ package testhelpers import ( "net" + "testing" ) // FreeTCPPortListener returns a listener listening on an unused local port. @@ -15,3 +16,13 @@ func FreeTCPPortListener() (net.Listener, error) { } return l, nil } + +// Func AddrTCPPort returns the port of a net.Addr. +func AddrTCPPort(n net.Addr, t *testing.T) int { + t.Helper() + tcpAddr, ok := n.(*net.TCPAddr) + if !ok { + t.Fatal("Could not get TCP address net.Addr") + } + return tcpAddr.Port +} diff --git a/util/testhelpers/port_test.go b/util/testhelpers/port_test.go index ef9bb18537..bb8f87b2f7 100644 --- a/util/testhelpers/port_test.go +++ b/util/testhelpers/port_test.go @@ -14,10 +14,18 @@ func TestFreeTCPPortListener(t *testing.T) { if err != nil { t.Fatal(err) } - if aListener.Addr().(*net.TCPAddr).Port == bListener.Addr().(*net.TCPAddr).Port { + aTCPAddr, ok := aListener.Addr().(*net.TCPAddr) + if !ok { + t.Fatalf("aListener.Addr() is not a *net.TCPAddr: %v", aListener.Addr()) + } + bTCPAddr, ok := bListener.Addr().(*net.TCPAddr) + if !ok { + t.Fatalf("bListener.Addr() is not a *net.TCPAddr: %v", aListener.Addr()) + } + if aTCPAddr.Port == bTCPAddr.Port { t.Errorf("FreeTCPPortListener() got same port: %v, %v", aListener, bListener) } - if aListener.Addr().(*net.TCPAddr).Port == 0 || bListener.Addr().(*net.TCPAddr).Port == 0 { + if aTCPAddr.Port == 0 || bTCPAddr.Port == 0 { t.Errorf("FreeTCPPortListener() got port 0") } } diff --git a/util/testhelpers/stackconfig.go b/util/testhelpers/stackconfig.go index c7c46befd1..9fe18ec35f 100644 --- a/util/testhelpers/stackconfig.go +++ b/util/testhelpers/stackconfig.go @@ -9,12 +9,12 @@ func CreateStackConfigForTest(dataDir string) *node.Config { stackConf := node.DefaultConfig stackConf.DataDir = dataDir stackConf.UseLightweightKDF = true - stackConf.AuthPort = 0 stackConf.WSPort = 0 stackConf.WSModules = append(stackConf.WSModules, "eth", "debug") stackConf.HTTPPort = 0 stackConf.HTTPHost = "" stackConf.HTTPModules = append(stackConf.HTTPModules, "eth", "debug") + stackConf.AuthPort = 0 stackConf.P2P.NoDiscovery = true stackConf.P2P.NoDial = true stackConf.P2P.ListenAddr = "" diff --git a/util/testhelpers/testhelpers.go b/util/testhelpers/testhelpers.go index 9fe2e299d0..8ef3c489e4 100644 --- a/util/testhelpers/testhelpers.go +++ b/util/testhelpers/testhelpers.go @@ -7,6 +7,7 @@ import ( "context" crypto "crypto/rand" "io" + "log/slog" "math/big" "math/rand" "os" @@ -17,8 +18,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/colors" - "golang.org/x/exp/slog" ) // Fail a test should an error occur @@ -65,6 +66,7 @@ func RandomCallValue(limit int64) *big.Int { // Computes a psuedo-random uint64 on the interval [min, max] func RandomUint32(min, max uint32) uint32 { + //#nosec G115 return uint32(RandomUint64(uint64(min), uint64(max))) } diff --git a/validator/client/redis/producer.go b/validator/client/redis/producer.go index f98c246d0e..4bfb721f59 100644 --- a/validator/client/redis/producer.go +++ b/validator/client/redis/producer.go @@ -5,10 +5,14 @@ import ( "fmt" "sync/atomic" + "github.com/redis/go-redis/v9" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" - "github.com/go-redis/redis/v8" + "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/redisutil" @@ -16,7 +20,6 @@ import ( "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_api" "github.com/offchainlabs/nitro/validator/server_common" - "github.com/spf13/pflag" ) type ValidationClientConfig struct { @@ -35,7 +38,7 @@ func (c ValidationClientConfig) Enabled() bool { func (c ValidationClientConfig) Validate() error { for _, arch := range c.StylusArchs { - if !rawdb.Target(arch).IsValid() { + if !rawdb.IsSupportedWasmTarget(ethdb.WasmTarget(arch)) { return fmt.Errorf("Invalid stylus arch: %v", arch) } } @@ -162,10 +165,10 @@ func (c *ValidationClient) Name() string { return c.config.Name } -func (c *ValidationClient) StylusArchs() []rawdb.Target { - stylusArchs := make([]rawdb.Target, 0, len(c.config.StylusArchs)) +func (c *ValidationClient) StylusArchs() []ethdb.WasmTarget { + stylusArchs := make([]ethdb.WasmTarget, 0, len(c.config.StylusArchs)) for _, arch := range c.config.StylusArchs { - stylusArchs = append(stylusArchs, rawdb.Target(arch)) + stylusArchs = append(stylusArchs, ethdb.WasmTarget(arch)) } return stylusArchs } diff --git a/validator/client/validation_client.go b/validator/client/validation_client.go index 80cff66675..c04817d654 100644 --- a/validator/client/validation_client.go +++ b/validator/client/validation_client.go @@ -11,27 +11,26 @@ import ( "sync/atomic" "time" - "github.com/offchainlabs/nitro/validator" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/util/stopwaiter" - + "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_api" "github.com/offchainlabs/nitro/validator/server_common" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" ) type ValidationClient struct { stopwaiter.StopWaiter client *rpcclient.RpcClient name string - stylusArchs []rawdb.Target + stylusArchs []ethdb.WasmTarget room atomic.Int32 wasmModuleRoots []common.Hash } @@ -40,7 +39,7 @@ func NewValidationClient(config rpcclient.ClientConfigFetcher, stack *node.Node) return &ValidationClient{ client: rpcclient.NewRpcClient(config, stack), name: "not started", - stylusArchs: []rawdb.Target{"not started"}, + stylusArchs: []ethdb.WasmTarget{"not started"}, } } @@ -67,20 +66,20 @@ func (c *ValidationClient) Start(ctx context.Context) error { if len(name) == 0 { return errors.New("couldn't read name from server") } - var stylusArchs []rawdb.Target + var stylusArchs []ethdb.WasmTarget if err := c.client.CallContext(ctx, &stylusArchs, server_api.Namespace+"_stylusArchs"); err != nil { var rpcError rpc.Error ok := errors.As(err, &rpcError) if !ok || rpcError.ErrorCode() != -32601 { return fmt.Errorf("could not read stylus arch from server: %w", err) } - stylusArchs = []rawdb.Target{rawdb.Target("pre-stylus")} // invalid, will fail if trying to validate block with stylus + stylusArchs = []ethdb.WasmTarget{ethdb.WasmTarget("pre-stylus")} // invalid, will fail if trying to validate block with stylus } else { if len(stylusArchs) == 0 { return fmt.Errorf("could not read stylus archs from validation server") } for _, stylusArch := range stylusArchs { - if stylusArch != rawdb.TargetWavm && stylusArch != rawdb.LocalTarget() && stylusArch != "mock" { + if !rawdb.IsSupportedWasmTarget(ethdb.WasmTarget(stylusArch)) && stylusArch != "mock" { return fmt.Errorf("unsupported stylus architecture: %v", stylusArch) } } @@ -102,6 +101,7 @@ func (c *ValidationClient) Start(ctx context.Context) error { } else { log.Info("connected to validation server", "name", name, "room", room) } + // #nosec G115 c.room.Store(int32(room)) c.wasmModuleRoots = moduleRoots c.name = name @@ -117,11 +117,11 @@ func (c *ValidationClient) WasmModuleRoots() ([]common.Hash, error) { return nil, errors.New("not started") } -func (c *ValidationClient) StylusArchs() []rawdb.Target { +func (c *ValidationClient) StylusArchs() []ethdb.WasmTarget { if c.Started() { return c.stylusArchs } - return []rawdb.Target{"not started"} + return []ethdb.WasmTarget{"not started"} } func (c *ValidationClient) Stop() { @@ -153,10 +153,14 @@ func NewExecutionClient(config rpcclient.ClientConfigFetcher, stack *node.Node) } } -func (c *ExecutionClient) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { - return stopwaiter.LaunchPromiseThread[validator.ExecutionRun](c, func(ctx context.Context) (validator.ExecutionRun, error) { +func (c *ExecutionClient) CreateExecutionRun( + wasmModuleRoot common.Hash, + input *validator.ValidationInput, + useBoldMachine bool, +) containers.PromiseInterface[validator.ExecutionRun] { + return stopwaiter.LaunchPromiseThread(c, func(ctx context.Context) (validator.ExecutionRun, error) { var res uint64 - err := c.client.CallContext(ctx, &res, server_api.Namespace+"_createExecutionRun", wasmModuleRoot, server_api.ValidationInputToJson(input)) + err := c.client.CallContext(ctx, &res, server_api.Namespace+"_createExecutionRun", wasmModuleRoot, server_api.ValidationInputToJson(input), useBoldMachine) if err != nil { return nil, err } @@ -186,19 +190,6 @@ func (c *ExecutionClient) LatestWasmModuleRoot() containers.PromiseInterface[com }) } -func (c *ExecutionClient) WriteToFile(input *validator.ValidationInput, expOut validator.GoGlobalState, moduleRoot common.Hash) containers.PromiseInterface[struct{}] { - jsonInput := server_api.ValidationInputToJson(input) - if err := jsonInput.WriteToFile(); err != nil { - return stopwaiter.LaunchPromiseThread[struct{}](c, func(ctx context.Context) (struct{}, error) { - return struct{}{}, err - }) - } - return stopwaiter.LaunchPromiseThread[struct{}](c, func(ctx context.Context) (struct{}, error) { - err := c.client.CallContext(ctx, nil, server_api.Namespace+"_writeToFile", jsonInput, expOut, moduleRoot) - return struct{}{}, err - }) -} - func (r *ExecutionClientRun) SendKeepAlive(ctx context.Context) time.Duration { err := r.client.client.CallContext(ctx, nil, server_api.Namespace+"_execKeepAlive", r.id) if err != nil { @@ -207,6 +198,10 @@ func (r *ExecutionClientRun) SendKeepAlive(ctx context.Context) time.Duration { return time.Minute // TODO: configurable } +func (r *ExecutionClientRun) CheckAlive(ctx context.Context) error { + return r.client.client.CallContext(ctx, nil, server_api.Namespace+"_checkAlive", r.id) +} + func (r *ExecutionClientRun) Start(ctx_in context.Context) { r.StopWaiter.Start(ctx_in, r) r.CallIteratively(r.SendKeepAlive) diff --git a/validator/execution_state.go b/validator/execution_state.go index 092fbe2908..81e32a6992 100644 --- a/validator/execution_state.go +++ b/validator/execution_state.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/solgen/go/challengegen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" ) @@ -18,6 +19,13 @@ type GoGlobalState struct { PosInBatch uint64 } +func (g GoGlobalState) String() string { + return fmt.Sprintf( + "BlockHash: %s, SendRoot: %s, Batch: %d, PosInBatch: %d", + g.BlockHash.Hex(), g.SendRoot.Hex(), g.Batch, g.PosInBatch, + ) +} + type MachineStatus uint8 const ( diff --git a/validator/inputs/writer.go b/validator/inputs/writer.go new file mode 100644 index 0000000000..1a476c52a3 --- /dev/null +++ b/validator/inputs/writer.go @@ -0,0 +1,157 @@ +package inputs + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/offchainlabs/nitro/validator/server_api" +) + +// Writer is a configurable writer of InputJSON files. +// +// The default Writer will write to a path like: +// +// $HOME/.arbuitrum/validation-inputs//block_inputs_.json +// +// The path can be nested under a slug directory so callers can provide a +// recognizable name to differentiate various contexts in which the InputJSON +// is being written. If the Writer is configured by calling WithSlug, then the +// path will be like: +// +// $HOME/.arbuitrum/validation-inputs///block_inputs_.json +// +// The inclusion of BlockId in the file's name is on by default, however that can be disabled +// by calling WithBlockIdInFileNameEnabled(false). In which case, the path will be like: +// +// $HOME/.arbuitrum/validation-inputs///block_inputs.json +// +// The inclusion of a timestamp directory is on by default to avoid conflicts which +// would result in files being overwritten. However, the Writer can be configured +// to not use a timestamp directory. If the Writer is configured by calling +// WithTimestampDirEnabled(false), then the path will be like: +// +// $HOME/.arbuitrum/validation-inputs//block_inputs_.json +// +// Finally, to give complete control to the clients, the base directory can be +// set directly with WithBaseDir. In which case, the path will be like: +// +// /block_inputs_.json +// or +// //block_inputs_.json +// or +// ///block_inputs_.json +type Writer struct { + clock Clock + baseDir string + slug string + useTimestampDir bool + useBlockIdInFileName bool +} + +// WriterOption is a function that configures a Writer. +type WriterOption func(*Writer) + +// Clock is an interface for getting the current time. +type Clock interface { + Now() time.Time +} + +type realClock struct{} + +func (realClock) Now() time.Time { + return time.Now() +} + +// NewWriter creates a new Writer with default settings. +func NewWriter(options ...WriterOption) (*Writer, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + baseDir := filepath.Join(homeDir, ".arbitrum", "validation-inputs") + w := &Writer{ + clock: realClock{}, + baseDir: baseDir, + slug: "", + useTimestampDir: true, + useBlockIdInFileName: true, + } + for _, o := range options { + o(w) + } + return w, nil +} + +// withTestClock configures the Writer to use the given clock. +// +// This is only intended for testing. +func withTestClock(clock Clock) WriterOption { + return func(w *Writer) { + w.clock = clock + } +} + +// WithSlug configures the Writer to use the given slug as a directory name. +func WithSlug(slug string) WriterOption { + return func(w *Writer) { + w.slug = slug + } +} + +// WithoutSlug clears the slug configuration. +// +// This is equivalent to the WithSlug("") option but is more readable. +func WithoutSlug() WriterOption { + return WithSlug("") +} + +// WithBaseDir configures the Writer to use the given base directory. +func WithBaseDir(baseDir string) WriterOption { + return func(w *Writer) { + w.baseDir = baseDir + } +} + +// WithTimestampDirEnabled controls the addition of a timestamp directory. +func WithTimestampDirEnabled(useTimestampDir bool) WriterOption { + return func(w *Writer) { + w.useTimestampDir = useTimestampDir + } +} + +// WithBlockIdInFileNameEnabled controls the inclusion of Block Id in the input json file's name +func WithBlockIdInFileNameEnabled(useBlockIdInFileName bool) WriterOption { + return func(w *Writer) { + w.useBlockIdInFileName = useBlockIdInFileName + } +} + +// Write writes the given InputJSON to a file in JSON format. +func (w *Writer) Write(json *server_api.InputJSON) error { + dir := w.baseDir + if w.slug != "" { + dir = filepath.Join(dir, w.slug) + } + if w.useTimestampDir { + t := w.clock.Now() + tStr := t.Format("20060102_150405") + dir = filepath.Join(dir, tStr) + } + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + contents, err := json.Marshal() + if err != nil { + return err + } + fileName := "block_inputs.json" + if w.useBlockIdInFileName { + fileName = fmt.Sprintf("block_inputs_%d.json", json.Id) + } + if err = os.WriteFile(filepath.Join(dir, fileName), contents, 0600); err != nil { + return err + } + return nil +} diff --git a/validator/inputs/writer_test.go b/validator/inputs/writer_test.go new file mode 100644 index 0000000000..59cb63dae7 --- /dev/null +++ b/validator/inputs/writer_test.go @@ -0,0 +1,92 @@ +package inputs + +import ( + "os" + "testing" + "time" + + "github.com/offchainlabs/nitro/validator/server_api" +) + +func TestDefaultBaseDir(t *testing.T) { + // Simply testing that the default baseDir is set relative to the user's home directory. + // This way, the other tests can all override the baseDir to a temporary directory. + w, err := NewWriter() + if err != nil { + t.Fatal(err) + } + homeDir, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + if w.baseDir != homeDir+"/.arbitrum/validation-inputs" { + t.Errorf("unexpected baseDir: %v", w.baseDir) + } +} + +type fakeClock struct { + now time.Time +} + +func (c fakeClock) Now() time.Time { + return c.now +} + +func TestWriting(t *testing.T) { + dir := t.TempDir() + w, err := NewWriter( + withTestClock(fakeClock{now: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC)}), + WithBaseDir(dir), + ) + if err != nil { + t.Fatal(err) + } + err = w.Write(&server_api.InputJSON{Id: 24601}) + if err != nil { + t.Fatal(err) + } + // The file should exist. + if _, err := os.Stat(dir + "/20210102_030405/block_inputs_24601.json"); err != nil { + t.Error(err) + } +} + +func TestWritingWithSlug(t *testing.T) { + dir := t.TempDir() + w, err := NewWriter( + withTestClock(fakeClock{now: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC)}), + WithBaseDir(dir), + WithSlug("foo"), + ) + if err != nil { + t.Fatal(err) + } + err = w.Write(&server_api.InputJSON{Id: 24601}) + if err != nil { + t.Fatal(err) + } + // The file should exist. + if _, err := os.Stat(dir + "/foo/20210102_030405/block_inputs_24601.json"); err != nil { + t.Error(err) + } +} + +func TestWritingWithoutTimestampDir(t *testing.T) { + dir := t.TempDir() + w, err := NewWriter( + withTestClock(fakeClock{now: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC)}), + WithBaseDir(dir), + WithTimestampDirEnabled(false), + ) + if err != nil { + t.Fatal(err) + } + err = w.Write(&server_api.InputJSON{Id: 24601}) + if err != nil { + t.Fatal(err) + } + // The file should exist. + if _, err := os.Stat(dir + "/block_inputs_24601.json"); err != nil { + t.Error(err) + } +} diff --git a/validator/interface.go b/validator/interface.go index 81b40ae5cf..249cf1b1c3 100644 --- a/validator/interface.go +++ b/validator/interface.go @@ -4,7 +4,8 @@ import ( "context" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/util/containers" ) @@ -14,7 +15,7 @@ type ValidationSpawner interface { Start(context.Context) error Stop() Name() string - StylusArchs() []rawdb.Target + StylusArchs() []ethdb.WasmTarget Room() int } @@ -25,9 +26,8 @@ type ValidationRun interface { type ExecutionSpawner interface { ValidationSpawner - CreateExecutionRun(wasmModuleRoot common.Hash, input *ValidationInput) containers.PromiseInterface[ExecutionRun] + CreateExecutionRun(wasmModuleRoot common.Hash, input *ValidationInput, useBoldMachine bool) containers.PromiseInterface[ExecutionRun] LatestWasmModuleRoot() containers.PromiseInterface[common.Hash] - WriteToFile(input *ValidationInput, expOut GoGlobalState, moduleRoot common.Hash) containers.PromiseInterface[struct{}] } type ExecutionRun interface { @@ -37,4 +37,5 @@ type ExecutionRun interface { GetProofAt(uint64) containers.PromiseInterface[[]byte] PrepareRange(uint64, uint64) containers.PromiseInterface[struct{}] Close() + CheckAlive(ctx context.Context) error } diff --git a/validator/server_api/json.go b/validator/server_api/json.go index dbe2bb1fee..f56493cd92 100644 --- a/validator/server_api/json.go +++ b/validator/server_api/json.go @@ -8,13 +8,12 @@ import ( "encoding/json" "errors" "fmt" - "os" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbutil" - "github.com/offchainlabs/nitro/util/jsonapi" "github.com/offchainlabs/nitro/validator" ) @@ -64,19 +63,13 @@ type InputJSON struct { BatchInfo []BatchInfoJson DelayedMsgB64 string StartState validator.GoGlobalState - UserWasms map[rawdb.Target]map[common.Hash]string + UserWasms map[ethdb.WasmTarget]map[common.Hash]string DebugChain bool } -func (i *InputJSON) WriteToFile() error { - contents, err := json.MarshalIndent(i, "", " ") - if err != nil { - return err - } - if err = os.WriteFile(fmt.Sprintf("block_inputs_%d.json", i.Id), contents, 0600); err != nil { - return err - } - return nil +// Marshal returns the JSON encoding of the InputJSON. +func (i *InputJSON) Marshal() ([]byte, error) { + return json.MarshalIndent(i, "", " ") } type BatchInfoJson struct { @@ -96,7 +89,7 @@ func ValidationInputToJson(entry *validator.ValidationInput) *InputJSON { DelayedMsgB64: base64.StdEncoding.EncodeToString(entry.DelayedMsg), StartState: entry.StartState, PreimagesB64: jsonPreimagesMap, - UserWasms: make(map[rawdb.Target]map[common.Hash]string), + UserWasms: make(map[ethdb.WasmTarget]map[common.Hash]string), DebugChain: entry.DebugChain, } for _, binfo := range entry.BatchInfo { @@ -128,7 +121,7 @@ func ValidationInputFromJson(entry *InputJSON) (*validator.ValidationInput, erro DelayedMsgNr: entry.DelayedMsgNr, StartState: entry.StartState, Preimages: preimages, - UserWasms: make(map[rawdb.Target]map[common.Hash][]byte), + UserWasms: make(map[ethdb.WasmTarget]map[common.Hash][]byte), DebugChain: entry.DebugChain, } delayed, err := base64.StdEncoding.DecodeString(entry.DelayedMsgB64) diff --git a/validator/server_arb/bold_machine.go b/validator/server_arb/bold_machine.go new file mode 100644 index 0000000000..6ca48ba228 --- /dev/null +++ b/validator/server_arb/bold_machine.go @@ -0,0 +1,145 @@ +package server_arb + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/validator" +) + +// boldMachine wraps a server_arb.MachineInterface. +type BoldMachine struct { + inner MachineInterface + zeroMachine *ArbitratorMachine + hasStepped bool +} + +// Ensure boldMachine implements server_arb.MachineInterface. +var _ MachineInterface = (*BoldMachine)(nil) + +func newBoldMachine(inner MachineInterface) *BoldMachine { + z := NewFinishedMachine(inner.GetGlobalState()) + return &BoldMachine{ + inner: inner, + zeroMachine: z, + hasStepped: false, + } +} + +// Wraps a server_arb.MachineInterface and adds one step to the +// front of the machine's execution. +// +// This zeroth step should be at the same global state as the inner arbitrator +// machine has at step 0, but the machine is in the Finished state rather than +// the Running state. +func BoldMachineWrapper(inner MachineInterface) MachineInterface { + return newBoldMachine(inner) +} + +// CloneMachineInterface returns a new boldMachine with the same inner machine. +func (m *BoldMachine) CloneMachineInterface() MachineInterface { + bMach := newBoldMachine(m.inner.CloneMachineInterface()) + bMach.hasStepped = m.hasStepped + return bMach +} + +// GetStepCount returns zero if the machine has not stepped, otherwise it +// returns the inner machine's step count plus one. +func (m *BoldMachine) GetStepCount() uint64 { + if !m.hasStepped { + return 0 + } + return m.inner.GetStepCount() + 1 +} + +// Hash returns the hash of the inner machine if the machine has not stepped, +// otherwise it returns the hash of the zeroth step machine. +func (m *BoldMachine) Hash() common.Hash { + if !m.hasStepped { + return m.zeroMachine.Hash() + } + return m.inner.Hash() +} + +// Destroy destroys the inner machine and the zeroth step machine. +func (m *BoldMachine) Destroy() { + m.inner.Destroy() + m.zeroMachine.Destroy() +} + +// Freeze freezes the inner machine and the zeroth step machine. +func (m *BoldMachine) Freeze() { + m.inner.Freeze() + m.zeroMachine.Freeze() +} + +// Status returns the status of the inner machine if the machine has not +// stepped, otherwise it returns the status of the zeroth step machine. +func (m *BoldMachine) Status() uint8 { + if !m.hasStepped { + return m.zeroMachine.Status() + } + return m.inner.Status() +} + +// IsRunning returns true if the machine has not stepped, otherwise it +// returns the running state of the inner machine. +func (m *BoldMachine) IsRunning() bool { + if !m.hasStepped { + return true + } + return m.inner.IsRunning() +} + +// IsErrored returns the errored state of the inner machine, or false if the +// machine has not stepped. +func (m *BoldMachine) IsErrored() bool { + if !m.hasStepped { + return false + } + return m.inner.IsErrored() +} + +// Step steps the inner machine if the machine has not stepped, otherwise it +// steps the zeroth step machine. +func (m *BoldMachine) Step(ctx context.Context, steps uint64) error { + if !m.hasStepped { + if steps == 0 { + // Zero is okay, but doesn't advance the machine. + return nil + } + m.hasStepped = true + // Only the first step or set of steps needs to be adjusted. + steps = steps - 1 + } + return m.inner.Step(ctx, steps) +} + +// ValidForStep returns true for step 0 if and only if the machine has not stepped yet, +// and the inner machine's ValidForStep for the step minus one otherwise. +func (m *BoldMachine) ValidForStep(step uint64) bool { + if step == 0 { + return !m.hasStepped + } + return m.inner.ValidForStep(step - 1) +} + +// GetGlobalState returns the global state of the inner machine if the machine +// has stepped, otherwise it returns the global state of the zeroth step. +func (m *BoldMachine) GetGlobalState() validator.GoGlobalState { + if !m.hasStepped { + return m.zeroMachine.GetGlobalState() + } + return m.inner.GetGlobalState() +} + +// ProveNextStep returns the proof of the next step of the inner machine if the +// machine has stepped, otherwise it returns the proof that the zeroth step +// results in the inner machine's initial global state. +func (m *BoldMachine) ProveNextStep() []byte { + if !m.hasStepped { + return m.zeroMachine.ProveNextStep() + } + return m.inner.ProveNextStep() +} diff --git a/validator/server_arb/execution_run.go b/validator/server_arb/execution_run.go index d29a88d34d..66d8e158d0 100644 --- a/validator/server_arb/execution_run.go +++ b/validator/server_arb/execution_run.go @@ -10,9 +10,8 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" @@ -25,7 +24,8 @@ type executionRun struct { } // NewExecutionRun creates a backend with the given arguments. -// Note: machineCache may be nil, but if present, it must not have a restricted range. +// Note: machineCache may be nil, but if present, it must not have a restricted +// range. func NewExecutionRun( ctxIn context.Context, initialMachineGetter func(context.Context) (MachineInterface, error), @@ -105,21 +105,9 @@ func (e *executionRun) machineHashesWithStepSize( if err != nil { return nil, err } - log.Debug(fmt.Sprintf("Advanced machine to index %d, beginning hash computation", machineStartIndex)) - - // In BOLD, the hash of a machine at index 0 is a special hash that is computed as the - // `machineFinishedHash(gs)` where `gs` is the global state of the machine at index 0. - // This is so that the hash aligns with the start state of the claimed challenge edge - // at the level above, as required by the BOLD protocol. - var machineHashes []common.Hash - if machineStartIndex == 0 { - gs := machine.GetGlobalState() - log.Debug(fmt.Sprintf("Start global state for machine index 0: %+v", gs)) - machineHashes = append(machineHashes, machineFinishedHash(gs)) - } else { - // Otherwise, we simply append the machine hash at the specified start index. - machineHashes = append(machineHashes, machine.Hash()) - } + log.Info("Advanced WASM machine index, beginning challenge hash computation", "machineStartIndex", machineStartIndex) + + machineHashes := []common.Hash{machine.Hash()} startHash := machineHashes[0] // If we only want 1 hash, we can return early. @@ -195,6 +183,6 @@ func (e *executionRun) GetLastStep() containers.PromiseInterface[*validator.Mach return e.GetStepAt(^uint64(0)) } -func machineFinishedHash(gs validator.GoGlobalState) common.Hash { - return crypto.Keccak256Hash([]byte("Machine finished:"), gs.Hash().Bytes()) +func (e *executionRun) CheckAlive(ctx context.Context) error { + return nil } diff --git a/validator/server_arb/execution_run_test.go b/validator/server_arb/execution_run_test.go index bdc1eefc4d..381cfa63a8 100644 --- a/validator/server_arb/execution_run_test.go +++ b/validator/server_arb/execution_run_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator" ) @@ -15,9 +16,6 @@ type mockMachine struct { } func (m *mockMachine) Hash() common.Hash { - if m.gs.PosInBatch == m.totalSteps-1 { - return machineFinishedHash(m.gs) - } return m.gs.Hash() } @@ -47,6 +45,9 @@ func (m *mockMachine) GetStepCount() uint64 { func (m *mockMachine) IsRunning() bool { return m.gs.PosInBatch < m.totalSteps-1 } +func (m *mockMachine) IsErrored() bool { + return false +} func (m *mockMachine) ValidForStep(uint64) bool { return true } @@ -102,7 +103,7 @@ func Test_machineHashesWithStep(t *testing.T) { if err != nil { t.Fatal(err) } - expected := machineFinishedHash(mm.gs) + expected := mm.gs.Hash() if len(hashes) != 1 { t.Error("Wanted one hash") } @@ -136,7 +137,7 @@ func Test_machineHashesWithStep(t *testing.T) { expectedHashes := make([]common.Hash, 0) for i := uint64(0); i < 4; i++ { if i == 0 { - expectedHashes = append(expectedHashes, machineFinishedHash(initialGs)) + expectedHashes = append(expectedHashes, initialGs.Hash()) continue } gs := validator.GoGlobalState{ @@ -181,7 +182,7 @@ func Test_machineHashesWithStep(t *testing.T) { expectedHashes := make([]common.Hash, 0) for i := uint64(0); i < 4; i++ { if i == 0 { - expectedHashes = append(expectedHashes, machineFinishedHash(initialGs)) + expectedHashes = append(expectedHashes, initialGs.Hash()) continue } gs := validator.GoGlobalState{ @@ -190,11 +191,11 @@ func Test_machineHashesWithStep(t *testing.T) { } expectedHashes = append(expectedHashes, gs.Hash()) } - expectedHashes = append(expectedHashes, machineFinishedHash(validator.GoGlobalState{ + expectedHashes = append(expectedHashes, validator.GoGlobalState{ Batch: 1, PosInBatch: mm.totalSteps - 1, - })) - if len(hashes) >= int(maxIterations) { + }.Hash()) + if uint64(len(hashes)) >= maxIterations { t.Fatal("Wanted fewer hashes than the max iterations") } for i := range hashes { diff --git a/validator/server_arb/machine.go b/validator/server_arb/machine.go index adca9695e2..e4e07d3c2d 100644 --- a/validator/server_arb/machine.go +++ b/validator/server_arb/machine.go @@ -4,12 +4,13 @@ package server_arb /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #include "arbitrator.h" ResolvedPreimage preimageResolverC(size_t context, uint8_t preimageType, const uint8_t* hash); */ import "C" + import ( "context" "errors" @@ -21,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/containers" @@ -37,6 +39,7 @@ type MachineInterface interface { CloneMachineInterface() MachineInterface GetStepCount() uint64 IsRunning() bool + IsErrored() bool ValidForStep(uint64) bool Status() uint8 Step(context.Context, uint64) error @@ -51,16 +54,32 @@ type MachineInterface interface { type ArbitratorMachine struct { mutex sync.Mutex // needed because go finalizers don't synchronize (meaning they aren't thread safe) ptr *C.struct_Machine - contextId *int64 // has a finalizer attached to remove the preimage resolver from the global map - frozen bool // does not allow anything that changes machine state, not cloned with the machine + contextId *int64 + frozen bool // does not allow anything that changes machine state, not cloned with the machine } // Assert that ArbitratorMachine implements MachineInterface var _ MachineInterface = (*ArbitratorMachine)(nil) -var preimageResolvers containers.SyncMap[int64, GoPreimageResolver] +var preimageResolvers containers.SyncMap[int64, goPreimageResolverWithRefCounter] var lastPreimageResolverId atomic.Int64 // atomic +func dereferenceContextId(contextId *int64) { + if contextId != nil { + resolverWithRefCounter, ok := preimageResolvers.Load(*contextId) + if !ok { + panic(fmt.Sprintf("dereferenceContextId: resolver with ref counter not found, contextId: %v", *contextId)) + } + + refCount := resolverWithRefCounter.refCounter.Add(-1) + if refCount < 0 { + panic(fmt.Sprintf("dereferenceContextId: ref counter is negative, contextId: %v", *contextId)) + } else if refCount == 0 { + preimageResolvers.Delete(*contextId) + } + } +} + // Any future calls to this machine will result in a panic func (m *ArbitratorMachine) Destroy() { m.mutex.Lock() @@ -71,11 +90,9 @@ func (m *ArbitratorMachine) Destroy() { // We no longer need a finalizer runtime.SetFinalizer(m, nil) } - m.contextId = nil -} -func freeContextId(context *int64) { - preimageResolvers.Delete(*context) + dereferenceContextId(m.contextId) + m.contextId = nil } func machineFromPointer(ptr *C.struct_Machine) *ArbitratorMachine { @@ -101,6 +118,14 @@ func LoadSimpleMachine(wasm string, libraries []string, debugChain bool) (*Arbit return machineFromPointer(mach), nil } +func NewFinishedMachine(gs validator.GoGlobalState) *ArbitratorMachine { + mach := C.arbitrator_new_finished(GlobalStateToC(gs)) + if mach == nil { + return nil + } + return machineFromPointer(mach) +} + func (m *ArbitratorMachine) Freeze() { m.frozen = true } @@ -112,6 +137,16 @@ func (m *ArbitratorMachine) Clone() *ArbitratorMachine { defer m.mutex.Unlock() newMach := machineFromPointer(C.arbitrator_clone_machine(m.ptr)) newMach.contextId = m.contextId + + if m.contextId != nil { + resolverWithRefCounter, ok := preimageResolvers.Load(*m.contextId) + if ok { + resolverWithRefCounter.refCounter.Add(1) + } else { + panic(fmt.Sprintf("Clone: resolver with ref counter not found, contextId: %v", *m.contextId)) + } + } + return newMach } @@ -269,9 +304,13 @@ func (m *ArbitratorMachine) ProveNextStep() []byte { m.mutex.Lock() defer m.mutex.Unlock() - rustProof := C.arbitrator_gen_proof(m.ptr) - proofBytes := C.GoBytes(unsafe.Pointer(rustProof.ptr), C.int(rustProof.len)) - C.arbitrator_free_proof(rustProof) + output := &C.RustBytes{} + C.arbitrator_gen_proof(m.ptr, output) + defer C.free_rust_bytes(*output) + if output.len == 0 { + return nil + } + proofBytes := C.GoBytes(unsafe.Pointer(output.ptr), C.int(output.len)) return proofBytes } @@ -350,19 +389,24 @@ func (m *ArbitratorMachine) AddDelayedInboxMessage(index uint64, data []byte) er } type GoPreimageResolver = func(arbutil.PreimageType, common.Hash) ([]byte, error) +type goPreimageResolverWithRefCounter struct { + resolver GoPreimageResolver + refCounter *atomic.Int64 +} //export preimageResolver func preimageResolver(context C.size_t, ty C.uint8_t, ptr unsafe.Pointer) C.ResolvedPreimage { var hash common.Hash input := (*[1 << 30]byte)(ptr)[:32] copy(hash[:], input) - resolver, ok := preimageResolvers.Load(int64(context)) + resolverWithRefCounter, ok := preimageResolvers.Load(int64(context)) if !ok { + log.Error("preimageResolver: resolver with ref counter not found", "context", int64(context)) return C.ResolvedPreimage{ len: -1, } } - preimage, err := resolver(arbutil.PreimageType(ty), hash) + preimage, err := resolverWithRefCounter.resolver(arbutil.PreimageType(ty), hash) if err != nil { log.Error("preimage resolution failed", "err", err) return C.ResolvedPreimage{ @@ -382,10 +426,18 @@ func (m *ArbitratorMachine) SetPreimageResolver(resolver GoPreimageResolver) err if m.frozen { return errors.New("machine frozen") } + dereferenceContextId(m.contextId) + id := lastPreimageResolverId.Add(1) - preimageResolvers.Store(id, resolver) + refCounter := atomic.Int64{} + refCounter.Store(1) + resolverWithRefCounter := goPreimageResolverWithRefCounter{ + resolver: resolver, + refCounter: &refCounter, + } + preimageResolvers.Store(id, resolverWithRefCounter) + m.contextId = &id - runtime.SetFinalizer(m.contextId, freeContextId) C.arbitrator_set_context(m.ptr, u64(id)) return nil } diff --git a/validator/server_arb/machine_cache.go b/validator/server_arb/machine_cache.go index 23fcdef6d6..35f3406236 100644 --- a/validator/server_arb/machine_cache.go +++ b/validator/server_arb/machine_cache.go @@ -31,7 +31,7 @@ type MachineCache struct { } type MachineCacheConfig struct { - CachedChallengeMachines int `koanf:"cached-challenge-machines"` + CachedChallengeMachines uint64 `koanf:"cached-challenge-machines"` InitialSteps uint64 `koanf:"initial-steps"` } @@ -42,7 +42,7 @@ var DefaultMachineCacheConfig = MachineCacheConfig{ func MachineCacheConfigConfigAddOptions(prefix string, f *flag.FlagSet) { f.Uint64(prefix+".initial-steps", DefaultMachineCacheConfig.InitialSteps, "initial steps between machines") - f.Int(prefix+".cached-challenge-machines", DefaultMachineCacheConfig.CachedChallengeMachines, "how many machines to store in cache while working on a challenge (should be even)") + f.Uint64(prefix+".cached-challenge-machines", DefaultMachineCacheConfig.CachedChallengeMachines, "how many machines to store in cache while working on a challenge (should be even)") } // `initialMachine` won't be mutated by this function. @@ -140,7 +140,7 @@ func (c *MachineCache) unlockBuild(err error) { } func (c *MachineCache) setRangeLocked(ctx context.Context, start uint64, end uint64) error { - newInterval := (end - start) / uint64(c.config.CachedChallengeMachines) + newInterval := (end - start) / c.config.CachedChallengeMachines if newInterval == 0 { newInterval = 2 } @@ -150,7 +150,7 @@ func (c *MachineCache) setRangeLocked(ctx context.Context, start uint64, end uin if end >= c.finalMachineStep { end = c.finalMachineStep - newInterval/2 } - newInterval = (end - start) / uint64(c.config.CachedChallengeMachines) + newInterval = (end - start) / c.config.CachedChallengeMachines if newInterval == 0 { newInterval = 1 } @@ -212,7 +212,7 @@ func (c *MachineCache) populateCache(ctx context.Context) error { if nextMachine.GetStepCount()+c.machineStepInterval >= c.finalMachineStep { break } - if len(c.machines) >= c.config.CachedChallengeMachines { + if uint64(len(c.machines)) >= c.config.CachedChallengeMachines { break } nextMachine = nextMachine.CloneMachineInterface() @@ -236,9 +236,11 @@ func (c *MachineCache) getClosestMachine(stepCount uint64) (int, MachineInterfac } stepsFromStart := stepCount - c.firstMachineStep var index int + // #nosec G115 if c.machineStepInterval == 0 || stepsFromStart > c.machineStepInterval*uint64(len(c.machines)-1) { index = len(c.machines) - 1 } else { + // #nosec G115 index = int(stepsFromStart / c.machineStepInterval) } return index, c.machines[index] diff --git a/validator/server_arb/machine_loader.go b/validator/server_arb/machine_loader.go index 13cf0f2403..8c9d37e174 100644 --- a/validator/server_arb/machine_loader.go +++ b/validator/server_arb/machine_loader.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator/server_common" ) diff --git a/validator/server_arb/machine_test.go b/validator/server_arb/machine_test.go new file mode 100644 index 0000000000..008d757889 --- /dev/null +++ b/validator/server_arb/machine_test.go @@ -0,0 +1,94 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package server_arb + +import ( + "path" + "reflect" + "runtime" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func TestEntriesAreDeletedFromPreimageResolversGlobalMap(t *testing.T) { + resolver := func(arbutil.PreimageType, common.Hash) ([]byte, error) { + return nil, nil + } + + sortedKeys := func() []int64 { + keys := preimageResolvers.Keys() + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + return keys + } + + // clear global map before running test + preimageKeys := sortedKeys() + for _, key := range preimageKeys { + preimageResolvers.Delete(key) + } + + _, filename, _, _ := runtime.Caller(0) + wasmDir := path.Join(path.Dir(filename), "../../arbitrator/prover/test-cases/") + wasmPath := path.Join(wasmDir, "global-state.wasm") + modulePaths := []string{path.Join(wasmDir, "global-state-wrapper.wasm")} + + machine1, err := LoadSimpleMachine(wasmPath, modulePaths, true) + testhelpers.RequireImpl(t, err) + err = machine1.SetPreimageResolver(resolver) + testhelpers.RequireImpl(t, err) + + machine2, err := LoadSimpleMachine(wasmPath, modulePaths, true) + testhelpers.RequireImpl(t, err) + err = machine2.SetPreimageResolver(resolver) + testhelpers.RequireImpl(t, err) + + machine1Clone1 := machine1.Clone() + machine1Clone2 := machine1.Clone() + + checkKeys := func(expectedKeys []int64, scenario string) { + keys := sortedKeys() + if !reflect.DeepEqual(keys, expectedKeys) { + t.Fatal("Unexpected preimageResolversKeys got", keys, "expected", expectedKeys, "scenario", scenario) + } + } + + machine1ContextId := *machine1.contextId + machine2ContextId := *machine2.contextId + + checkKeys([]int64{machine1ContextId, machine2ContextId}, "initial") + + // the machine's contextId should change when setting preimage resolver for the second time, + // and the entry for the old context id should be deleted + err = machine2.SetPreimageResolver(resolver) + testhelpers.RequireImpl(t, err) + if machine2ContextId == *machine2.contextId { + t.Fatal("Context id didn't change after setting preimage resolver for the second time") + } + machine2ContextId = *machine2.contextId + checkKeys([]int64{machine1ContextId, machine2ContextId}, "after setting preimage resolver for machine2 for the second time") + + machine1Clone1.Destroy() + checkKeys([]int64{machine1ContextId, machine2ContextId}, "after machine1Clone1 is destroyed") + + machine1.Destroy() + checkKeys([]int64{machine1ContextId, machine2ContextId}, "after machine1 is destroyed") + + // it is possible to destroy the same machine multiple times + machine1.Destroy() + checkKeys([]int64{machine1ContextId, machine2ContextId}, "after machine1 is destroyed again") + + // entry for machine1ContextId should be deleted only after machine1 and all its clones are destroyed + machine1Clone2.Destroy() + checkKeys([]int64{machine2ContextId}, "after machine1Clone2 is destroyed") + + machine2.Destroy() + checkKeys([]int64{}, "after machine2 is destroyed") +} diff --git a/validator/server_arb/nitro_machine.go b/validator/server_arb/nitro_machine.go index 2b2cb230b6..a2f7de3153 100644 --- a/validator/server_arb/nitro_machine.go +++ b/validator/server_arb/nitro_machine.go @@ -4,11 +4,12 @@ package server_arb /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #include "arbitrator.h" #include */ import "C" + import ( "context" "errors" @@ -19,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/validator/server_common" ) diff --git a/validator/server_arb/preimage_resolver.go b/validator/server_arb/preimage_resolver.go index cd4ea40e28..f01d79f4dd 100644 --- a/validator/server_arb/preimage_resolver.go +++ b/validator/server_arb/preimage_resolver.go @@ -4,7 +4,7 @@ package server_arb /* -#cgo CFLAGS: -g -Wall -I../../target/include/ +#cgo CFLAGS: -g -I../../target/include/ #include "arbitrator.h" extern ResolvedPreimage preimageResolver(size_t context, uint8_t preimageType, const uint8_t* hash); diff --git a/validator/server_arb/prover_interface.go b/validator/server_arb/prover_interface.go index bdd81ed588..8479a8aa8f 100644 --- a/validator/server_arb/prover_interface.go +++ b/validator/server_arb/prover_interface.go @@ -4,7 +4,7 @@ package server_arb /* -#cgo CFLAGS: -g -Wall -I../target/include/ +#cgo CFLAGS: -g -I../target/include/ #cgo LDFLAGS: ${SRCDIR}/../../target/lib/libstylus.a -ldl -lm #include "arbitrator.h" #include @@ -22,10 +22,12 @@ void AddToStringList(char** list, int index, char* val) { } */ import "C" + import ( "unsafe" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator" ) diff --git a/validator/server_arb/validator_spawner.go b/validator/server_arb/validator_spawner.go index 844a988d28..4c74bca695 100644 --- a/validator/server_arb/validator_spawner.go +++ b/validator/server_arb/validator_spawner.go @@ -2,28 +2,26 @@ package server_arb import ( "context" - "encoding/binary" "errors" "fmt" - "os" - "path/filepath" "runtime" "sync/atomic" "time" "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_common" "github.com/offchainlabs/nitro/validator/valnode/redis" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" ) var arbitratorValidationSteps = metrics.NewRegisteredHistogram("arbitrator/validation/steps", nil, metrics.NewBoundedHistogramSample()) @@ -58,20 +56,42 @@ func DefaultArbitratorSpawnerConfigFetcher() *ArbitratorSpawnerConfig { return &DefaultArbitratorSpawnerConfig } +// MachineWrapper is a function that wraps a MachineInterface +// +// This is a mechanism to allow clients of the AribtratorSpawner to inject +// functionality around the arbitrator machine. Possible use cases include +// mocking out the machine for testing purposes, or having the machine behave +// differently when certain features (like BoLD) are enabled. +type MachineWrapper func(MachineInterface) MachineInterface + +type SpawnerOption func(*ArbitratorSpawner) + type ArbitratorSpawner struct { stopwaiter.StopWaiter count atomic.Int32 locator *server_common.MachineLocator machineLoader *ArbMachineLoader - config ArbitratorSpawnerConfigFecher + // Oreder of wrappers is important. The first wrapper is the innermost. + machineWrappers []MachineWrapper + config ArbitratorSpawnerConfigFecher } -func NewArbitratorSpawner(locator *server_common.MachineLocator, config ArbitratorSpawnerConfigFecher) (*ArbitratorSpawner, error) { +func WithWrapper(wrapper MachineWrapper) SpawnerOption { + return func(s *ArbitratorSpawner) { + s.machineWrappers = append(s.machineWrappers, wrapper) + } +} + +func NewArbitratorSpawner(locator *server_common.MachineLocator, config ArbitratorSpawnerConfigFecher, opts ...SpawnerOption) (*ArbitratorSpawner, error) { // TODO: preload machines spawner := &ArbitratorSpawner{ - locator: locator, - machineLoader: NewArbMachineLoader(&DefaultArbitratorMachineConfig, locator), - config: config, + locator: locator, + machineLoader: NewArbMachineLoader(&DefaultArbitratorMachineConfig, locator), + machineWrappers: make([]MachineWrapper, 0), + config: config, + } + for _, opt := range opts { + opt(spawner) } return spawner, nil } @@ -89,15 +109,15 @@ func (s *ArbitratorSpawner) WasmModuleRoots() ([]common.Hash, error) { return s.locator.ModuleRoots(), nil } -func (s *ArbitratorSpawner) StylusArchs() []rawdb.Target { - return []rawdb.Target{rawdb.TargetWavm} +func (s *ArbitratorSpawner) StylusArchs() []ethdb.WasmTarget { + return []ethdb.WasmTarget{rawdb.TargetWavm} } func (s *ArbitratorSpawner) Name() string { return "arbitrator" } -func (v *ArbitratorSpawner) loadEntryToMachine(ctx context.Context, entry *validator.ValidationInput, mach *ArbitratorMachine) error { +func (v *ArbitratorSpawner) loadEntryToMachine(_ context.Context, entry *validator.ValidationInput, mach *ArbitratorMachine) error { resolver := func(ty arbutil.PreimageType, hash common.Hash) ([]byte, error) { // Check if it's a known preimage if preimage, ok := entry.Preimages[ty][hash]; ok { @@ -161,12 +181,16 @@ func (v *ArbitratorSpawner) execute( return validator.GoGlobalState{}, fmt.Errorf("unabled to get WASM machine: %w", err) } - mach := basemachine.Clone() - defer mach.Destroy() - err = v.loadEntryToMachine(ctx, entry, mach) + arbMach := basemachine.Clone() + defer arbMach.Destroy() + err = v.loadEntryToMachine(ctx, entry, arbMach) if err != nil { return validator.GoGlobalState{}, err } + var mach MachineInterface = arbMach + for _, wrapper := range v.machineWrappers { + mach = wrapper(mach) + } var steps uint64 for mach.IsRunning() { var count uint64 = 500000000 @@ -179,7 +203,10 @@ func (v *ArbitratorSpawner) execute( } steps += count } + + // #nosec G115 arbitratorValidationSteps.Update(int64(mach.GetStepCount())) + if mach.IsErrored() { log.Error("machine entered errored state during attempted validation", "block", entry.Id) return validator.GoGlobalState{}, errors.New("machine entered errored state during attempted validation") @@ -189,7 +216,7 @@ func (v *ArbitratorSpawner) execute( func (v *ArbitratorSpawner) Launch(entry *validator.ValidationInput, moduleRoot common.Hash) validator.ValidationRun { v.count.Add(1) - promise := stopwaiter.LaunchPromiseThread[validator.GoGlobalState](v, func(ctx context.Context) (validator.GoGlobalState, error) { + promise := stopwaiter.LaunchPromiseThread(v, func(ctx context.Context) (validator.GoGlobalState, error) { defer v.count.Add(-1) return v.execute(ctx, entry, moduleRoot) }) @@ -204,140 +231,7 @@ func (v *ArbitratorSpawner) Room() int { return avail } -var launchTime = time.Now().Format("2006_01_02__15_04") - -//nolint:gosec -func (v *ArbitratorSpawner) writeToFile(ctx context.Context, input *validator.ValidationInput, expOut validator.GoGlobalState, moduleRoot common.Hash) error { - outDirPath := filepath.Join(v.locator.RootPath(), v.config().OutputPath, launchTime, fmt.Sprintf("block_%d", input.Id)) - err := os.MkdirAll(outDirPath, 0755) - if err != nil { - return err - } - if ctx.Err() != nil { - return ctx.Err() - } - - rootPathAssign := "" - if executable, err := os.Executable(); err == nil { - rootPathAssign = "ROOTPATH=\"" + filepath.Dir(executable) + "\"\n" - } - cmdFile, err := os.OpenFile(filepath.Join(outDirPath, "run-prover.sh"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer cmdFile.Close() - _, err = cmdFile.WriteString("#!/bin/bash\n" + - fmt.Sprintf("# expected output: batch %d, postion %d, hash %s\n", expOut.Batch, expOut.PosInBatch, expOut.BlockHash) + - "MACHPATH=\"" + v.locator.GetMachinePath(moduleRoot) + "\"\n" + - rootPathAssign + - "if (( $# > 1 )); then\n" + - " if [[ $1 == \"-m\" ]]; then\n" + - " MACHPATH=$2\n" + - " shift\n" + - " shift\n" + - " fi\n" + - "fi\n" + - "${ROOTPATH}/bin/prover ${MACHPATH}/replay.wasm") - if err != nil { - return err - } - if ctx.Err() != nil { - return ctx.Err() - } - - libraries := []string{"soft-float.wasm", "wasi_stub.wasm", "go_stub.wasm", "host_io.wasm", "brotli.wasm"} - for _, module := range libraries { - _, err = cmdFile.WriteString(" -l " + "${MACHPATH}/" + module) - if err != nil { - return err - } - } - _, err = cmdFile.WriteString(fmt.Sprintf(" --inbox-position %d --position-within-message %d --last-block-hash %s", input.StartState.Batch, input.StartState.PosInBatch, input.StartState.BlockHash)) - if err != nil { - return err - } - - for _, msg := range input.BatchInfo { - if ctx.Err() != nil { - return ctx.Err() - } - sequencerFileName := fmt.Sprintf("sequencer_%d.bin", msg.Number) - err = os.WriteFile(filepath.Join(outDirPath, sequencerFileName), msg.Data, 0644) - if err != nil { - return err - } - _, err = cmdFile.WriteString(" --inbox " + sequencerFileName) - if err != nil { - return err - } - } - - preimageFile, err := os.Create(filepath.Join(outDirPath, "preimages.bin")) - if err != nil { - return err - } - defer preimageFile.Close() - for ty, preimages := range input.Preimages { - _, err = preimageFile.Write([]byte{byte(ty)}) - if err != nil { - return err - } - for _, data := range preimages { - if ctx.Err() != nil { - return ctx.Err() - } - lenbytes := make([]byte, 8) - binary.LittleEndian.PutUint64(lenbytes, uint64(len(data))) - _, err := preimageFile.Write(lenbytes) - if err != nil { - return err - } - _, err = preimageFile.Write(data) - if err != nil { - return err - } - } - } - - _, err = cmdFile.WriteString(" --preimages preimages.bin") - if err != nil { - return err - } - - if input.HasDelayedMsg { - if ctx.Err() != nil { - return ctx.Err() - } - _, err = cmdFile.WriteString(fmt.Sprintf(" --delayed-inbox-position %d", input.DelayedMsgNr)) - if err != nil { - return err - } - filename := fmt.Sprintf("delayed_%d.bin", input.DelayedMsgNr) - err = os.WriteFile(filepath.Join(outDirPath, filename), input.DelayedMsg, 0644) - if err != nil { - return err - } - _, err = cmdFile.WriteString(fmt.Sprintf(" --delayed-inbox %s", filename)) - if err != nil { - return err - } - } - - _, err = cmdFile.WriteString(" \"$@\"\n") - if err != nil { - return err - } - return nil -} - -func (v *ArbitratorSpawner) WriteToFile(input *validator.ValidationInput, expOut validator.GoGlobalState, moduleRoot common.Hash) containers.PromiseInterface[struct{}] { - return stopwaiter.LaunchPromiseThread[struct{}](v, func(ctx context.Context) (struct{}, error) { - err := v.writeToFile(ctx, input, expOut, moduleRoot) - return struct{}{}, err - }) -} - -func (v *ArbitratorSpawner) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { +func (v *ArbitratorSpawner) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput, useBoldMachine bool) containers.PromiseInterface[validator.ExecutionRun] { getMachine := func(ctx context.Context) (MachineInterface, error) { initialFrozenMachine, err := v.machineLoader.GetZeroStepMachine(ctx, wasmModuleRoot) if err != nil { @@ -349,7 +243,16 @@ func (v *ArbitratorSpawner) CreateExecutionRun(wasmModuleRoot common.Hash, input machine.Destroy() return nil, err } - return machine, nil + var wrapped MachineInterface + if useBoldMachine { + wrapped = BoldMachineWrapper(machine) + } else { + wrapped = MachineInterface(machine) + } + for _, wrapper := range v.machineWrappers { + wrapped = wrapper(wrapped) + } + return wrapped, nil } currentExecConfig := v.config().Execution return stopwaiter.LaunchPromiseThread[validator.ExecutionRun](v, func(ctx context.Context) (validator.ExecutionRun, error) { diff --git a/validator/server_common/machine_loader.go b/validator/server_common/machine_loader.go index f4633ebedf..e86589b657 100644 --- a/validator/server_common/machine_loader.go +++ b/validator/server_common/machine_loader.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/util/containers" ) diff --git a/validator/server_common/valrun.go b/validator/server_common/valrun.go index 8486664008..9a2a6cb414 100644 --- a/validator/server_common/valrun.go +++ b/validator/server_common/valrun.go @@ -2,6 +2,7 @@ package server_common import ( "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/validator" ) diff --git a/validator/server_jit/jit_machine.go b/validator/server_jit/jit_machine.go index 23a75bba83..dc7657441e 100644 --- a/validator/server_jit/jit_machine.go +++ b/validator/server_jit/jit_machine.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "math" "net" "os" "os/exec" @@ -18,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/validator" ) @@ -29,9 +31,10 @@ type JitMachine struct { process *exec.Cmd stdin io.WriteCloser wasmMemoryUsageLimit int + maxExecutionTime time.Duration } -func createJitMachine(jitBinary string, binaryPath string, cranelift bool, wasmMemoryUsageLimit int, moduleRoot common.Hash, fatalErrChan chan error) (*JitMachine, error) { +func createJitMachine(jitBinary string, binaryPath string, cranelift bool, wasmMemoryUsageLimit int, maxExecutionTime time.Duration, _ common.Hash, fatalErrChan chan error) (*JitMachine, error) { invocation := []string{"--binary", binaryPath, "--forks"} if cranelift { invocation = append(invocation, "--cranelift") @@ -54,6 +57,7 @@ func createJitMachine(jitBinary string, binaryPath string, cranelift bool, wasmM process: process, stdin: stdin, wasmMemoryUsageLimit: wasmMemoryUsageLimit, + maxExecutionTime: maxExecutionTime, } return machine, nil } @@ -72,7 +76,7 @@ func (machine *JitMachine) prove( defer cancel() // ensure our cleanup functions run when we're done state := validator.GoGlobalState{} - timeout := time.Now().Add(60 * time.Second) + timeout := time.Now().Add(machine.maxExecutionTime) tcp, err := net.ListenTCP("tcp4", &net.TCPAddr{ IP: []byte{127, 0, 0, 1}, }) @@ -125,6 +129,13 @@ func (machine *JitMachine) prove( writeUint32 := func(data uint32) error { return writeExact(arbmath.Uint32ToBytes(data)) } + writeIntAsUint32 := func(data int) error { + if data < 0 || data > math.MaxUint32 { + return fmt.Errorf("attempted to write out-of-bounds int %v as uint32", data) + } + // #nosec G115 + return writeUint32(uint32(data)) + } writeUint64 := func(data uint64) error { return writeExact(arbmath.UintToBytes(data)) } @@ -192,14 +203,14 @@ func (machine *JitMachine) prove( // send known preimages preimageTypes := entry.Preimages - if err := writeUint32(uint32(len(preimageTypes))); err != nil { + if err := writeIntAsUint32(len(preimageTypes)); err != nil { return state, err } for ty, preimages := range preimageTypes { if err := writeUint8(uint8(ty)); err != nil { return state, err } - if err := writeUint32(uint32(len(preimages))); err != nil { + if err := writeIntAsUint32(len(preimages)); err != nil { return state, err } for hash, preimage := range preimages { @@ -224,7 +235,7 @@ func (machine *JitMachine) prove( } } - if err := writeUint32(uint32(len(userWasms))); err != nil { + if err := writeIntAsUint32(len(userWasms)); err != nil { return state, err } for moduleHash, program := range userWasms { @@ -298,9 +309,11 @@ func (machine *JitMachine) prove( if err != nil { return state, fmt.Errorf("failed to read memory usage from Jit machine: %w", err) } + // #nosec G115 if memoryUsed > uint64(machine.wasmMemoryUsageLimit) { log.Warn("memory used by jit wasm exceeds the wasm memory usage limit", "limit", machine.wasmMemoryUsageLimit, "memoryUsed", memoryUsed) } + // #nosec G115 jitWasmMemoryUsage.Update(int64(memoryUsed)) return state, nil default: diff --git a/validator/server_jit/machine_loader.go b/validator/server_jit/machine_loader.go index cfa475370c..a4ccede324 100644 --- a/validator/server_jit/machine_loader.go +++ b/validator/server_jit/machine_loader.go @@ -7,8 +7,10 @@ import ( "path/filepath" "runtime" "strings" + "time" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/validator/server_common" ) @@ -52,14 +54,14 @@ type JitMachineLoader struct { stopped bool } -func NewJitMachineLoader(config *JitMachineConfig, locator *server_common.MachineLocator, fatalErrChan chan error) (*JitMachineLoader, error) { +func NewJitMachineLoader(config *JitMachineConfig, locator *server_common.MachineLocator, maxExecutionTime time.Duration, fatalErrChan chan error) (*JitMachineLoader, error) { jitPath, err := getJitPath() if err != nil { return nil, err } createMachineThreadFunc := func(ctx context.Context, moduleRoot common.Hash) (*JitMachine, error) { binPath := filepath.Join(locator.GetMachinePath(moduleRoot), config.ProverBinPath) - return createJitMachine(jitPath, binPath, config.JitCranelift, config.WasmMemoryUsageLimit, moduleRoot, fatalErrChan) + return createJitMachine(jitPath, binPath, config.JitCranelift, config.WasmMemoryUsageLimit, maxExecutionTime, moduleRoot, fatalErrChan) } return &JitMachineLoader{ MachineLoader: *server_common.NewMachineLoader[JitMachine](locator, createMachineThreadFunc), diff --git a/validator/server_jit/spawner.go b/validator/server_jit/spawner.go index 92b50b17cb..91b1e818f0 100644 --- a/validator/server_jit/spawner.go +++ b/validator/server_jit/spawner.go @@ -5,11 +5,13 @@ import ( "fmt" "runtime" "sync/atomic" + "time" flag "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" @@ -17,8 +19,9 @@ import ( ) type JitSpawnerConfig struct { - Workers int `koanf:"workers" reload:"hot"` - Cranelift bool `koanf:"cranelift"` + Workers int `koanf:"workers" reload:"hot"` + Cranelift bool `koanf:"cranelift"` + MaxExecutionTime time.Duration `koanf:"max-execution-time" reload:"hot"` // TODO: change WasmMemoryUsageLimit to a string and use resourcemanager.ParseMemLimit WasmMemoryUsageLimit int `koanf:"wasm-memory-usage-limit"` @@ -29,6 +32,7 @@ type JitSpawnerConfigFecher func() *JitSpawnerConfig var DefaultJitSpawnerConfig = JitSpawnerConfig{ Workers: 0, Cranelift: true, + MaxExecutionTime: time.Minute * 10, WasmMemoryUsageLimit: 4294967296, // 2^32 WASM memeory limit } @@ -36,6 +40,7 @@ func JitSpawnerConfigAddOptions(prefix string, f *flag.FlagSet) { f.Int(prefix+".workers", DefaultJitSpawnerConfig.Workers, "number of concurrent validation threads") f.Bool(prefix+".cranelift", DefaultJitSpawnerConfig.Cranelift, "use Cranelift instead of LLVM when validating blocks using the jit-accelerated block validator") f.Int(prefix+".wasm-memory-usage-limit", DefaultJitSpawnerConfig.WasmMemoryUsageLimit, "if memory used by a jit wasm exceeds this limit, a warning is logged") + f.Duration(prefix+".max-execution-time", DefaultJitSpawnerConfig.MaxExecutionTime, "if execution time used by a jit wasm exceeds this limit, a rpc error is returned") } type JitSpawner struct { @@ -51,7 +56,8 @@ func NewJitSpawner(locator *server_common.MachineLocator, config JitSpawnerConfi machineConfig := DefaultJitMachineConfig machineConfig.JitCranelift = config().Cranelift machineConfig.WasmMemoryUsageLimit = config().WasmMemoryUsageLimit - loader, err := NewJitMachineLoader(&machineConfig, locator, fatalErrChan) + maxExecutionTime := config().MaxExecutionTime + loader, err := NewJitMachineLoader(&machineConfig, locator, maxExecutionTime, fatalErrChan) if err != nil { return nil, err } @@ -72,8 +78,8 @@ func (v *JitSpawner) WasmModuleRoots() ([]common.Hash, error) { return v.locator.ModuleRoots(), nil } -func (v *JitSpawner) StylusArchs() []rawdb.Target { - return []rawdb.Target{rawdb.LocalTarget()} +func (v *JitSpawner) StylusArchs() []ethdb.WasmTarget { + return []ethdb.WasmTarget{rawdb.LocalTarget()} } func (v *JitSpawner) execute( diff --git a/validator/validation_entry.go b/validator/validation_entry.go index 2c357659ad..555a4c76c7 100644 --- a/validator/validation_entry.go +++ b/validator/validation_entry.go @@ -2,14 +2,14 @@ package validator import ( "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/offchainlabs/nitro/arbutil" ) type BatchInfo struct { - Number uint64 - BlockHash common.Hash - Data []byte + Number uint64 + Data []byte } type ValidationInput struct { @@ -17,7 +17,7 @@ type ValidationInput struct { HasDelayedMsg bool DelayedMsgNr uint64 Preimages map[arbutil.PreimageType]map[common.Hash][]byte - UserWasms map[rawdb.Target]map[common.Hash][]byte + UserWasms map[ethdb.WasmTarget]map[common.Hash][]byte BatchInfo []BatchInfo DelayedMsg []byte StartState GoGlobalState diff --git a/validator/valnode/redis/consumer.go b/validator/valnode/redis/consumer.go index fb7db1e870..93b3eddd3f 100644 --- a/validator/valnode/redis/consumer.go +++ b/validator/valnode/redis/consumer.go @@ -3,16 +3,19 @@ package redis import ( "context" "fmt" + "runtime" "time" + "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/pubsub" "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_api" - "github.com/spf13/pflag" ) // ValidationServer implements consumer for the requests originated from @@ -22,8 +25,9 @@ type ValidationServer struct { spawner validator.ValidationSpawner // consumers stores moduleRoot to consumer mapping. - consumers map[common.Hash]*pubsub.Consumer[*validator.ValidationInput, validator.GoGlobalState] - streamTimeout time.Duration + consumers map[common.Hash]*pubsub.Consumer[*validator.ValidationInput, validator.GoGlobalState] + + config *ValidationServerConfig } func NewValidationServer(cfg *ValidationServerConfig, spawner validator.ValidationSpawner) (*ValidationServer, error) { @@ -44,9 +48,9 @@ func NewValidationServer(cfg *ValidationServerConfig, spawner validator.Validati consumers[mr] = c } return &ValidationServer{ - consumers: consumers, - spawner: spawner, - streamTimeout: cfg.StreamTimeout, + consumers: consumers, + spawner: spawner, + config: cfg, }, nil } @@ -54,6 +58,23 @@ func (s *ValidationServer) Start(ctx_in context.Context) { s.StopWaiter.Start(ctx_in, s) // Channel that all consumers use to indicate their readiness. readyStreams := make(chan struct{}, len(s.consumers)) + type workUnit struct { + req *pubsub.Message[*validator.ValidationInput] + moduleRoot common.Hash + } + workers := s.config.Workers + if workers == 0 { + workers = runtime.NumCPU() + } + workQueue := make(chan workUnit, workers) + tokensCount := workers + if s.config.BufferReads { + tokensCount += workers + } + requestTokenQueue := make(chan struct{}, tokensCount) + for i := 0; i < tokensCount; i++ { + requestTokenQueue <- struct{}{} + } for moduleRoot, c := range s.consumers { c := c moduleRoot := moduleRoot @@ -84,26 +105,31 @@ func (s *ValidationServer) Start(ctx_in context.Context) { case <-ready: // Wait until the stream exists and start consuming iteratively. } s.StopWaiter.CallIteratively(func(ctx context.Context) time.Duration { + log.Debug("waiting for request token", "cid", c.Id()) + select { + case <-ctx.Done(): + return 0 + case <-requestTokenQueue: + } + log.Debug("got request token", "cid", c.Id()) req, err := c.Consume(ctx) if err != nil { log.Error("Consuming request", "error", err) + requestTokenQueue <- struct{}{} return 0 } if req == nil { - // There's nothing in the queue. + log.Debug("consumed nil", "cid", c.Id()) + // There's nothing in the queue + requestTokenQueue <- struct{}{} return time.Second } - valRun := s.spawner.Launch(req.Value, moduleRoot) - res, err := valRun.Await(ctx) - if err != nil { - log.Error("Error validating", "request value", req.Value, "error", err) - return 0 - } - if err := c.SetResult(ctx, req.ID, res); err != nil { - log.Error("Error setting result for request", "id", req.ID, "result", res, "error", err) - return 0 + log.Debug("forwarding work", "cid", c.Id(), "workid", req.ID) + select { + case <-ctx.Done(): + case workQueue <- workUnit{req, moduleRoot}: } - return time.Second + return 0 }) }) } @@ -111,9 +137,9 @@ func (s *ValidationServer) Start(ctx_in context.Context) { for { select { case <-readyStreams: - log.Trace("At least one stream is ready") + log.Debug("At least one stream is ready") return // Don't block Start if at least one of the stream is ready. - case <-time.After(s.streamTimeout): + case <-time.After(s.config.StreamTimeout): log.Error("Waiting for redis streams timed out") case <-ctx.Done(): log.Info("Context done while waiting redis streams to be ready, failed to start") @@ -121,6 +147,41 @@ func (s *ValidationServer) Start(ctx_in context.Context) { } } }) + for i := 0; i < workers; i++ { + i := i + s.StopWaiter.LaunchThread(func(ctx context.Context) { + for { + log.Debug("waiting for work", "thread", i) + var work workUnit + select { + case <-ctx.Done(): + return + case work = <-workQueue: + } + log.Debug("got work", "thread", i, "workid", work.req.ID) + valRun := s.spawner.Launch(work.req.Value, work.moduleRoot) + res, err := valRun.Await(ctx) + if err != nil { + log.Error("Error validating", "request value", work.req.Value, "error", err) + work.req.Ack() + } else { + log.Debug("done work", "thread", i, "workid", work.req.ID) + err := s.consumers[work.moduleRoot].SetResult(ctx, work.req.ID, res) + // Even in error we close ackNotifier as there's no retry mechanism here and closing it will alow other consumers to autoclaim + work.req.Ack() + if err != nil { + log.Error("Error setting result for request", "id", work.req.ID, "result", res, "error", err) + } + log.Debug("set result", "thread", i, "workid", work.req.ID) + } + select { + case <-ctx.Done(): + return + case requestTokenQueue <- struct{}{}: + } + } + }) + } } type ValidationServerConfig struct { @@ -131,6 +192,8 @@ type ValidationServerConfig struct { // Timeout on polling for existence of each redis stream. StreamTimeout time.Duration `koanf:"stream-timeout"` StreamPrefix string `koanf:"stream-prefix"` + Workers int `koanf:"workers"` + BufferReads bool `koanf:"buffer-reads"` } var DefaultValidationServerConfig = ValidationServerConfig{ @@ -139,6 +202,8 @@ var DefaultValidationServerConfig = ValidationServerConfig{ ConsumerConfig: pubsub.DefaultConsumerConfig, ModuleRoots: []string{}, StreamTimeout: 10 * time.Minute, + Workers: 0, + BufferReads: true, } var TestValidationServerConfig = ValidationServerConfig{ @@ -147,6 +212,8 @@ var TestValidationServerConfig = ValidationServerConfig{ ConsumerConfig: pubsub.TestConsumerConfig, ModuleRoots: []string{}, StreamTimeout: time.Minute, + Workers: 1, + BufferReads: true, } func ValidationServerConfigAddOptions(prefix string, f *pflag.FlagSet) { @@ -155,6 +222,8 @@ func ValidationServerConfigAddOptions(prefix string, f *pflag.FlagSet) { f.String(prefix+".redis-url", DefaultValidationServerConfig.RedisURL, "url of redis server") f.String(prefix+".stream-prefix", DefaultValidationServerConfig.StreamPrefix, "prefix for stream name") f.Duration(prefix+".stream-timeout", DefaultValidationServerConfig.StreamTimeout, "Timeout on polling for existence of redis streams") + f.Int(prefix+".workers", DefaultValidationServerConfig.Workers, "number of validation threads (0 to use number of CPUs)") + f.Bool(prefix+".buffer-reads", DefaultValidationServerConfig.BufferReads, "buffer reads (read next while working)") } func (cfg *ValidationServerConfig) Enabled() bool { diff --git a/validator/valnode/redis/consumer_test.go b/validator/valnode/redis/consumer_test.go index 0ebd697f16..595aecc9ca 100644 --- a/validator/valnode/redis/consumer_test.go +++ b/validator/valnode/redis/consumer_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/redisutil" "github.com/offchainlabs/nitro/util/testhelpers" ) diff --git a/validator/valnode/validation_api.go b/validator/valnode/validation_api.go index a79ac7fa55..dab74f6e29 100644 --- a/validator/valnode/validation_api.go +++ b/validator/valnode/validation_api.go @@ -12,7 +12,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" @@ -45,7 +45,7 @@ func (a *ValidationServerAPI) WasmModuleRoots() ([]common.Hash, error) { return a.spawner.WasmModuleRoots() } -func (a *ValidationServerAPI) StylusArchs() ([]rawdb.Target, error) { +func (a *ValidationServerAPI) StylusArchs() ([]ethdb.WasmTarget, error) { return a.spawner.StylusArchs(), nil } @@ -80,12 +80,16 @@ func NewExecutionServerAPI(valSpawner validator.ValidationSpawner, execution val } } -func (a *ExecServerAPI) CreateExecutionRun(ctx context.Context, wasmModuleRoot common.Hash, jsonInput *server_api.InputJSON) (uint64, error) { +func (a *ExecServerAPI) CreateExecutionRun(ctx context.Context, wasmModuleRoot common.Hash, jsonInput *server_api.InputJSON, useBoldMachineOptional *bool) (uint64, error) { input, err := server_api.ValidationInputFromJson(jsonInput) if err != nil { return 0, err } - execRun, err := a.execSpawner.CreateExecutionRun(wasmModuleRoot, input).Await(ctx) + useBoldMachine := false + if useBoldMachineOptional != nil { + useBoldMachine = *useBoldMachineOptional + } + execRun, err := a.execSpawner.CreateExecutionRun(wasmModuleRoot, input, useBoldMachine).Await(ctx) if err != nil { return 0, err } @@ -118,15 +122,6 @@ func (a *ExecServerAPI) Start(ctx_in context.Context) { a.CallIteratively(a.removeOldRuns) } -func (a *ExecServerAPI) WriteToFile(ctx context.Context, jsonInput *server_api.InputJSON, expOut validator.GoGlobalState, moduleRoot common.Hash) error { - input, err := server_api.ValidationInputFromJson(jsonInput) - if err != nil { - return err - } - _, err = a.execSpawner.WriteToFile(input, expOut, moduleRoot).Await(ctx) - return err -} - var errRunNotFound error = errors.New("run not found") func (a *ExecServerAPI) getRun(id uint64) (validator.ExecutionRun, error) { @@ -196,6 +191,14 @@ func (a *ExecServerAPI) ExecKeepAlive(ctx context.Context, execid uint64) error return nil } +func (a *ExecServerAPI) CheckAlive(ctx context.Context, execid uint64) error { + run, err := a.getRun(execid) + if err != nil { + return err + } + return run.CheckAlive(ctx) +} + func (a *ExecServerAPI) CloseExec(execid uint64) { a.runIdLock.Lock() defer a.runIdLock.Unlock() diff --git a/validator/valnode/valnode.go b/validator/valnode/valnode.go index 972e11189d..e3bf662aaa 100644 --- a/validator/valnode/valnode.go +++ b/validator/valnode/valnode.go @@ -3,17 +3,18 @@ package valnode import ( "context" - "github.com/offchainlabs/nitro/validator" + "github.com/spf13/pflag" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" + + "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_api" "github.com/offchainlabs/nitro/validator/server_arb" "github.com/offchainlabs/nitro/validator/server_common" "github.com/offchainlabs/nitro/validator/server_jit" "github.com/offchainlabs/nitro/validator/valnode/redis" - "github.com/spf13/pflag" ) type WasmConfig struct { @@ -93,7 +94,7 @@ func EnsureValidationExposedViaAuthRPC(stackConf *node.Config) { } } -func CreateValidationNode(configFetcher ValidationConfigFetcher, stack *node.Node, fatalErrChan chan error) (*ValidationNode, error) { +func CreateValidationNode(configFetcher ValidationConfigFetcher, stack *node.Node, fatalErrChan chan error, spawnerOpts ...server_arb.SpawnerOption) (*ValidationNode, error) { config := configFetcher() locator, err := server_common.NewMachineLocator(config.Wasm.RootPath) if err != nil { @@ -102,7 +103,7 @@ func CreateValidationNode(configFetcher ValidationConfigFetcher, stack *node.Nod arbConfigFetcher := func() *server_arb.ArbitratorSpawnerConfig { return &configFetcher().Arbitrator } - arbSpawner, err := server_arb.NewArbitratorSpawner(locator, arbConfigFetcher) + arbSpawner, err := server_arb.NewArbitratorSpawner(locator, arbConfigFetcher, spawnerOpts...) if err != nil { return nil, err } diff --git a/wavmio/stub.go b/wavmio/stub.go index 7fd29e2062..01031860e9 100644 --- a/wavmio/stub.go +++ b/wavmio/stub.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbutil" ) @@ -60,13 +61,14 @@ func parsePreimageBytes(path string) { if read != len(lenBuf) { panic(fmt.Sprintf("missing bytes reading len got %d", read)) } - fieldSize := int(binary.LittleEndian.Uint64(lenBuf)) + fieldSize := binary.LittleEndian.Uint64(lenBuf) dataBuf := make([]byte, fieldSize) read, err = file.Read(dataBuf) if err != nil { panic(err) } - if read != fieldSize { + // #nosec G115 + if uint64(read) != fieldSize { panic("missing bytes reading data") } hash := crypto.Keccak256Hash(dataBuf) @@ -77,18 +79,18 @@ func parsePreimageBytes(path string) { func StubInit() { preimages = make(map[common.Hash][]byte) var delayedMsgPath arrayFlags - seqMsgPosFlag := flag.Int("inbox-position", 0, "position for sequencer inbox message") - posWithinMsgFlag := flag.Int("position-within-message", 0, "position inside sequencer inbox message") - delayedPositionFlag := flag.Int("delayed-inbox-position", 0, "position for first delayed inbox message") + seqMsgPosFlag := flag.Uint64("inbox-position", 0, "position for sequencer inbox message") + posWithinMsgFlag := flag.Uint64("position-within-message", 0, "position inside sequencer inbox message") + delayedPositionFlag := flag.Uint64("delayed-inbox-position", 0, "position for first delayed inbox message") lastBlockFlag := flag.String("last-block-hash", "0000000000000000000000000000000000000000000000000000000000000000", "lastBlockHash") flag.Var(&delayedMsgPath, "delayed-inbox", "delayed inbox messages (multiple values)") inboxPath := flag.String("inbox", "", "file to load sequencer message") preimagesPath := flag.String("preimages", "", "file to load preimages from") flag.Parse() - seqMsgPos = uint64(*seqMsgPosFlag) - posWithinMsg = uint64(*posWithinMsgFlag) - delayedMsgFirstPos = uint64(*delayedPositionFlag) + seqMsgPos = *seqMsgPosFlag + posWithinMsg = *posWithinMsgFlag + delayedMsgFirstPos = *delayedPositionFlag lastBlockHash = common.HexToHash(*lastBlockFlag) for _, path := range delayedMsgPath { msg, err := os.ReadFile(path) @@ -125,7 +127,7 @@ func ReadInboxMessage(msgNum uint64) []byte { } func ReadDelayedInboxMessage(seqNum uint64) []byte { - if seqNum < delayedMsgFirstPos || (int(seqNum-delayedMsgFirstPos) > len(delayedMsgs)) { + if seqNum < delayedMsgFirstPos || (seqNum-delayedMsgFirstPos > uint64(len(delayedMsgs))) { panic(fmt.Sprintf("trying to read bad delayed msg %d", seqNum)) } return delayedMsgs[seqNum-delayedMsgFirstPos] diff --git a/wsbroadcastserver/clientconnection.go b/wsbroadcastserver/clientconnection.go index 16a8f64daf..2585452db7 100644 --- a/wsbroadcastserver/clientconnection.go +++ b/wsbroadcastserver/clientconnection.go @@ -13,14 +13,15 @@ import ( "sync/atomic" "time" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsflate" + "github.com/mailru/easygo/netpoll" + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/broadcaster/backlog" m "github.com/offchainlabs/nitro/broadcaster/message" - - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsflate" - "github.com/mailru/easygo/netpoll" "github.com/offchainlabs/nitro/util/stopwaiter" ) @@ -135,6 +136,7 @@ func (cc *ClientConnection) writeBacklog(ctx context.Context, segment backlog.Ba msgs := prevSegment.Messages() if isFirstSegment && prevSegment.Contains(uint64(cc.requestedSeqNum)) { + // #nosec G115 requestedIdx := int(cc.requestedSeqNum) - int(prevSegment.Start()) // This might be false if messages were added after we fetched the segment's messages if len(msgs) >= requestedIdx { diff --git a/wsbroadcastserver/connectionlimiter.go b/wsbroadcastserver/connectionlimiter.go index e483eb545e..d086fc074e 100644 --- a/wsbroadcastserver/connectionlimiter.go +++ b/wsbroadcastserver/connectionlimiter.go @@ -8,9 +8,10 @@ import ( "sync" "time" + flag "github.com/spf13/pflag" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" - flag "github.com/spf13/pflag" ) var ( diff --git a/wsbroadcastserver/utils.go b/wsbroadcastserver/utils.go index 9df1d7d9ca..40ceb3e5bf 100644 --- a/wsbroadcastserver/utils.go +++ b/wsbroadcastserver/utils.go @@ -12,10 +12,11 @@ import ( "strings" "time" - "github.com/ethereum/go-ethereum/log" "github.com/gobwas/ws" "github.com/gobwas/ws/wsflate" "github.com/gobwas/ws/wsutil" + + "github.com/ethereum/go-ethereum/log" ) func init() { @@ -136,7 +137,7 @@ func ReadData(ctx context.Context, conn net.Conn, earlyFrameData io.Reader, time var data []byte if msg.IsCompressed() { if !compression { - return nil, 0, errors.New("Received compressed frame even though compression is disabled") + return nil, 0, errors.New("Received compressed frame even though compression extension wasn't negotiated") } flateReader.Reset(&reader) data, err = io.ReadAll(flateReader) diff --git a/wsbroadcastserver/wsbroadcastserver.go b/wsbroadcastserver/wsbroadcastserver.go index ee21cbaae3..da9420a527 100644 --- a/wsbroadcastserver/wsbroadcastserver.go +++ b/wsbroadcastserver/wsbroadcastserver.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/broadcaster/backlog" m "github.com/offchainlabs/nitro/broadcaster/message"