diff --git a/.github/workflows/guardian-prover-health-check-ui.yml b/.github/workflows/guardian-prover-health-check-ui.yml index 52edcb02cb3..fbf9a709934 100644 --- a/.github/workflows/guardian-prover-health-check-ui.yml +++ b/.github/workflows/guardian-prover-health-check-ui.yml @@ -25,6 +25,18 @@ jobs: vercel_org_id: ${{ secrets.VERCEL_ORG_ID }} vercel_token: ${{ secrets.VERCEL_TOKEN }} + # deploy_guardians-ui_devnet_preview: + # if: ${{ github.event.pull_request.draft == false && !startsWith(github.head_ref, 'release-please') }} + # needs: build-and-test + # uses: ./.github/workflows/repo--vercel-deploy.yml + # with: + # environment: "preview" + # flags: "" + # secrets: + # vercel_project_id: ${{ secrets.VERCEL_PROJECT_ID_GUARDIAN_UI_INTERNAL }} + # vercel_org_id: ${{ secrets.VERCEL_ORG_ID }} + # vercel_token: ${{ secrets.VERCEL_TOKEN }} + deploy_guardians-ui_hekla_preview: if: ${{ github.ref_name != 'main' }} needs: build-and-test diff --git a/.github/workflows/taiko-client--pages.yml b/.github/workflows/taiko-client--pages.yml new file mode 100644 index 00000000000..997b2ed7c73 --- /dev/null +++ b/.github/workflows/taiko-client--pages.yml @@ -0,0 +1,60 @@ +name: "Taiko Client Github Pages" + +on: + push: + branches: [main] + paths: + - "packages/taiko-client/**" + +jobs: + swagger-gen: + runs-on: [arc-runner-set] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Install swaggo + run: | + export CGO_ENABLED=0 + go install github.com/swaggo/swag/cmd/swag@latest + + - name: Generate Swagger documentation + run: | + export CGO_ENABLED=0 + cd packages/taiko-client + ./scripts/gen_swagger_json.sh + + - name: Commit Swagger docs + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + if ! git diff --quiet; then + git commit -m "Update Swagger documentation" + git push origin HEAD:${{ github.ref_name }} + else + echo "No changes to commit" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + deploy: + runs-on: [arc-runner-set] + needs: swagger-gen + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: packages/taiko-client/docs # Set this to where your `index.html` is located + publish_branch: gh-pages + destination_dir: soft-block-apis diff --git a/docker-compose.yml b/docker-compose.yml index 52533eb3354..adcdf04fca1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: dockerfile: ./packages/taiko-client/Dockerfile restart: unless-stopped pull_policy: always + ports: + - "8090:8090" volumes: - ./host:/host command: @@ -25,6 +27,14 @@ services: - host/jwt.txt - --l2.auth - "${L2_AUTH}" + - --softBlock.port + - "${SOFT_BLOCK_SERVER_PORT}" + - --softBlock.jwtSecret + - "${SOFT_BLOCK_SERVER_JWT_SECRET}" + - --softBlock.corsOrigins + - "${SOFT_BLOCK_SERVER_CORS_ORIGINS}" + - --softBlock.signatureCheck + - "${SOFT_BLOCK_SERVER_SIGNATURE_CHECK}" - --verbosity - "4" extra_hosts: diff --git a/go.mod b/go.mod index efcd1bd7c6e..610126412af 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,8 @@ require ( github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/labstack/echo-contrib v0.17.1 - github.com/labstack/echo/v4 v4.12.0 + github.com/labstack/echo-jwt/v4 v4.3.0 + github.com/labstack/echo/v4 v4.13.0 github.com/labstack/gommon v0.4.2 github.com/modern-go/reflect2 v1.0.2 github.com/morkid/paginate v1.1.7 @@ -33,12 +34,12 @@ require ( github.com/prysmaticlabs/prysm/v4 v4.2.0 github.com/rabbitmq/amqp091-go v1.10.0 github.com/shopspring/decimal v1.4.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/swaggo/swag v1.16.3 github.com/testcontainers/testcontainers-go v0.30.0 github.com/urfave/cli/v2 v2.27.2 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.10.0 gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.2.1 @@ -125,8 +126,8 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // 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 @@ -296,14 +297,14 @@ require ( go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.30.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.31.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/api v0.44.0 // indirect google.golang.org/appengine v1.6.8 // indirect @@ -334,7 +335,7 @@ exclude ( github.com/ethereum/go-ethereum v1.14.7 ) -replace github.com/ethereum/go-ethereum v1.13.15 => github.com/taikoxyz/taiko-geth v1.5.1-0.20240808041410-882a6cd3294c +replace github.com/ethereum/go-ethereum v1.13.15 => github.com/pufferfinance/unifi-geth v0.0.0-20250115124835-f9a45115a840 replace github.com/ethereum-optimism/optimism v1.7.4 => github.com/taikoxyz/optimism v0.0.0-20240627102435-4845247ff00c diff --git a/go.sum b/go.sum index 6432cde520b..416614efb61 100644 --- a/go.sum +++ b/go.sum @@ -409,11 +409,11 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -678,10 +678,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU= github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA= +github.com/labstack/echo-jwt/v4 v4.3.0 h1:8JcvVCrK9dRkPx/aWY3ZempZLO336Bebh4oAtBcxAv4= +github.com/labstack/echo-jwt/v4 v4.3.0/go.mod h1:OlWm3wqfnq3Ma8DLmmH7GiEAz2S7Bj23im2iPMEAR+Q= github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno= github.com/labstack/echo/v4 v4.1.15/go.mod h1:GWO5IBVzI371K8XJe50CSvHjQCafK6cw8R/moLhEU6o= -github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= -github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/echo/v4 v4.13.0 h1:8DjSi4H/k+RqoOmwXkxW14A2H1pdPdS95+qmdJ4q1Tg= +github.com/labstack/echo/v4 v4.13.0/go.mod h1:61j7WN2+bp8V21qerqRs4yVlVTGyOagMBpF0vE7VcmM= github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -1044,6 +1046,8 @@ github.com/prysmaticlabs/protoc-gen-go-cast v0.0.0-20230228205207-28762a7b9294 h github.com/prysmaticlabs/protoc-gen-go-cast v0.0.0-20230228205207-28762a7b9294/go.mod h1:ZVEbRdnMkGhp/pu35zq4SXxtvUwWK0J1MATtekZpH2Y= github.com/prysmaticlabs/prysm/v4 v4.2.0 h1:87QoRT3Azs7c1Y6SnIq0+CNtQRbAt0sVKGj2OxRT1Rw= github.com/prysmaticlabs/prysm/v4 v4.2.0/go.mod h1:PQrQtHJeeqTz4K3udN/EX1Gs2xhWR4j93gSj0OQZ1f4= +github.com/pufferfinance/unifi-geth v0.0.0-20250115124835-f9a45115a840 h1:tHk3IjBRiv3M1KNWcoMD9q1cF4O5c8VcB4v0DDCSKtM= +github.com/pufferfinance/unifi-geth v0.0.0-20250115124835-f9a45115a840/go.mod h1:nqByouVW0a0qx5KKgvYgoXba+pYEHznAAQp6LhZilgM= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0= @@ -1164,8 +1168,9 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= @@ -1177,8 +1182,6 @@ github.com/taikoxyz/hive v0.0.0-20240827015317-405b241dd082 h1:ymZR+Y88LOnA8i3Ke github.com/taikoxyz/hive v0.0.0-20240827015317-405b241dd082/go.mod h1:RHnIu3EFehrWX3JhFAMQSXD5uz7l0xaNroTzXrap7EQ= github.com/taikoxyz/optimism v0.0.0-20240627102435-4845247ff00c h1:Hfhh/icxShwpLdX7RqYzZN1EU40MGWhvSXc2V+ZzTxw= github.com/taikoxyz/optimism v0.0.0-20240627102435-4845247ff00c/go.mod h1:jKn73pLX8eDIG0Y3XeuUSetepecM8OvRflyPHbi05B4= -github.com/taikoxyz/taiko-geth v1.5.1-0.20240808041410-882a6cd3294c h1:XQDnwQfisAlFAGKqabDcLdg9B+pRwS3nxS+03yP1g9o= -github.com/taikoxyz/taiko-geth v1.5.1-0.20240808041410-882a6cd3294c/go.mod h1:nqByouVW0a0qx5KKgvYgoXba+pYEHznAAQp6LhZilgM= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= @@ -1326,8 +1329,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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= @@ -1437,8 +1440,8 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1469,8 +1472,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -1575,8 +1578,8 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= @@ -1590,8 +1593,8 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -1609,14 +1612,14 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/packages/taiko-client/.swaggo b/packages/taiko-client/.swaggo index 8cc34d878a4..e69de29bb2d 100644 --- a/packages/taiko-client/.swaggo +++ b/packages/taiko-client/.swaggo @@ -1,2 +0,0 @@ -replace common.Address string -replace encoding.TierFee uint64 \ No newline at end of file diff --git a/packages/taiko-client/bindings/encoding/input.go b/packages/taiko-client/bindings/encoding/input.go index 8cfc0e8811a..1af1a9d5b0a 100644 --- a/packages/taiko-client/bindings/encoding/input.go +++ b/packages/taiko-client/bindings/encoding/input.go @@ -3,6 +3,7 @@ package encoding import ( "errors" "fmt" + "math/big" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/log" @@ -294,6 +295,16 @@ var ( {Name: "TaikoData.Transition", Type: transitionComponentsType}, {Name: "TaikoData.TierProof", Type: tierProofComponentsType}, } + stringType, _ = abi.NewType("string", "TAIKO_DIFFICULTY", nil) + uint64Type, _ = abi.NewType("uint64", "local.b.numBlocks", nil) + difficultyCalculationInputArgs = abi.Arguments{{Type: stringType}, {Type: uint64Type}} + proveBlocksInputArgs = abi.Arguments{ + {Name: "TaikoData.BlockMetadata", Type: blockMetadataV2ComponentsType}, + {Name: "TaikoData.Transition", Type: transitionComponentsType}, + } + proveBlocksBatchProofArgs = abi.Arguments{ + {Name: "TaikoData.TierProof", Type: tierProofComponentsType}, + } ) // Contract ABIs. @@ -423,6 +434,53 @@ func EncodeProveBlockInput( return b, nil } +// EncodeDifficultCalcutionParams performs the solidity `abi.encode` for the +// `block.difficulty` hash payload. +func EncodeDifficultyCalcutionParams(numBlocks uint64) ([]byte, error) { + b, err := difficultyCalculationInputArgs.Pack("TAIKO_DIFFICULTY", numBlocks) + if err != nil { + return nil, fmt.Errorf("failed to abi.encode `block.difficulty` hash payload, %w", err) + } + return b, nil +} + +// EncodeProveBlocksInput performs the solidity `abi.encode` for the given TaikoL1.proveBlocks input. +func EncodeProveBlocksInput( + metas []metadata.TaikoBlockMetaData, + transitions []bindings.TaikoDataTransition, +) ([][]byte, error) { + if len(metas) != len(transitions) { + return nil, fmt.Errorf("both arrays of TaikoBlockMetaData and TaikoDataTransition must be equal in length") + } + b := make([][]byte, 0, len(metas)) + for i := range metas { + input, err := proveBlocksInputArgs.Pack( + metas[i].(*metadata.TaikoDataBlockMetadataOntake).InnerMetadata(), + transitions[i], + ) + if err != nil { + return nil, fmt.Errorf("failed to abi.encode TaikoL1.proveBlocks input item after ontake fork, %w", err) + } + + b = append(b, input) + } + + return b, nil +} + +// EncodeProveBlocksBatchProof performs the solidity `abi.encode` for the given TaikoL1.proveBlocks batchProof. +func EncodeProveBlocksBatchProof( + tierProof *bindings.TaikoDataTierProof, +) ([]byte, error) { + input, err := proveBlocksBatchProofArgs.Pack( + tierProof, + ) + if err != nil { + return nil, fmt.Errorf("failed to abi.encode TaikoL1.proveBlocks input item after ontake fork, %w", err) + } + return input, nil +} + // UnpackTxListBytes unpacks the input data of a TaikoL1.proposeBlock transaction, and returns the txList bytes. func UnpackTxListBytes(txData []byte) ([]byte, error) { method, err := TaikoL1ABI.MethodById(txData) @@ -449,3 +507,13 @@ func UnpackTxListBytes(txData []byte) ([]byte, error) { return inputs, nil } + +// EncodeBaseFeeConfig encodes the block.extraData field from the given base fee config. +func EncodeBaseFeeConfig(baseFeeConfig *bindings.LibSharedDataBaseFeeConfig) [32]byte { + var ( + bytes32Value [32]byte + uintValue = new(big.Int).SetUint64(uint64(baseFeeConfig.SharingPctg)) + ) + copy(bytes32Value[32-len(uintValue.Bytes()):], uintValue.Bytes()) + return bytes32Value +} diff --git a/packages/taiko-client/cmd/flags/driver.go b/packages/taiko-client/cmd/flags/driver.go index 3c28dc7571a..8f8dad3dbf1 100644 --- a/packages/taiko-client/cmd/flags/driver.go +++ b/packages/taiko-client/cmd/flags/driver.go @@ -52,6 +52,33 @@ var ( Category: driverCategory, EnvVars: []string{"BLOB_SOCIAL_SCAN_ENDPOINT"}, } + // soft block server + SoftBlockServerPort = &cli.Uint64Flag{ + Name: "softBlock.port", + Usage: "HTTP port of the soft block server, 0 means disabled", + Category: driverCategory, + EnvVars: []string{"SOFT_BLOCK_SERVER_PORT"}, + } + SoftBlockServerJWTSecret = &cli.StringFlag{ + Name: "softBlock.jwtSecret", + Usage: "Path to a JWT secret to use for the soft block server", + Category: driverCategory, + EnvVars: []string{"SOFT_BLOCK_SERVER_JWT_SECRET"}, + } + SoftBlockServerCORSOrigins = &cli.StringFlag{ + Name: "softBlock.corsOrigins", + Usage: "CORS Origins settings for the soft block server", + Category: driverCategory, + Value: "*", + EnvVars: []string{"SOFT_BLOCK_SERVER_CORS_ORIGINS"}, + } + SoftBlockServerCheckSig = &cli.BoolFlag{ + Name: "softBlock.signatureCheck", + Usage: "If the soft block server will check the signature of the incoming transactions batches", + Category: driverCategory, + Value: false, + EnvVars: []string{"SOFT_BLOCK_SERVER_SIGNATURE_CHECK"}, + } ) // DriverFlags All driver flags. @@ -66,4 +93,8 @@ var DriverFlags = MergeFlags(CommonFlags, []cli.Flag{ MaxExponent, BlobServerEndpoint, SocialScanEndpoint, + SoftBlockServerPort, + SoftBlockServerJWTSecret, + SoftBlockServerCORSOrigins, + SoftBlockServerCheckSig, }) diff --git a/packages/taiko-client/docs/docs.go b/packages/taiko-client/docs/docs.go new file mode 100644 index 00000000000..ca9a73974e5 --- /dev/null +++ b/packages/taiko-client/docs/docs.go @@ -0,0 +1,378 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "https://community.taiko.xyz/", + "email": "info@taiko.xyz" + }, + "license": { + "name": "MIT", + "url": "https://github.com/taikoxyz/taiko-mono/blob/main/LICENSE.md" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/healthz": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get current server health status", + "operationId": "health-check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/softBlocks": { + "post": { + "description": "Insert a batch of transactions into a soft block for preconfirmation. If the batch is the\nfirst for a block, a new soft block will be created. Otherwise, the transactions will\nbe appended to the existing soft block. The API will fail if:\n1) the block is not soft\n2) block-level parameters are invalid or do not match the current soft block’s parameters\n3) the batch ID is not exactly 1 greater than the previous one\n4) the last batch of the block indicates no further transactions are allowed", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "soft block creation request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/softblocks.BuildSoftBlockRequestBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/softblocks.BuildSoftBlockResponseBody" + } + } + } + }, + "delete": { + "description": "Remove all soft blocks from the blockchain beyond the specified block height,\nensuring the latest block ID does not exceed the given height. This method will fail if\nthe block with an ID one greater than the specified height is not a soft block. If the\nspecified block height is greater than the latest soft block ID, the method will succeed\nwithout modifying the blockchain.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "soft blocks removing request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/softblocks.RemoveSoftBlocksRequestBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/softblocks.RemoveSoftBlocksResponseBody" + } + } + } + } + } + }, + "definitions": { + "big.Int": { + "type": "object" + }, + "softblocks.BuildSoftBlockRequestBody": { + "type": "object", + "properties": { + "transactionBatch": { + "description": "@param transactionBatch TransactionBatch Transaction batch to be inserted into the soft block", + "allOf": [ + { + "$ref": "#/definitions/softblocks.TransactionBatch" + } + ] + } + } + }, + "softblocks.BuildSoftBlockResponseBody": { + "type": "object", + "properties": { + "blockHeader": { + "description": "@param blockHeader types.Header of the soft block", + "allOf": [ + { + "$ref": "#/definitions/types.Header" + } + ] + } + } + }, + "softblocks.RemoveSoftBlocksRequestBody": { + "type": "object", + "properties": { + "newLastBlockId": { + "description": "@param newLastBlockID uint64 New last block ID of the blockchain, it should\n@param not smaller than the canonical chain's highest block ID.", + "type": "integer" + } + } + }, + "softblocks.RemoveSoftBlocksResponseBody": { + "type": "object", + "properties": { + "headsRemoved": { + "description": "@param headsRemoved uint64 Number of soft heads removed", + "type": "integer" + }, + "lastBlockId": { + "description": "@param lastBlockID uint64 Current highest block ID of the blockchain (including soft blocks)", + "type": "integer" + }, + "lastProposedBlockID": { + "description": "@param lastProposedBlockID uint64 Highest block ID of the cnonical chain", + "type": "integer" + } + } + }, + "softblocks.SoftBlockParams": { + "type": "object", + "properties": { + "anchorBlockID": { + "description": "@param anchorBlockID uint64 ` + "`" + `_anchorBlockId` + "`" + ` parameter of the ` + "`" + `anchorV2` + "`" + ` transaction in soft block", + "type": "integer" + }, + "anchorStateRoot": { + "description": "@param anchorStateRoot string ` + "`" + `_anchorStateRoot` + "`" + ` parameter of the ` + "`" + `anchorV2` + "`" + ` transaction in soft block", + "type": "array", + "items": { + "type": "integer" + } + }, + "coinbase": { + "description": "@param coinbase string Coinbase of the soft block", + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamp": { + "description": "@param timestamp uint64 Timestamp of the soft block", + "type": "integer" + } + } + }, + "softblocks.TransactionBatch": { + "type": "object", + "properties": { + "batchId": { + "description": "@param batchId uint64 ID of this transaction batch", + "type": "integer" + }, + "batchType": { + "description": "@param batchType TransactionBatchMarker Marker of the transaction batch,\n@param either ` + "`" + `end_of_block` + "`" + `, ` + "`" + `end_of_preconf` + "`" + ` or empty", + "allOf": [ + { + "$ref": "#/definitions/softblocks.TransactionBatchMarker" + } + ] + }, + "blockId": { + "description": "@param blockId uint64 Block ID of the soft block", + "type": "integer" + }, + "blockParams": { + "description": "@param blockParams SoftBlockParams Block parameters of the soft block", + "allOf": [ + { + "$ref": "#/definitions/softblocks.SoftBlockParams" + } + ] + }, + "signature": { + "description": "@param signature string Signature of this transaction batch", + "type": "string" + }, + "transactions": { + "description": "@param transactions string zlib compressed RLP encoded bytes of a transactions list", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "softblocks.TransactionBatchMarker": { + "type": "string", + "enum": [ + "", + "endOfBlock", + "endOfPreconf" + ], + "x-enum-varnames": [ + "BatchMarkerEmpty", + "BatchMarkerEOB", + "BatchMarkerEOP" + ] + }, + "types.Header": { + "type": "object", + "properties": { + "baseFeePerGas": { + "description": "BaseFee was added by EIP-1559 and is ignored in legacy headers.", + "allOf": [ + { + "$ref": "#/definitions/big.Int" + } + ] + }, + "blobGasUsed": { + "description": "BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.", + "type": "integer" + }, + "difficulty": { + "$ref": "#/definitions/big.Int" + }, + "excessBlobGas": { + "description": "ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.", + "type": "integer" + }, + "extraData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "gasLimit": { + "type": "integer" + }, + "gasUsed": { + "type": "integer" + }, + "logsBloom": { + "type": "array", + "items": { + "type": "integer" + } + }, + "miner": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mixHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "nonce": { + "type": "array", + "items": { + "type": "integer" + } + }, + "number": { + "$ref": "#/definitions/big.Int" + }, + "parentBeaconBlockRoot": { + "description": "ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers.", + "type": "array", + "items": { + "type": "integer" + } + }, + "parentHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "receiptsRoot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "requestsRoot": { + "description": "RequestsHash was added by EIP-7685 and is ignored in legacy headers.", + "type": "array", + "items": { + "type": "integer" + } + }, + "sha3Uncles": { + "type": "array", + "items": { + "type": "integer" + } + }, + "stateRoot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamp": { + "type": "integer" + }, + "transactionsRoot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "withdrawalsRoot": { + "description": "WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.", + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Taiko Soft Block Server API", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/packages/taiko-client/docs/index.html b/packages/taiko-client/docs/index.html new file mode 100644 index 00000000000..fd6da6d8abf --- /dev/null +++ b/packages/taiko-client/docs/index.html @@ -0,0 +1,29 @@ + + + + + + Taiko Preconfirmation Server API + + +
+ + + diff --git a/packages/taiko-client/docs/swagger.json b/packages/taiko-client/docs/swagger.json new file mode 100644 index 00000000000..2e72737ed95 --- /dev/null +++ b/packages/taiko-client/docs/swagger.json @@ -0,0 +1,335 @@ +{ + "swagger": "2.0", + "info": { + "title": "Taiko Soft Block Server API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "https://community.taiko.xyz/", + "email": "info@taiko.xyz" + }, + "license": { + "name": "MIT", + "url": "https://github.com/taikoxyz/taiko-mono/blob/main/LICENSE.md" + }, + "version": "1.0" + }, + "paths": { + "/healthz": { + "get": { + "consumes": ["application/json"], + "produces": ["application/json"], + "summary": "Get current server health status", + "operationId": "health-check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/softBlocks": { + "post": { + "description": "Insert a batch of transactions into a soft block for preconfirmation. If the batch is the\nfirst for a block, a new soft block will be created. Otherwise, the transactions will\nbe appended to the existing soft block. The API will fail if:\n1) the block is not soft\n2) block-level parameters are invalid or do not match the current soft block’s parameters\n3) the batch ID is not exactly 1 greater than the previous one\n4) the last batch of the block indicates no further transactions are allowed", + "consumes": ["application/json"], + "produces": ["application/json"], + "parameters": [ + { + "description": "soft block creation request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/softblocks.BuildSoftBlockRequestBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/softblocks.BuildSoftBlockResponseBody" + } + } + } + }, + "delete": { + "description": "Remove all soft blocks from the blockchain beyond the specified block height,\nensuring the latest block ID does not exceed the given height. This method will fail if\nthe block with an ID one greater than the specified height is not a soft block. If the\nspecified block height is greater than the latest soft block ID, the method will succeed\nwithout modifying the blockchain.", + "consumes": ["application/json"], + "produces": ["application/json"], + "parameters": [ + { + "description": "soft blocks removing request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/softblocks.RemoveSoftBlocksRequestBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/softblocks.RemoveSoftBlocksResponseBody" + } + } + } + } + } + }, + "definitions": { + "big.Int": { + "type": "object" + }, + "softblocks.BuildSoftBlockRequestBody": { + "type": "object", + "properties": { + "transactionBatch": { + "description": "@param transactionBatch TransactionBatch Transaction batch to be inserted into the soft block", + "allOf": [ + { + "$ref": "#/definitions/softblocks.TransactionBatch" + } + ] + } + } + }, + "softblocks.BuildSoftBlockResponseBody": { + "type": "object", + "properties": { + "blockHeader": { + "description": "@param blockHeader types.Header of the soft block", + "allOf": [ + { + "$ref": "#/definitions/types.Header" + } + ] + } + } + }, + "softblocks.RemoveSoftBlocksRequestBody": { + "type": "object", + "properties": { + "newLastBlockId": { + "description": "@param newLastBlockID uint64 New last block ID of the blockchain, it should\n@param not smaller than the canonical chain's highest block ID.", + "type": "integer" + } + } + }, + "softblocks.RemoveSoftBlocksResponseBody": { + "type": "object", + "properties": { + "headsRemoved": { + "description": "@param headsRemoved uint64 Number of soft heads removed", + "type": "integer" + }, + "lastBlockId": { + "description": "@param lastBlockID uint64 Current highest block ID of the blockchain (including soft blocks)", + "type": "integer" + }, + "lastProposedBlockID": { + "description": "@param lastProposedBlockID uint64 Highest block ID of the cnonical chain", + "type": "integer" + } + } + }, + "softblocks.SoftBlockParams": { + "type": "object", + "properties": { + "anchorBlockID": { + "description": "@param anchorBlockID uint64 `_anchorBlockId` parameter of the `anchorV2` transaction in soft block", + "type": "integer" + }, + "anchorStateRoot": { + "description": "@param anchorStateRoot string `_anchorStateRoot` parameter of the `anchorV2` transaction in soft block", + "type": "array", + "items": { + "type": "integer" + } + }, + "coinbase": { + "description": "@param coinbase string Coinbase of the soft block", + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamp": { + "description": "@param timestamp uint64 Timestamp of the soft block", + "type": "integer" + } + } + }, + "softblocks.TransactionBatch": { + "type": "object", + "properties": { + "batchId": { + "description": "@param batchId uint64 ID of this transaction batch", + "type": "integer" + }, + "batchType": { + "description": "@param batchType TransactionBatchMarker Marker of the transaction batch,\n@param either `end_of_block`, `end_of_preconf` or empty", + "allOf": [ + { + "$ref": "#/definitions/softblocks.TransactionBatchMarker" + } + ] + }, + "blockId": { + "description": "@param blockId uint64 Block ID of the soft block", + "type": "integer" + }, + "blockParams": { + "description": "@param blockParams SoftBlockParams Block parameters of the soft block", + "allOf": [ + { + "$ref": "#/definitions/softblocks.SoftBlockParams" + } + ] + }, + "signature": { + "description": "@param signature string Signature of this transaction batch", + "type": "string" + }, + "transactions": { + "description": "@param transactions string zlib compressed RLP encoded bytes of a transactions list", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "softblocks.TransactionBatchMarker": { + "type": "string", + "enum": ["", "endOfBlock", "endOfPreconf"], + "x-enum-varnames": [ + "BatchMarkerEmpty", + "BatchMarkerEOB", + "BatchMarkerEOP" + ] + }, + "types.Header": { + "type": "object", + "properties": { + "baseFeePerGas": { + "description": "BaseFee was added by EIP-1559 and is ignored in legacy headers.", + "allOf": [ + { + "$ref": "#/definitions/big.Int" + } + ] + }, + "blobGasUsed": { + "description": "BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.", + "type": "integer" + }, + "difficulty": { + "$ref": "#/definitions/big.Int" + }, + "excessBlobGas": { + "description": "ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.", + "type": "integer" + }, + "extraData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "gasLimit": { + "type": "integer" + }, + "gasUsed": { + "type": "integer" + }, + "logsBloom": { + "type": "array", + "items": { + "type": "integer" + } + }, + "miner": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mixHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "nonce": { + "type": "array", + "items": { + "type": "integer" + } + }, + "number": { + "$ref": "#/definitions/big.Int" + }, + "parentBeaconBlockRoot": { + "description": "ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers.", + "type": "array", + "items": { + "type": "integer" + } + }, + "parentHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "receiptsRoot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "requestsRoot": { + "description": "RequestsHash was added by EIP-7685 and is ignored in legacy headers.", + "type": "array", + "items": { + "type": "integer" + } + }, + "sha3Uncles": { + "type": "array", + "items": { + "type": "integer" + } + }, + "stateRoot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamp": { + "type": "integer" + }, + "transactionsRoot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "withdrawalsRoot": { + "description": "WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.", + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } +} diff --git a/packages/taiko-client/docs/swagger.yaml b/packages/taiko-client/docs/swagger.yaml new file mode 100644 index 00000000000..88bafda32e4 --- /dev/null +++ b/packages/taiko-client/docs/swagger.yaml @@ -0,0 +1,266 @@ +definitions: + big.Int: + type: object + softblocks.BuildSoftBlockRequestBody: + properties: + transactionBatch: + allOf: + - $ref: "#/definitions/softblocks.TransactionBatch" + description: + "@param transactionBatch TransactionBatch Transaction batch to + be inserted into the soft block" + type: object + softblocks.BuildSoftBlockResponseBody: + properties: + blockHeader: + allOf: + - $ref: "#/definitions/types.Header" + description: "@param blockHeader types.Header of the soft block" + type: object + softblocks.RemoveSoftBlocksRequestBody: + properties: + newLastBlockId: + description: |- + @param newLastBlockID uint64 New last block ID of the blockchain, it should + @param not smaller than the canonical chain's highest block ID. + type: integer + type: object + softblocks.RemoveSoftBlocksResponseBody: + properties: + headsRemoved: + description: "@param headsRemoved uint64 Number of soft heads removed" + type: integer + lastBlockId: + description: + "@param lastBlockID uint64 Current highest block ID of the blockchain + (including soft blocks)" + type: integer + lastProposedBlockID: + description: + "@param lastProposedBlockID uint64 Highest block ID of the cnonical + chain" + type: integer + type: object + softblocks.SoftBlockParams: + properties: + anchorBlockID: + description: + "@param anchorBlockID uint64 `_anchorBlockId` parameter of the + `anchorV2` transaction in soft block" + type: integer + anchorStateRoot: + description: + "@param anchorStateRoot string `_anchorStateRoot` parameter of + the `anchorV2` transaction in soft block" + items: + type: integer + type: array + coinbase: + description: "@param coinbase string Coinbase of the soft block" + items: + type: integer + type: array + timestamp: + description: "@param timestamp uint64 Timestamp of the soft block" + type: integer + type: object + softblocks.TransactionBatch: + properties: + batchId: + description: "@param batchId uint64 ID of this transaction batch" + type: integer + batchType: + allOf: + - $ref: "#/definitions/softblocks.TransactionBatchMarker" + description: |- + @param batchType TransactionBatchMarker Marker of the transaction batch, + @param either `end_of_block`, `end_of_preconf` or empty + blockId: + description: "@param blockId uint64 Block ID of the soft block" + type: integer + blockParams: + allOf: + - $ref: "#/definitions/softblocks.SoftBlockParams" + description: + "@param blockParams SoftBlockParams Block parameters of the soft + block" + signature: + description: "@param signature string Signature of this transaction batch" + type: string + transactions: + description: + "@param transactions string zlib compressed RLP encoded bytes + of a transactions list" + items: + type: integer + type: array + type: object + softblocks.TransactionBatchMarker: + enum: + - "" + - endOfBlock + - endOfPreconf + type: string + x-enum-varnames: + - BatchMarkerEmpty + - BatchMarkerEOB + - BatchMarkerEOP + types.Header: + properties: + baseFeePerGas: + allOf: + - $ref: "#/definitions/big.Int" + description: BaseFee was added by EIP-1559 and is ignored in legacy headers. + blobGasUsed: + description: BlobGasUsed was added by EIP-4844 and is ignored in legacy headers. + type: integer + difficulty: + $ref: "#/definitions/big.Int" + excessBlobGas: + description: + ExcessBlobGas was added by EIP-4844 and is ignored in legacy + headers. + type: integer + extraData: + items: + type: integer + type: array + gasLimit: + type: integer + gasUsed: + type: integer + logsBloom: + items: + type: integer + type: array + miner: + items: + type: integer + type: array + mixHash: + items: + type: integer + type: array + nonce: + items: + type: integer + type: array + number: + $ref: "#/definitions/big.Int" + parentBeaconBlockRoot: + description: + ParentBeaconRoot was added by EIP-4788 and is ignored in legacy + headers. + items: + type: integer + type: array + parentHash: + items: + type: integer + type: array + receiptsRoot: + items: + type: integer + type: array + requestsRoot: + description: RequestsHash was added by EIP-7685 and is ignored in legacy headers. + items: + type: integer + type: array + sha3Uncles: + items: + type: integer + type: array + stateRoot: + items: + type: integer + type: array + timestamp: + type: integer + transactionsRoot: + items: + type: integer + type: array + withdrawalsRoot: + description: + WithdrawalsHash was added by EIP-4895 and is ignored in legacy + headers. + items: + type: integer + type: array + type: object +info: + contact: + email: info@taiko.xyz + name: API Support + url: https://community.taiko.xyz/ + license: + name: MIT + url: https://github.com/taikoxyz/taiko-mono/blob/main/LICENSE.md + termsOfService: http://swagger.io/terms/ + title: Taiko Soft Block Server API + version: "1.0" +paths: + /healthz: + get: + consumes: + - application/json + operationId: health-check + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: Get current server health status + /softBlocks: + delete: + consumes: + - application/json + description: |- + Remove all soft blocks from the blockchain beyond the specified block height, + ensuring the latest block ID does not exceed the given height. This method will fail if + the block with an ID one greater than the specified height is not a soft block. If the + specified block height is greater than the latest soft block ID, the method will succeed + without modifying the blockchain. + parameters: + - description: soft blocks removing request body + in: body + name: body + required: true + schema: + $ref: "#/definitions/softblocks.RemoveSoftBlocksRequestBody" + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/softblocks.RemoveSoftBlocksResponseBody" + post: + consumes: + - application/json + description: |- + Insert a batch of transactions into a soft block for preconfirmation. If the batch is the + first for a block, a new soft block will be created. Otherwise, the transactions will + be appended to the existing soft block. The API will fail if: + 1) the block is not soft + 2) block-level parameters are invalid or do not match the current soft block’s parameters + 3) the batch ID is not exactly 1 greater than the previous one + 4) the last batch of the block indicates no further transactions are allowed + parameters: + - description: soft block creation request body + in: body + name: body + required: true + schema: + $ref: "#/definitions/softblocks.BuildSoftBlockRequestBody" + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/softblocks.BuildSoftBlockResponseBody" +swagger: "2.0" diff --git a/packages/taiko-client/driver/chain_syncer/blob/soft_block.go b/packages/taiko-client/driver/chain_syncer/blob/soft_block.go new file mode 100644 index 00000000000..7f015223df6 --- /dev/null +++ b/packages/taiko-client/driver/chain_syncer/blob/soft_block.go @@ -0,0 +1,300 @@ +package blob + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + consensus "github.com/ethereum/go-ethereum/consensus/taiko" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/encoding" + softblocks "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/soft_blocks" + "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc" + "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/utils" +) + +// InsertSoftBlockFromTransactionsBatch inserts a soft block into the L2 execution engine's blockchain +// from the given transactions batch. +func (s *Syncer) InsertSoftBlockFromTransactionsBatch( + ctx context.Context, + blockID uint64, + batchID uint64, + txListBytes []byte, + batchMarker softblocks.TransactionBatchMarker, + blockParams *softblocks.SoftBlockParams, +) (*types.Header, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + parent, err := s.rpc.L2.HeaderByNumber(ctx, new(big.Int).Sub(new(big.Int).SetUint64(blockID), common.Big1)) + if err != nil { + return nil, err + } + + if parent.Number.Uint64()+1 != blockID { + return nil, fmt.Errorf("parent block number (%d) is not equal to blockID - 1 (%d)", parent.Number.Uint64(), blockID) + } + + // Calculate the other block parameters + difficultyHashPaylaod, err := encoding.EncodeDifficultyCalcutionParams(blockID) + if err != nil { + return nil, fmt.Errorf("failed to encode `block.difficulty` calculation parameters: %w", err) + } + protocolConfigs, err := rpc.GetProtocolConfigs(s.rpc.TaikoL1, &bind.CallOpts{Context: ctx}) + if err != nil { + return nil, fmt.Errorf("failed to fetch protocol configs: %w", err) + } + + var ( + txList []*types.Transaction + fc = &engine.ForkchoiceStateV1{HeadBlockHash: parent.Hash()} + difficulty = crypto.Keccak256Hash(difficultyHashPaylaod) + extraData = encoding.EncodeBaseFeeConfig(&protocolConfigs.BaseFeeConfig) + ) + + if err := rlp.DecodeBytes(txListBytes, &txList); err != nil { + return nil, fmt.Errorf("failed to RLP decode txList bytes: %w", err) + } + + baseFee, err := s.rpc.CalculateBaseFee( + ctx, + parent, + new(big.Int).SetUint64(blockParams.AnchorBlockID), + true, + &protocolConfigs.BaseFeeConfig, + blockParams.Timestamp, + ) + if err != nil { + return nil, fmt.Errorf("failed to calculate base fee: %w", err) + } + + // Insert the anchor transaction at the head of the transactions list. + if batchID == 0 { + // Assemble a TaikoL2.anchorV2 transaction. + anchorTx, err := s.anchorConstructor.AssembleAnchorV2Tx( + ctx, + new(big.Int).SetUint64(blockParams.AnchorBlockID), + blockParams.AnchorStateRoot, + parent.GasUsed, + &protocolConfigs.BaseFeeConfig, + new(big.Int).SetUint64(blockID), + baseFee, + ) + if err != nil { + return nil, fmt.Errorf("failed to create TaikoL2.anchorV2 transaction: %w", err) + } + + txList = append([]*types.Transaction{anchorTx}, txList...) + } else { + prevSoftBlock, err := s.rpc.L2.BlockByNumber(ctx, new(big.Int).SetUint64(blockID)) + if err != nil { + return nil, fmt.Errorf("failed to fetch previous soft block (%d): %w", blockID, err) + } + + // Ensure the previous soft block is the current chain head. + blockNums, err := s.rpc.L2.BlockNumber(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch the chain block number: %w", err) + } + + if prevSoftBlock.Number().Uint64() != blockNums { + return nil, fmt.Errorf( + "soft block (%d) to update is not the current chain head (%d)", + prevSoftBlock.Number().Uint64(), + blockNums, + ) + } + + // Check baseFee + if prevSoftBlock.BaseFee().Cmp(baseFee) != 0 { + return nil, fmt.Errorf( + "baseFee is not equal to the latest soft block's, expect: %s, actual: %s", + prevSoftBlock.BaseFee().String(), + baseFee.String(), + ) + } + + // Check the previous soft block status. + l1Origin, err := s.rpc.L2.L1OriginByID(ctx, prevSoftBlock.Number()) + if err != nil { + return nil, fmt.Errorf("failed to fetch L1 origin for block %d: %w", blockID, err) + } + if l1Origin.BatchID == nil { + return nil, fmt.Errorf("batch ID is nil for block %d", blockID) + } + if l1Origin.BatchID.Uint64()+1 != batchID { + return nil, fmt.Errorf("batch ID mismatch: expected %d, got %d", l1Origin.BatchID.Uint64()+1, batchID) + } + if l1Origin.EndOfBlock { + return nil, fmt.Errorf("soft block %d has already been marked as ended", blockID) + } + if l1Origin.EndOfPreconf { + return nil, fmt.Errorf("preconfirmation from %s has already been marked as ended", blockParams.Coinbase) + } + + txList = append(prevSoftBlock.Transactions(), txList...) + } + + if txListBytes, err = rlp.EncodeToBytes(txList); err != nil { + log.Error("Encode txList error", "blockID", blockID, "error", err) + return nil, err + } + + attributes := &engine.PayloadAttributes{ + Timestamp: blockParams.Timestamp, + Random: difficulty, + SuggestedFeeRecipient: blockParams.Coinbase, + Withdrawals: []*types.Withdrawal{}, + BlockMetadata: &engine.BlockMetadata{ + Beneficiary: blockParams.Coinbase, + GasLimit: uint64(protocolConfigs.BlockMaxGasLimit) + consensus.AnchorGasLimit, + Timestamp: blockParams.Timestamp, + TxList: txListBytes, + MixHash: difficulty, + ExtraData: extraData[:], + }, + BaseFeePerGas: baseFee, + L1Origin: &rawdb.L1Origin{ + BlockID: new(big.Int).SetUint64(blockID), + L2BlockHash: common.Hash{}, // Will be set by taiko-geth. + L1BlockHeight: nil, // No L1 block height for soft blocks. + L1BlockHash: common.Hash{}, // No L1 block hash for soft blocks. + BatchID: new(big.Int).SetUint64(batchID), + EndOfBlock: batchMarker == softblocks.BatchMarkerEOB, + EndOfPreconf: batchMarker == softblocks.BatchMarkerEOP, + Preconfer: blockParams.Coinbase, + }, + } + + log.Info( + "Soft block payloadAttributes", + "blockID", blockID, + "batchID", batchID, + "timestamp", attributes.Timestamp, + "random", attributes.Random, + "suggestedFeeRecipient", attributes.SuggestedFeeRecipient, + "withdrawals", len(attributes.Withdrawals), + "gasLimit", attributes.BlockMetadata.GasLimit, + "timestamp", attributes.BlockMetadata.Timestamp, + "mixHash", attributes.BlockMetadata.MixHash, + "baseFee", utils.WeiToGWei(attributes.BaseFeePerGas), + "extraData", common.Bytes2Hex(attributes.BlockMetadata.ExtraData), + "transactions", len(txList), + "l1Origin.blockID", attributes.L1Origin.BlockID, + "l1Origin.batchID", attributes.L1Origin.BatchID, + ) + + // Step 1, prepare a payload + fcRes, err := s.rpc.L2Engine.ForkchoiceUpdate(ctx, fc, attributes) + if err != nil { + return nil, fmt.Errorf("failed to update fork choice: %w", err) + } + if fcRes.PayloadStatus.Status != engine.VALID { + return nil, fmt.Errorf("unexpected ForkchoiceUpdate response status: %s", fcRes.PayloadStatus.Status) + } + if fcRes.PayloadID == nil { + return nil, errors.New("empty payload ID") + } + + // Step 2, get the payload + payload, err := s.rpc.L2Engine.GetPayload(ctx, fcRes.PayloadID) + if err != nil { + return nil, fmt.Errorf("failed to get payload: %w", err) + } + + log.Info( + "Soft block payload", + "blockID", blockID, + "batchID", batchID, + "baseFee", utils.WeiToGWei(payload.BaseFeePerGas), + "number", payload.Number, + "hash", payload.BlockHash, + "gasLimit", payload.GasLimit, + "gasUsed", payload.GasUsed, + "timestamp", payload.Timestamp, + "withdrawalsHash", payload.WithdrawalsHash, + ) + + // Step 3, execute the payload + execStatus, err := s.rpc.L2Engine.NewPayload(ctx, payload) + if err != nil { + return nil, fmt.Errorf("failed to create a new payload: %w", err) + } + if execStatus.Status != engine.VALID { + return nil, fmt.Errorf("unexpected NewPayload response status: %s", execStatus.Status) + } + + lastVerifiedBlockHash, err := s.rpc.GetLastVerifiedBlock(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch last verified block hash: %w", err) + } + + canonicalHead, err := s.rpc.L2.HeadL1Origin(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch canonical head: %w", err) + } + + // Step 4, update the fork choice + fc = &engine.ForkchoiceStateV1{ + HeadBlockHash: payload.BlockHash, + SafeBlockHash: canonicalHead.L2BlockHash, + FinalizedBlockHash: lastVerifiedBlockHash.BlockHash, + } + fcRes, err = s.rpc.L2Engine.ForkchoiceUpdate(ctx, fc, nil) + if err != nil { + return nil, err + } + if fcRes.PayloadStatus.Status != engine.VALID { + return nil, fmt.Errorf("unexpected ForkchoiceUpdate response status: %s", fcRes.PayloadStatus.Status) + } + + header, err := s.rpc.L2.HeaderByHash(ctx, payload.BlockHash) + if err != nil { + return nil, err + } + + log.Info( + "⏰ New soft L2 block inserted", + "blockID", blockID, + "batchID", batchID, + "hash", header.Hash(), + "transactions", len(payload.Transactions), + "baseFee", utils.WeiToGWei(header.BaseFee), + "withdrawals", len(payload.Withdrawals), + "endOfBlock", attributes.L1Origin.EndOfBlock, + "endOfPreconf", attributes.L1Origin.EndOfPreconf, + ) + + return header, nil +} + +// RemoveSoftBlocks removes soft blocks from the L2 execution engine's blockchain. +func (s *Syncer) RemoveSoftBlocks(ctx context.Context, newLastBlockID uint64) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + newHead, err := s.rpc.L2.HeaderByNumber(ctx, new(big.Int).SetUint64(newLastBlockID)) + if err != nil { + return err + } + + fc := &engine.ForkchoiceStateV1{HeadBlockHash: newHead.Hash()} + fcRes, err := s.rpc.L2Engine.ForkchoiceUpdate(ctx, fc, nil) + if err != nil { + return err + } + if fcRes.PayloadStatus.Status != engine.VALID { + return fmt.Errorf("unexpected ForkchoiceUpdate response status: %s", fcRes.PayloadStatus.Status) + } + + return nil +} diff --git a/packages/taiko-client/driver/chain_syncer/blob/syncer.go b/packages/taiko-client/driver/chain_syncer/blob/syncer.go index 47f2d997a15..f57e22483ab 100644 --- a/packages/taiko-client/driver/chain_syncer/blob/syncer.go +++ b/packages/taiko-client/driver/chain_syncer/blob/syncer.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "net/url" + "sync" "time" "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings" @@ -18,7 +19,6 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/encoding" @@ -49,6 +49,7 @@ type Syncer struct { reorgDetectedFlag bool maxRetrieveExponent uint64 blobDatasource *rpc.BlobDataSource + mutex sync.Mutex } // NewSyncer creates a new syncer instance. @@ -90,6 +91,8 @@ func NewSyncer( // ProcessL1Blocks fetches all `TaikoL1.BlockProposed` events between given // L1 block heights, and then tries inserting them into L2 execution engine's blockchain. func (s *Syncer) ProcessL1Blocks(ctx context.Context) error { + s.mutex.Lock() + defer s.mutex.Unlock() for { if err := s.processL1Blocks(ctx); err != nil { return err @@ -257,23 +260,17 @@ func (s *Syncer) onBlockProposed( return fmt.Errorf("failed to fetch tx list: %w", err) } - var decompressedTxListBytes []byte - if s.rpc.L2.ChainID.Cmp(params.HeklaNetworkID) == 0 { - decompressedTxListBytes = s.txListDecompressor.TryDecompressHekla( - meta.GetBlockID(), - txListBytes, - meta.GetBlobUsed(), - ) - } else { - decompressedTxListBytes = s.txListDecompressor.TryDecompress(meta.GetBlockID(), txListBytes, meta.GetBlobUsed()) - } - // Decompress the transactions list and try to insert a new head block to L2 EE. payloadData, err := s.insertNewHead( ctx, meta, parent, - decompressedTxListBytes, + s.txListDecompressor.TryDecompress( + s.rpc.L2.ChainID, + meta.GetBlockID(), + txListBytes, + meta.GetBlobUsed(), + ), &rawdb.L1Origin{ BlockID: meta.GetBlockID(), L2BlockHash: common.Hash{}, // Will be set by taiko-geth. @@ -409,10 +406,19 @@ func (s *Syncer) insertNewHead( return nil, fmt.Errorf("failed to create execution payloads: %w", err) } + var lastVerifiedBlockHash common.Hash + lastVerifiedBlockInfo, err := s.rpc.GetLastVerifiedBlock(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch last verified block: %w", err) + } + if payload.Number > lastVerifiedBlockInfo.BlockId { + lastVerifiedBlockHash = lastVerifiedBlockInfo.BlockHash + } + fc := &engine.ForkchoiceStateV1{ HeadBlockHash: payload.BlockHash, SafeBlockHash: payload.BlockHash, - FinalizedBlockHash: payload.BlockHash, + FinalizedBlockHash: lastVerifiedBlockHash, } // Update the fork choice @@ -467,7 +473,7 @@ func (s *Syncer) createExecutionPayloads( "timestamp", attributes.BlockMetadata.Timestamp, "mixHash", attributes.BlockMetadata.MixHash, "baseFee", utils.WeiToGWei(attributes.BaseFeePerGas), - "extraData", string(attributes.BlockMetadata.ExtraData), + "extraData", common.Bytes2Hex(attributes.BlockMetadata.ExtraData), "l1OriginHeight", attributes.L1Origin.L1BlockHeight, "l1OriginHash", attributes.L1Origin.L1BlockHash, ) diff --git a/packages/taiko-client/driver/chain_syncer/chain_syncer.go b/packages/taiko-client/driver/chain_syncer/chain_syncer.go index 6e17215d73d..27bbe746fec 100644 --- a/packages/taiko-client/driver/chain_syncer/chain_syncer.go +++ b/packages/taiko-client/driver/chain_syncer/chain_syncer.go @@ -114,20 +114,20 @@ func (s *L2ChainSyncer) Sync() error { // Get the execution engine's chain head. l2Head, err := s.rpc.L2.HeaderByNumber(s.ctx, nil) if err != nil { - return err + return fmt.Errorf("failed to get L2 chain head: %w", err) } log.Info( "L2 head information", "number", l2Head.Number, "hash", l2Head.Hash(), - "lastSyncedVerifiedBlockID", s.progressTracker.LastSyncedBlockID(), - "lastSyncedVerifiedBlockHash", s.progressTracker.LastSyncedBlockHash(), + "lastSyncedBlockID", s.progressTracker.LastSyncedBlockID(), + "lastSyncedBlockHash", s.progressTracker.LastSyncedBlockHash(), ) // Reset the L1Current cursor. if err := s.state.ResetL1Current(s.ctx, l2Head.Number); err != nil { - return err + return fmt.Errorf("failed to reset L1 current cursor: %w", err) } // Reset to the latest L2 execution engine's chain status. @@ -140,7 +140,7 @@ func (s *L2ChainSyncer) Sync() error { // AheadOfHeadToSync checks whether the L2 chain is ahead of the head to sync in protocol. func (s *L2ChainSyncer) AheadOfHeadToSync(heightToSync uint64) bool { - log.Debug( + log.Info( "Checking whether the execution engine is ahead of the head to sync", "heightToSync", heightToSync, "executionEngineHead", s.state.GetL2Head().Number, @@ -156,12 +156,23 @@ func (s *L2ChainSyncer) AheadOfHeadToSync(heightToSync uint64) bool { // If the L2 execution engine's chain is behind of the block head to sync, // we should keep the beacon sync. if s.state.GetL2Head().Number.Uint64() < heightToSync { + log.Info( + "L2 execution engine is behind of the head to sync", + "heightToSync", heightToSync, + "executionEngineHead", s.state.GetL2Head().Number, + ) return false } // If the L2 execution engine's chain is ahead of the block head to sync, // we can mark the beacon sync progress as finished. if s.progressTracker.LastSyncedBlockID() != nil { + log.Info( + "L2 execution engine is ahead of the head to sync", + "heightToSync", heightToSync, + "executionEngineHead", s.state.GetL2Head().Number, + "lastSyncedBlockID", s.progressTracker.LastSyncedBlockID(), + ) return s.state.GetL2Head().Number.Uint64() >= s.progressTracker.LastSyncedBlockID().Uint64() } @@ -182,15 +193,14 @@ func (s *L2ChainSyncer) needNewBeaconSyncTriggered() (uint64, bool, error) { // For full sync mode, we will use the verified block head, // and for snap sync mode, we will use the latest block head. - var ( - blockID uint64 - err error - ) + var blockID uint64 switch s.syncMode { case downloader.SnapSync.String(): - if blockID, err = s.rpc.L2CheckPoint.BlockNumber(s.ctx); err != nil { + headL1Origin, err := s.rpc.L2CheckPoint.HeadL1Origin(s.ctx) + if err != nil { return 0, false, err } + blockID = headL1Origin.BlockID.Uint64() case downloader.FullSync.String(): stateVars, err := s.rpc.GetProtocolStateVariables(&bind.CallOpts{Context: s.ctx}) if err != nil { diff --git a/packages/taiko-client/driver/config.go b/packages/taiko-client/driver/config.go index 3eb3b95e236..e393128c33b 100644 --- a/packages/taiko-client/driver/config.go +++ b/packages/taiko-client/driver/config.go @@ -17,12 +17,16 @@ import ( // Config contains the configurations to initialize a Taiko driver. type Config struct { *rpc.ClientConfig - P2PSync bool - P2PSyncTimeout time.Duration - RetryInterval time.Duration - MaxExponent uint64 - BlobServerEndpoint *url.URL - SocialScanEndpoint *url.URL + P2PSync bool + P2PSyncTimeout time.Duration + RetryInterval time.Duration + MaxExponent uint64 + BlobServerEndpoint *url.URL + SocialScanEndpoint *url.URL + SoftBlockServerPort uint64 + SoftBlockServerJWTSecret []byte + SoftBlockServerCORSOrigins string + SoftBlockServerCheckSig bool } // NewConfigFromCliContext creates a new config instance from @@ -69,6 +73,15 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) { return nil, errors.New("empty L1 beacon endpoint, blob server and Social Scan endpoint") } + var softBlockServerJWTSecret []byte + if c.String(flags.SoftBlockServerJWTSecret.Name) != "" { + if softBlockServerJWTSecret, err = jwt.ParseSecretFromFile( + c.String(flags.SoftBlockServerJWTSecret.Name), + ); err != nil { + return nil, fmt.Errorf("invalid JWT secret file: %w", err) + } + } + var timeout = c.Duration(flags.RPCTimeout.Name) return &Config{ ClientConfig: &rpc.ClientConfig{ @@ -82,11 +95,15 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) { JwtSecret: string(jwtSecret), Timeout: timeout, }, - RetryInterval: c.Duration(flags.BackOffRetryInterval.Name), - P2PSync: p2pSync, - P2PSyncTimeout: c.Duration(flags.P2PSyncTimeout.Name), - MaxExponent: c.Uint64(flags.MaxExponent.Name), - BlobServerEndpoint: blobServerEndpoint, - SocialScanEndpoint: socialScanEndpoint, + RetryInterval: c.Duration(flags.BackOffRetryInterval.Name), + P2PSync: p2pSync, + P2PSyncTimeout: c.Duration(flags.P2PSyncTimeout.Name), + MaxExponent: c.Uint64(flags.MaxExponent.Name), + BlobServerEndpoint: blobServerEndpoint, + SocialScanEndpoint: socialScanEndpoint, + SoftBlockServerPort: c.Uint64(flags.SoftBlockServerPort.Name), + SoftBlockServerJWTSecret: softBlockServerJWTSecret, + SoftBlockServerCORSOrigins: c.String(flags.SoftBlockServerCORSOrigins.Name), + SoftBlockServerCheckSig: c.Bool(flags.SoftBlockServerCheckSig.Name), }, nil } diff --git a/packages/taiko-client/driver/driver.go b/packages/taiko-client/driver/driver.go index 51967df1d93..c093c90c1f8 100644 --- a/packages/taiko-client/driver/driver.go +++ b/packages/taiko-client/driver/driver.go @@ -17,6 +17,7 @@ import ( "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/encoding" chainSyncer "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/chain_syncer" + softblocks "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/soft_blocks" "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/state" "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc" ) @@ -30,9 +31,10 @@ const ( // contract. type Driver struct { *Config - rpc *rpc.Client - l2ChainSyncer *chainSyncer.L2ChainSyncer - state *state.State + rpc *rpc.Client + l2ChainSyncer *chainSyncer.L2ChainSyncer + softblockServer *softblocks.SoftBlockAPIServer + state *state.State l1HeadCh chan *types.Header l1HeadSub event.Subscription @@ -89,6 +91,18 @@ func (d *Driver) InitFromConfig(ctx context.Context, cfg *Config) (err error) { d.l1HeadSub = d.state.SubL1HeadsFeed(d.l1HeadCh) + if d.SoftBlockServerPort > 0 { + if d.softblockServer, err = softblocks.New( + d.SoftBlockServerCORSOrigins, + d.SoftBlockServerJWTSecret, + d.l2ChainSyncer.BlobSyncer(), + d.rpc, + d.Config.SoftBlockServerCheckSig, + ); err != nil { + return err + } + } + return nil } @@ -98,6 +112,16 @@ func (d *Driver) Start() error { go d.reportProtocolStatus() go d.exchangeTransitionConfigLoop() + // Start the soft block server if it is enabled. + if d.softblockServer != nil { + log.Info("Starting soft block server", "port", d.SoftBlockServerPort) + go func() { + if err := d.softblockServer.Start(d.SoftBlockServerPort); err != nil { + log.Crit("Failed to start soft block server", "error", err) + } + }() + } + return nil } @@ -105,6 +129,12 @@ func (d *Driver) Start() error { func (d *Driver) Close(_ context.Context) { d.l1HeadSub.Unsubscribe() d.state.Close() + // Close the soft block server if it is enabled. + if d.softblockServer != nil { + if err := d.softblockServer.Shutdown(d.ctx); err != nil { + log.Error("Failed to shutdown soft block server", "error", err) + } + } d.wg.Wait() } diff --git a/packages/taiko-client/driver/driver_test.go b/packages/taiko-client/driver/driver_test.go index cbae3bf9525..b226c22ce32 100644 --- a/packages/taiko-client/driver/driver_test.go +++ b/packages/taiko-client/driver/driver_test.go @@ -1,18 +1,28 @@ package driver import ( + "bytes" + "compress/zlib" "context" + "fmt" "math/big" + "net/url" "os" "testing" "time" "github.com/ethereum-optimism/optimism/op-service/txmgr" "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/ethereum/go-ethereum/rlp" + "github.com/go-resty/resty/v2" "github.com/stretchr/testify/suite" "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/encoding" + softblocks "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/soft_blocks" "github.com/taikoxyz/taiko-mono/packages/taiko-client/internal/testutils" "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/jwt" "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc" @@ -340,6 +350,350 @@ func (s *DriverTestSuite) InitProposer() { s.p = p } +func (s *DriverTestSuite) TestInsertSoftBlocks() { + var ( + port = uint64(testutils.RandomPort()) + err error + ) + s.d.softblockServer, err = softblocks.New("*", nil, s.d.ChainSyncer().BlobSyncer(), s.RPCClient, true) + s.Nil(err) + go func() { s.NotNil(s.d.softblockServer.Start(port)) }() + defer func() { s.Nil(s.d.softblockServer.Shutdown(s.d.ctx)) }() + + url, err := url.Parse(fmt.Sprintf("http://localhost:%v", port)) + s.Nil(err) + + l2Head1, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + s.Nil(s.d.ChainSyncer().BlobSyncer().ProcessL1Blocks(context.Background())) + + // Propose a valid L2 block + s.ProposeAndInsertValidBlock(s.p, s.d.ChainSyncer().BlobSyncer()) + + l2Head2, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + l1Head1, err := s.d.rpc.L1.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + s.Greater(l2Head2.Number.Uint64(), l2Head1.Number.Uint64()) + + res, err := resty.New().R().Get(url.String() + "/healthz") + s.Nil(err) + s.True(res.IsSuccess()) + + // Try to insert a soft block with batch ID 0 + s.True(s.insertSoftBlock(url, l1Head1, l2Head2.Number.Uint64()+1, 0, false, false).IsSuccess()) + l2Head3, err := s.d.rpc.L2.BlockByNumber(context.Background(), nil) + s.Nil(err) + + s.Equal(2, len(l2Head3.Transactions())) + + l1Origin, err := s.RPCClient.L2.L1OriginByID(context.Background(), new(big.Int).Add(l2Head2.Number, common.Big1)) + s.Nil(err) + s.Equal(l2Head3.Number().Uint64(), l1Origin.BlockID.Uint64()) + s.Equal(l2Head3.Hash(), l1Origin.L2BlockHash) + s.Equal(uint64(0), l1Origin.L1BlockHeight.Uint64()) + s.Equal(common.Hash{}, l1Origin.L1BlockHash) + s.Equal(false, l1Origin.EndOfBlock) + s.Equal(false, l1Origin.EndOfPreconf) + s.Equal(uint64(0), l1Origin.BatchID.Uint64()) + s.True(l1Origin.IsSoftBlock()) + + // Try to patch a soft block with batch ID 1 + s.True(s.insertSoftBlock(url, l1Head1, l2Head2.Number.Uint64()+1, 1, true, false).IsSuccess()) + l2Head4, err := s.d.rpc.L2.BlockByNumber(context.Background(), nil) + s.Nil(err) + s.Equal(3, len(l2Head4.Transactions())) + s.Equal(l2Head3.Number().Uint64(), l2Head4.Number().Uint64()) + s.NotEqual(l2Head3.Hash(), l2Head4.Hash()) + + l1Origin2, err := s.RPCClient.L2.L1OriginByID(context.Background(), new(big.Int).Add(l2Head2.Number, common.Big1)) + s.Nil(err) + s.Equal(l2Head4.Number().Uint64(), l1Origin2.BlockID.Uint64()) + s.Equal(l2Head4.Hash(), l1Origin2.L2BlockHash) + s.Equal(uint64(0), l1Origin2.L1BlockHeight.Uint64()) + s.Equal(common.Hash{}, l1Origin2.L1BlockHash) + s.Equal(true, l1Origin2.EndOfBlock) + s.Equal(false, l1Origin2.EndOfPreconf) + s.Equal(uint64(1), l1Origin2.BatchID.Uint64()) + s.True(l1Origin2.IsSoftBlock()) + + canonicalL1Origin, err := s.RPCClient.L2.HeadL1Origin(context.Background()) + s.Nil(err) + s.Equal(l2Head2.Number.Uint64(), canonicalL1Origin.BlockID.Uint64()) + + // Try to patch an ended soft block + s.False(s.insertSoftBlock(url, l1Head1, l2Head2.Number.Uint64()+1, 1, true, false).IsSuccess()) + + // Try to insert a new soft block with batch ID 0 + s.True(s.insertSoftBlock(url, l1Head1, l2Head2.Number.Uint64()+2, 0, false, false).IsSuccess()) + l2Head5, err := s.d.rpc.L2.BlockByNumber(context.Background(), nil) + s.Nil(err) + s.Equal(2, len(l2Head5.Transactions())) + + // Propose 3 valid L2 blocks + s.ProposeAndInsertEmptyBlocks(s.p, s.d.ChainSyncer().BlobSyncer()) + + l2Head6, err := s.d.rpc.L2.BlockByNumber(context.Background(), l2Head3.Number()) + s.Nil(err) + s.Equal(l2Head3.Number().Uint64(), l2Head6.Number().Uint64()) + s.Equal(1, len(l2Head6.Transactions())) + + l1Origin3, err := s.RPCClient.L2.L1OriginByID(context.Background(), l2Head6.Number()) + s.Nil(err) + s.Equal(l2Head3.Number().Uint64(), l1Origin3.BlockID.Uint64()) + s.Equal(l2Head6.Hash(), l1Origin3.L2BlockHash) + s.NotZero(l1Origin3.L1BlockHeight.Uint64()) + s.NotEmpty(l1Origin3.L1BlockHash) + s.Equal(false, l1Origin3.EndOfBlock) + s.Equal(false, l1Origin3.EndOfPreconf) + s.Nil(l1Origin3.BatchID) + s.False(l1Origin3.IsSoftBlock()) +} + +func (s *DriverTestSuite) TestInsertSoftBlocksAfterEOB() { + var ( + port = uint64(testutils.RandomPort()) + epochs = testutils.RandomHash().Big().Uint64()%10 + 5 + err error + ) + s.d.softblockServer, err = softblocks.New("*", nil, s.d.ChainSyncer().BlobSyncer(), s.RPCClient, true) + s.Nil(err) + go func() { s.NotNil(s.d.softblockServer.Start(port)) }() + defer func() { s.Nil(s.d.softblockServer.Shutdown(s.d.ctx)) }() + + url, err := url.Parse(fmt.Sprintf("http://localhost:%v", port)) + s.Nil(err) + + headL1Origin, err := s.RPCClient.L2.HeadL1Origin(context.Background()) + s.Nil(err) + + for i := uint64(0); i < headL1Origin.BlockID.Uint64()+epochs+1; i++ { + s.ProposeAndInsertEmptyBlocks(s.p, s.d.ChainSyncer().BlobSyncer()) + } + + l1Head, err := s.d.rpc.L1.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + l2Head, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + for i := uint64(0); i < epochs; i++ { + s.True(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1+i, 0, false, false).IsSuccess()) + s.True(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1+i, 1, true, false).IsSuccess()) + s.False(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1+i, 0, true, false).IsSuccess()) + } + s.True(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1+epochs, 0, true, false).IsSuccess()) + s.False(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1+epochs, 1, false, false).IsSuccess()) + s.True(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+2+epochs, 0, false, true).IsSuccess()) + s.False(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+2+epochs, 1, true, false).IsSuccess()) + + l2Head2, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + s.Equal(l2Head.Number.Uint64()+2+epochs, l2Head2.Number.Uint64()) + + l1Origin, err := s.RPCClient.L2.L1OriginByID(context.Background(), l2Head2.Number) + s.Nil(err) + + s.Equal(l2Head2.Number.Uint64(), l1Origin.BlockID.Uint64()) + s.Equal(false, l1Origin.EndOfBlock) + s.Equal(true, l1Origin.EndOfPreconf) + s.True(l1Origin.IsSoftBlock()) + + headL1Origin, err = s.RPCClient.L2.HeadL1Origin(context.Background()) + s.Nil(err) + s.Equal(l2Head.Number.Uint64(), headL1Origin.BlockID.Uint64()) + s.False(headL1Origin.IsSoftBlock()) + + s.ProposeAndInsertEmptyBlocks(s.p, s.d.ChainSyncer().BlobSyncer()) + + l2Head3, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + s.Less(l2Head3.Number.Uint64(), l2Head2.Number.Uint64()) + headL1Origin, err = s.RPCClient.L2.HeadL1Origin(context.Background()) + s.Nil(err) + s.Equal(l2Head3.Number.Uint64(), headL1Origin.BlockID.Uint64()) + s.False(headL1Origin.IsSoftBlock()) +} + +func (s *DriverTestSuite) TestInsertSoftBlocksAfterEOP() { + var ( + port = uint64(testutils.RandomPort()) + epochs = testutils.RandomHash().Big().Uint64() % 5 + err error + ) + s.d.softblockServer, err = softblocks.New("*", nil, s.d.ChainSyncer().BlobSyncer(), s.RPCClient, true) + s.Nil(err) + go func() { s.NotNil(s.d.softblockServer.Start(port)) }() + defer func() { s.Nil(s.d.softblockServer.Shutdown(s.d.ctx)) }() + + url, err := url.Parse(fmt.Sprintf("http://localhost:%v", port)) + s.Nil(err) + + headL1Origin, err := s.RPCClient.L2.HeadL1Origin(context.Background()) + s.Nil(err) + + for i := uint64(0); i < headL1Origin.BlockID.Uint64()+epochs+1; i++ { + s.ProposeAndInsertEmptyBlocks(s.p, s.d.ChainSyncer().BlobSyncer()) + } + + l1Head, err := s.d.rpc.L1.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + l2Head, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + for i := uint64(0); i < epochs; i++ { + s.True(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1, i, false, false).IsSuccess()) + + latestSafeBlock, err := s.RPCClient.L2.HeaderByNumber(context.Background(), big.NewInt(-4)) + s.Nil(err) + s.Equal(l2Head.Number.Uint64(), latestSafeBlock.Number.Uint64()) + + latestFinalizedBlock, err := s.RPCClient.L2.HeaderByNumber(context.Background(), big.NewInt(-3)) + s.Nil(err) + s.Equal(uint64(0), latestFinalizedBlock.Number.Uint64()) + } + s.True(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1, epochs, false, true).IsSuccess()) + s.False(s.insertSoftBlock(url, l1Head, l2Head.Number.Uint64()+1, epochs+1, false, false).IsSuccess()) + + latestSafeBlock, err := s.RPCClient.L2.HeaderByNumber(context.Background(), big.NewInt(-4)) + s.Nil(err) + s.Equal(l2Head.Number.Uint64(), latestSafeBlock.Number.Uint64()) + + l2Head2, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + + // Remove soft blocks + res, err := resty.New(). + R(). + SetBody(&softblocks.RemoveSoftBlocksRequestBody{ + NewLastBlockID: l2Head2.Number.Uint64() - 1, + }). + Delete(url.String() + "/softBlocks") + s.Nil(err) + s.True(res.IsSuccess()) + + l2Head3, err := s.d.rpc.L2.HeaderByNumber(context.Background(), nil) + s.Nil(err) + s.Equal(l2Head2.Number.Uint64()-1, l2Head3.Number.Uint64()) + + latestFinalizedBlock, err := s.RPCClient.L2.HeaderByNumber(context.Background(), big.NewInt(-3)) + s.Nil(err) + s.Equal(uint64(0), latestFinalizedBlock.Number.Uint64()) +} + func TestDriverTestSuite(t *testing.T) { suite.Run(t, new(DriverTestSuite)) } + +// insertSoftBlock inserts a soft block with the given parameters. +func (s *DriverTestSuite) insertSoftBlock( + url *url.URL, + anchoredL1Block *types.Header, + l2BlockID uint64, + batchID uint64, + endOfBlock bool, + endOfPreconf bool, +) *resty.Response { + preconferPrivKey, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) + s.Nil(err) + + preconferAddress := crypto.PubkeyToAddress(preconferPrivKey.PublicKey) + + nonce, err := s.RPCClient.L2.NonceAt(context.Background(), s.TestAddr, nil) + s.Nil(err) + + tx := types.NewTransaction( + nonce, + common.BytesToAddress(testutils.RandomBytes(32)), + common.Big0, + 100_000, + new(big.Int).SetUint64(uint64(10*params.GWei)), + []byte{}, + ) + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(s.RPCClient.L2.ChainID), s.TestAddrPrivKey) + s.Nil(err) + + // If the transaction is underpriced, we just ingore it. + err = s.RPCClient.L2.SendTransaction(context.Background(), signedTx) + if err != nil { + s.Equal("replacement transaction underpriced", err.Error()) + } + + b, err := encodeAndCompressTxList([]*types.Transaction{signedTx}) + s.Nil(err) + + var marker softblocks.TransactionBatchMarker + if endOfBlock { + marker = softblocks.BatchMarkerEOB + } else if endOfPreconf { + marker = softblocks.BatchMarkerEOP + } else { + marker = softblocks.BatchMarkerEmpty + } + + txBatch := &softblocks.TransactionBatch{ + BlockID: l2BlockID, + ID: batchID, + TransactionsList: b, + BatchMarker: marker, + Signature: "", + BlockParams: &softblocks.SoftBlockParams{ + AnchorBlockID: anchoredL1Block.Number.Uint64(), + AnchorStateRoot: anchoredL1Block.Root, + Timestamp: anchoredL1Block.Time + 12, + Coinbase: preconferAddress, + }, + } + + payload, err := rlp.EncodeToBytes(txBatch) + s.Nil(err) + s.NotEmpty(payload) + + sig, err := crypto.Sign(crypto.Keccak256(payload), preconferPrivKey) + s.Nil(err) + txBatch.Signature = common.Bytes2Hex(sig) + + // Try to propose a soft block with batch ID 0 + res, err := resty.New(). + R(). + SetBody(&softblocks.BuildSoftBlockRequestBody{ + TransactionBatch: txBatch, + }). + Post(url.String() + "/softBlocks") + s.Nil(err) + log.Info("Soft block response", "body", res.String()) + return res +} + +// compress compresses the given txList bytes using zlib. +func compress(txListBytes []byte) ([]byte, error) { + var b bytes.Buffer + w := zlib.NewWriter(&b) + defer w.Close() + + if _, err := w.Write(txListBytes); err != nil { + return nil, err + } + + if err := w.Flush(); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// encodeAndCompressTxList encodes and compresses the given transactions list. +func encodeAndCompressTxList(txs types.Transactions) ([]byte, error) { + b, err := rlp.EncodeToBytes(txs) + if err != nil { + return nil, err + } + + return compress(b) +} diff --git a/packages/taiko-client/driver/soft_blocks/api.go b/packages/taiko-client/driver/soft_blocks/api.go new file mode 100644 index 00000000000..227ded79d66 --- /dev/null +++ b/packages/taiko-client/driver/soft_blocks/api.go @@ -0,0 +1,312 @@ +package softblocks + +import ( + "encoding/hex" + "errors" + "math/big" + "net/http" + + "github.com/ethereum/go-ethereum" + "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/rlp" + "github.com/labstack/echo/v4" +) + +// TransactionBatchMarker represents the status of a soft block transactions group. +type TransactionBatchMarker string + +// BatchMarker valid values. +const ( + BatchMarkerEmpty TransactionBatchMarker = "" + BatchMarkerEOB TransactionBatchMarker = "endOfBlock" + BatchMarkerEOP TransactionBatchMarker = "endOfPreconf" +) + +// SoftBlockParams represents the parameters for building a soft block. +type SoftBlockParams struct { + // @param timestamp uint64 Timestamp of the soft block + Timestamp uint64 `json:"timestamp"` + // @param coinbase string Coinbase of the soft block + Coinbase common.Address `json:"coinbase"` + + // @param anchorBlockID uint64 `_anchorBlockId` parameter of the `anchorV2` transaction in soft block + AnchorBlockID uint64 `json:"anchorBlockID"` + // @param anchorStateRoot string `_anchorStateRoot` parameter of the `anchorV2` transaction in soft block + AnchorStateRoot common.Hash `json:"anchorStateRoot"` +} + +// TransactionBatch represents a soft block group. +type TransactionBatch struct { + // @param blockId uint64 Block ID of the soft block + BlockID uint64 `json:"blockId"` + // @param batchId uint64 ID of this transaction batch + ID uint64 `json:"batchId"` + // @param transactions string zlib compressed RLP encoded bytes of a transactions list + TransactionsList []byte `json:"transactions"` + // @param batchType TransactionBatchMarker Marker of the transaction batch, + // @param either `end_of_block`, `end_of_preconf` or empty + BatchMarker TransactionBatchMarker `json:"batchType"` + // @param signature string Signature of this transaction batch + Signature string `json:"signature" rlp:"-"` + // @param blockParams SoftBlockParams Block parameters of the soft block + BlockParams *SoftBlockParams `json:"blockParams"` +} + +// ValidateSignature validates the signature of the transaction batch. +func (b *TransactionBatch) ValidateSignature() (bool, error) { + payload, err := rlp.EncodeToBytes(b) + if err != nil { + return false, err + } + + pubKey, err := crypto.SigToPub(crypto.Keccak256(payload), common.FromHex(b.Signature)) + if err != nil { + return false, err + } + + log.Info("Validate signature addresses", "expected", b.BlockParams.Coinbase.Hex(), "actual", crypto.PubkeyToAddress(*pubKey).Hex()) + + return crypto.PubkeyToAddress(*pubKey).Hex() == b.BlockParams.Coinbase.Hex(), nil +} + +// BuildSoftBlockRequestBody represents a request body when handling +// soft blocks creation requests. +type BuildSoftBlockRequestBody struct { + // @param transactionBatch TransactionBatch Transaction batch to be inserted into the soft block + TransactionBatch *TransactionBatch `json:"transactionBatch"` +} + +// CreateOrUpdateBlocksFromBatchResponseBody represents a response body when handling soft +// blocks creation requests. +type BuildSoftBlockResponseBody struct { + // @param blockHeader types.Header of the soft block + BlockHeader *types.Header `json:"blockHeader"` +} + +// BuildSoftBlock handles a soft block creation request, +// if the soft block transactions batch in request are valid, it will insert or reorg the correspoinding the soft +// block to the backend L2 execution engine and return a success response. +// +// @Description Insert a batch of transactions into a soft block for preconfirmation. If the batch is the +// @Description first for a block, a new soft block will be created. Otherwise, the transactions will +// @Description be appended to the existing soft block. The API will fail if: +// @Description 1) the block is not soft +// @Description 2) block-level parameters are invalid or do not match the current soft block’s parameters +// @Description 3) the batch ID is not exactly 1 greater than the previous one +// @Description 4) the last batch of the block indicates no further transactions are allowed +// @Param body body BuildSoftBlockRequestBody true "soft block creation request body" +// @Accept json +// @Produce json +// @Success 200 {object} BuildSoftBlockResponseBody +// @Router /softBlocks [post] +func (s *SoftBlockAPIServer) BuildSoftBlock(c echo.Context) error { + // Parse the request body. + reqBody := new(BuildSoftBlockRequestBody) + if err := c.Bind(reqBody); err != nil { + return s.returnError(c, http.StatusUnprocessableEntity, err) + } + if reqBody.TransactionBatch == nil { + return s.returnError(c, http.StatusBadRequest, errors.New("transactionBatch is required")) + } + + log.Info( + "New soft block building request", + "blockID", reqBody.TransactionBatch.BlockID, + "batchID", reqBody.TransactionBatch.ID, + "batchMarker", reqBody.TransactionBatch.BatchMarker, + "transactionsListBytes", len(reqBody.TransactionBatch.TransactionsList), + "signature", reqBody.TransactionBatch.Signature, + "timestamp", reqBody.TransactionBatch.BlockParams.Timestamp, + "coinbase", reqBody.TransactionBatch.BlockParams.Coinbase, + "anchorBlockID", reqBody.TransactionBatch.BlockParams.AnchorBlockID, + "anchorStateRoot", reqBody.TransactionBatch.BlockParams.AnchorStateRoot, + "transactionsList", hex.EncodeToString(reqBody.TransactionBatch.TransactionsList), + ) + + // Request body validation. + if reqBody.TransactionBatch.BlockParams == nil { + return s.returnError(c, http.StatusBadRequest, errors.New("blockParams is required")) + } + if reqBody.TransactionBatch.BlockParams.AnchorBlockID == 0 { + return s.returnError(c, http.StatusBadRequest, errors.New("non-zero anchorBlockID is required")) + } + if reqBody.TransactionBatch.BlockParams.AnchorStateRoot == (common.Hash{}) { + return s.returnError(c, http.StatusBadRequest, errors.New("empty anchorStateRoot")) + } + if reqBody.TransactionBatch.BlockParams.Timestamp == 0 { + return s.returnError(c, http.StatusBadRequest, errors.New("non-zero timestamp is required")) + } + if reqBody.TransactionBatch.BlockParams.Coinbase == (common.Address{}) { + return s.returnError(c, http.StatusBadRequest, errors.New("empty coinbase")) + } + + // If the `--softBlock.signatureCheck` flag is enabled, validate the signature. + if s.checkSig { + ok, err := reqBody.TransactionBatch.ValidateSignature() + if err != nil { + log.Warn("Failed to validate signature", "err", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + if !ok { + log.Warn( + "Invalid signature", + "signature", reqBody.TransactionBatch.Signature, + "coinbase", reqBody.TransactionBatch.BlockParams.Coinbase.Hex(), + ) + return s.returnError(c, http.StatusBadRequest, errors.New("invalid signature")) + } + } + + // Check if the L2 execution engine is syncing from L1. + progress, err := s.rpc.L2ExecutionEngineSyncProgress(c.Request().Context()) + if err != nil { + log.Warn("Failed to get L2EE progress", "err", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + if progress.IsSyncing() { + return s.returnError(c, http.StatusBadRequest, errors.New("L2 execution engine is syncing")) + } + + // Check if the softblock batch or the current preconf process is ended. + l1Origin, err := s.rpc.L2.L1OriginByID( + c.Request().Context(), + new(big.Int).SetUint64(reqBody.TransactionBatch.BlockID), + ) + if err != nil && err.Error() != ethereum.NotFound.Error() { + log.Warn("Failed to find l1 origin", "err", err) + return s.returnError(c, http.StatusInternalServerError, err) + } + if l1Origin != nil { + if l1Origin.EndOfBlock { + return s.returnError(c, http.StatusBadRequest, errors.New("soft block has already been marked as ended")) + } + if l1Origin.EndOfPreconf { + return s.returnError( + c, + http.StatusBadRequest, + errors.New("preconfirmation has already been marked as ended"), + ) + } + } + + // Insert the soft block. + header, err := s.chainSyncer.InsertSoftBlockFromTransactionsBatch( + c.Request().Context(), + reqBody.TransactionBatch.BlockID, + reqBody.TransactionBatch.ID, + s.txListDecompressor.TryDecompress( + s.rpc.L2.ChainID, + new(big.Int).SetUint64(reqBody.TransactionBatch.BlockID), + reqBody.TransactionBatch.TransactionsList, + true, + ), + reqBody.TransactionBatch.BatchMarker, + reqBody.TransactionBatch.BlockParams, + ) + if err != nil { + log.Warn("Failed to insert softblock from tx batch", "err", err) + return s.returnError(c, http.StatusInternalServerError, err) + } + + return c.JSON(http.StatusOK, BuildSoftBlockResponseBody{BlockHeader: header}) +} + +// RemoveSoftBlocksRequestBody represents a request body when resetting the backend +// L2 execution engine soft head. +type RemoveSoftBlocksRequestBody struct { + // @param newLastBlockID uint64 New last block ID of the blockchain, it should + // @param not smaller than the canonical chain's highest block ID. + NewLastBlockID uint64 `json:"newLastBlockId"` +} + +// RemoveSoftBlocksResponseBody represents a response body when resetting the backend +// L2 execution engine soft head. +type RemoveSoftBlocksResponseBody struct { + // @param lastBlockID uint64 Current highest block ID of the blockchain (including soft blocks) + LastBlockID uint64 `json:"lastBlockId"` + // @param lastProposedBlockID uint64 Highest block ID of the cnonical chain + LastProposedBlockID uint64 `json:"lastProposedBlockID"` + // @param headsRemoved uint64 Number of soft heads removed + HeadsRemoved uint64 `json:"headsRemoved"` +} + +// RemoveSoftBlocks removes the backend L2 execution engine soft head. +// +// @Description Remove all soft blocks from the blockchain beyond the specified block height, +// @Description ensuring the latest block ID does not exceed the given height. This method will fail if +// @Description the block with an ID one greater than the specified height is not a soft block. If the +// @Description specified block height is greater than the latest soft block ID, the method will succeed +// @Description without modifying the blockchain. +// @Param body body RemoveSoftBlocksRequestBody true "soft blocks removing request body" +// @Accept json +// @Produce json +// @Success 200 {object} RemoveSoftBlocksResponseBody +// @Router /softBlocks [delete] +func (s *SoftBlockAPIServer) RemoveSoftBlocks(c echo.Context) error { + // Parse the request body. + reqBody := new(RemoveSoftBlocksRequestBody) + if err := c.Bind(reqBody); err != nil { + return s.returnError(c, http.StatusUnprocessableEntity, err) + } + + // Request body validation. + canonicalHeadL1Origin, err := s.rpc.L2.HeadL1Origin(c.Request().Context()) + if err != nil { + return s.returnError(c, http.StatusInternalServerError, err) + } + + currentHead, err := s.rpc.L2.HeaderByNumber(c.Request().Context(), nil) + if err != nil { + return s.returnError(c, http.StatusInternalServerError, err) + } + + log.Info( + "New soft block removing request", + "newLastBlockId", reqBody.NewLastBlockID, + "canonicalHead", canonicalHeadL1Origin.BlockID.Uint64(), + "currentHead", currentHead.Number.Uint64(), + ) + + if reqBody.NewLastBlockID < canonicalHeadL1Origin.BlockID.Uint64() { + return s.returnError( + c, + http.StatusBadRequest, + errors.New("newLastBlockId must not be smaller than the canonical chain's highest block ID"), + ) + } + + if err := s.chainSyncer.RemoveSoftBlocks(c.Request().Context(), reqBody.NewLastBlockID); err != nil { + return s.returnError(c, http.StatusBadRequest, err) + } + + newHead, err := s.rpc.L2.HeaderByNumber(c.Request().Context(), nil) + if err != nil { + return s.returnError(c, http.StatusInternalServerError, err) + } + + return c.JSON(http.StatusOK, RemoveSoftBlocksResponseBody{ + LastBlockID: newHead.Number.Uint64(), + LastProposedBlockID: canonicalHeadL1Origin.BlockID.Uint64(), + HeadsRemoved: currentHead.Number.Uint64() - newHead.Number.Uint64(), + }) +} + +// HealthCheck is the endpoints for probes. +// +// @Summary Get current server health status +// @ID health-check +// @Accept json +// @Produce json +// @Success 200 {object} string +// @Router /healthz [get] +func (s *SoftBlockAPIServer) HealthCheck(c echo.Context) error { + return c.NoContent(http.StatusOK) +} + +// returnError is a helper function to return an error response. +func (s *SoftBlockAPIServer) returnError(c echo.Context, statusCode int, err error) error { + return c.JSON(statusCode, map[string]string{"error": err.Error()}) +} diff --git a/packages/taiko-client/driver/soft_blocks/server.go b/packages/taiko-client/driver/soft_blocks/server.go new file mode 100644 index 00000000000..1ace625b32d --- /dev/null +++ b/packages/taiko-client/driver/soft_blocks/server.go @@ -0,0 +1,133 @@ +package softblocks + +import ( + "context" + "fmt" + "os" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + echojwt "github.com/labstack/echo-jwt/v4" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + + txListDecompressor "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/txlist_decompressor" + "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc" +) + +// softBlockChainSyncer is an interface for soft block chain syncer. +type softBlockChainSyncer interface { + InsertSoftBlockFromTransactionsBatch( + ctx context.Context, + blockID uint64, + batchID uint64, + txListBytes []byte, + batchMarker TransactionBatchMarker, + softBlockParams *SoftBlockParams, + ) (*types.Header, error) + RemoveSoftBlocks(ctx context.Context, newLastBlockID uint64) error +} + +// @title Taiko Soft Block Server API +// @version 1.0 +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url https://community.taiko.xyz/ +// @contact.email info@taiko.xyz + +// @license.name MIT +// @license.url https://github.com/taikoxyz/taiko-mono/blob/main/LICENSE.md +// SoftBlockAPIServer represents a soft blcok server instance. +type SoftBlockAPIServer struct { + echo *echo.Echo + chainSyncer softBlockChainSyncer + rpc *rpc.Client + txListDecompressor *txListDecompressor.TxListDecompressor + checkSig bool +} + +// New creates a new soft blcok server instance, and starts the server. +func New( + cors string, + jwtSecret []byte, + chainSyncer softBlockChainSyncer, + cli *rpc.Client, + checkSig bool, +) (*SoftBlockAPIServer, error) { + protocolConfigs, err := rpc.GetProtocolConfigs(cli.TaikoL1, nil) + if err != nil { + return nil, fmt.Errorf("failed to fetch protocol configs: %w", err) + } + + log.Info("Initializing soft block server", "checkSig", checkSig) + + server := &SoftBlockAPIServer{ + echo: echo.New(), + chainSyncer: chainSyncer, + txListDecompressor: txListDecompressor.NewTxListDecompressor( + uint64(protocolConfigs.BlockMaxGasLimit), + rpc.BlockMaxTxListBytes, + cli.L2.ChainID, + ), + rpc: cli, + checkSig: checkSig, + } + + server.echo.HideBanner = true + server.configureMiddleware([]string{cors}) + server.configureRoutes() + if jwtSecret != nil { + server.echo.Use(echojwt.JWT(jwtSecret)) + } + + return server, nil +} + +// LogSkipper implements the `middleware.Skipper` interface. +func LogSkipper(c echo.Context) bool { + switch c.Request().URL.Path { + case "/healthz": + return true + default: + return true + } +} + +// configureMiddleware configures the server middlewares. +func (s *SoftBlockAPIServer) configureMiddleware(corsOrigins []string) { + s.echo.Use(middleware.RequestID()) + + s.echo.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + Skipper: LogSkipper, + Format: `{"time":"${time_rfc3339_nano}","level":"INFO","message":{"id":"${id}","remote_ip":"${remote_ip}",` + + `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + + `"response_status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}",` + + `"bytes_in":${bytes_in},"bytes_out":${bytes_out}}}` + "\n", + Output: os.Stdout, + })) + + // Add CORS middleware + s.echo.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: corsOrigins, + AllowCredentials: true, + })) +} + +// Start starts the HTTP server. +func (s *SoftBlockAPIServer) Start(port uint64) error { + return s.echo.Start(fmt.Sprintf(":%v", port)) +} + +// Shutdown shuts down the HTTP server. +func (s *SoftBlockAPIServer) Shutdown(ctx context.Context) error { + return s.echo.Shutdown(ctx) +} + +// configureRoutes contains all routes which will be used by prover server. +func (s *SoftBlockAPIServer) configureRoutes() { + s.echo.GET("/", s.HealthCheck) + s.echo.GET("/healthz", s.HealthCheck) + s.echo.POST("/softBlocks", s.BuildSoftBlock) + s.echo.DELETE("/softBlocks", s.RemoveSoftBlocks) +} diff --git a/packages/taiko-client/driver/soft_blocks/server_test.go b/packages/taiko-client/driver/soft_blocks/server_test.go new file mode 100644 index 00000000000..c1aa66d1777 --- /dev/null +++ b/packages/taiko-client/driver/soft_blocks/server_test.go @@ -0,0 +1,36 @@ +package softblocks + +import ( + "context" + "testing" + + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/suite" + + "github.com/taikoxyz/taiko-mono/packages/taiko-client/internal/testutils" +) + +type SoftBlockAPIServerTestSuite struct { + testutils.ClientTestSuite + s *SoftBlockAPIServer +} + +func (s *SoftBlockAPIServerTestSuite) SetupTest() { + s.ClientTestSuite.SetupTest() + server, err := New("*", nil, nil, s.RPCClient, true) + s.Nil(err) + s.s = server + go func() { + s.NotPanics(func() { + log.Error("Start test soft block server", "error", s.s.Start(uint64(testutils.RandomPort()))) + }) + }() +} + +func (s *SoftBlockAPIServerTestSuite) TestShutdown() { + s.Nil(s.s.Shutdown(context.Background())) +} + +func TestSoftBlockAPIServerTestSuite(t *testing.T) { + suite.Run(t, new(SoftBlockAPIServerTestSuite)) +} diff --git a/packages/taiko-client/driver/state/l1_current.go b/packages/taiko-client/driver/state/l1_current.go index 01f038387d4..467a94fa933 100644 --- a/packages/taiko-client/driver/state/l1_current.go +++ b/packages/taiko-client/driver/state/l1_current.go @@ -3,6 +3,7 @@ package state import ( "context" "errors" + "fmt" "math/big" "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings" @@ -57,11 +58,11 @@ func (s *State) ResetL1Current(ctx context.Context, blockID *big.Int) error { blockInfo, err = s.rpc.GetL2BlockInfo(ctx, blockID) } if err != nil { - return err + return fmt.Errorf("failed to get L2 block (%d) info from TaikoL1 contract: %w", blockID, err) } l1Current, err := s.rpc.L1.HeaderByNumber(ctx, new(big.Int).SetUint64(blockInfo.ProposedIn)) if err != nil { - return err + return fmt.Errorf("failed to fetch L1 header by number (%d): %w", blockID, err) } s.SetL1Current(l1Current) diff --git a/packages/taiko-client/driver/txlist_decompressor/txlist_decompressor.go b/packages/taiko-client/driver/txlist_decompressor/txlist_decompressor.go index e288115fc8b..bcdc4ee4010 100644 --- a/packages/taiko-client/driver/txlist_decompressor/txlist_decompressor.go +++ b/packages/taiko-client/driver/txlist_decompressor/txlist_decompressor.go @@ -1,6 +1,7 @@ package txlistdecompressor import ( + "github.com/ethereum/go-ethereum/params" "math/big" "github.com/ethereum/go-ethereum/core/types" @@ -39,6 +40,26 @@ func NewTxListDecompressor( // less than or equal to maxBytesPerTxList. // 2. The transaction list bytes must be able to be RLP decoded into a list of transactions. func (v *TxListDecompressor) TryDecompress( + chainID *big.Int, + blockID *big.Int, + txListBytes []byte, + blobUsed bool, +) []byte { + if chainID.Cmp(params.HeklaNetworkID) == 0 { + return v.tryDecompressHekla(blockID, txListBytes, blobUsed) + } + + return v.tryDecompress(blockID, txListBytes, blobUsed) +} + +// TryDecompress validates and decompresses whether the transactions list in the TaikoL1.proposeBlock transaction's +// input data is valid, the rules are: +// - If the transaction list is empty, it's valid. +// - If the transaction list is not empty: +// 1. If the transaction list is using calldata, the compressed bytes of the transaction list must be +// less than or equal to maxBytesPerTxList. +// 2. The transaction list bytes must be able to be RLP decoded into a list of transactions. +func (v *TxListDecompressor) tryDecompress( blockID *big.Int, txListBytes []byte, blobUsed bool, @@ -76,10 +97,10 @@ func (v *TxListDecompressor) TryDecompress( return txListBytes } -// TryDecompressHekla is the same as TryDecompress, but it's used for Hekla network with +// tryDecompressHekla is the same as TryDecompress, but it's used for Hekla network with // an incorrect legacy bytes size check. // ref: https://github.com/taikoxyz/taiko-client/pull/783 -func (v *TxListDecompressor) TryDecompressHekla( +func (v *TxListDecompressor) tryDecompressHekla( blockID *big.Int, txListBytes []byte, blobUsed bool, diff --git a/packages/taiko-client/integration_test/hive_test.go b/packages/taiko-client/integration_test/hive_test.go index 0df80196797..cbf4a74e5af 100644 --- a/packages/taiko-client/integration_test/hive_test.go +++ b/packages/taiko-client/integration_test/hive_test.go @@ -80,6 +80,7 @@ func testBlobAPI(t *testing.T, pattern string, clients []string) { handler, err := hivesim.NewHiveFramework(&hivesim.HiveConfig{ BuildOutput: false, ContainerOutput: true, + DockerPull: false, BaseDir: os.Getenv("HIVE_DIR"), SimPattern: "taiko", SimTestPattern: pattern, diff --git a/packages/taiko-client/internal/docker/nodes/docker-compose.yml b/packages/taiko-client/internal/docker/nodes/docker-compose.yml index 4880101823c..160d2aa9502 100644 --- a/packages/taiko-client/internal/docker/nodes/docker-compose.yml +++ b/packages/taiko-client/internal/docker/nodes/docker-compose.yml @@ -16,9 +16,9 @@ services: - --hardfork - cancun - l2_execution_engine: - container_name: l2_node - image: us-docker.pkg.dev/evmchain/images/taiko-geth:taiko + l2_geth: + container_name: l2_geth + image: us-docker.pkg.dev/evmchain/images/taiko-geth:softblock restart: unless-stopped pull_policy: always volumes: diff --git a/packages/taiko-client/pkg/rpc/methods.go b/packages/taiko-client/pkg/rpc/methods.go index d89b1457a7c..56bc2a01554 100644 --- a/packages/taiko-client/pkg/rpc/methods.go +++ b/packages/taiko-client/pkg/rpc/methods.go @@ -114,7 +114,7 @@ func (c *Client) WaitTillL2ExecutionEngineSynced(ctx context.Context) error { return err } - if progress.isSyncing() { + if progress.IsSyncing() { log.Info( "L2 execution engine is syncing", "currentBlockID", progress.CurrentBlockID, @@ -369,23 +369,50 @@ func (c *Client) GetPoolContent( ctxWithTimeout, cancel := CtxWithTimeoutOrDefault(ctx, defaultTimeout) defer cancel() - l1Head, err := c.L1.HeaderByNumber(ctx, nil) + // Get the latest L2 block header at first. + l2Head, err := c.L2.HeaderByNumber(ctx, nil) if err != nil { return nil, err } - l2Head, err := c.L2.HeaderByNumber(ctx, nil) + l1Origin, err := c.L2.L1OriginByID(ctx, l2Head.Number) + if err != nil && err.Error() != ethereum.NotFound.Error() { + return nil, err + } + + var ( + L1HeadNum *big.Int + L2HeadNum *big.Int + timestamp = uint64(time.Now().Unix()) + ) + + if l1Origin != nil && l1Origin.IsSoftBlock() && !l1Origin.EndOfPreconf && !l1Origin.EndOfBlock { + // Check if this is an unfinished soft block, if not, we will use the latest L1 / L2 block number from the L1Origin. + // Otherwise, we will use the L1 / L2 block number in L1Origin. + L1HeadNum = l1Origin.L1BlockHeight + L2HeadNum = new(big.Int).Sub(l1Origin.BlockID, common.Big1) + } + + l1Head, err := c.L1.HeaderByNumber(ctx, L1HeadNum) if err != nil { return nil, err } + if L2HeadNum != nil { + timestamp = l2Head.Time + l2Head, err = c.L2.HeaderByNumber(ctx, L2HeadNum) + if err != nil { + return nil, err + } + } + baseFee, err := c.CalculateBaseFee( ctx, l2Head, l1Head.Number, - chainConfig.IsOntake(new(big.Int).Add(l2Head.Number, common.Big1)), + true, &chainConfig.ProtocolConfigs.BaseFeeConfig, - uint64(time.Now().Unix()), + timestamp, ) if err != nil { return nil, err @@ -430,8 +457,8 @@ type L2SyncProgress struct { HighestBlockID *big.Int } -// isSyncing returns true if the L2 execution engine is syncing with L1. -func (p *L2SyncProgress) isSyncing() bool { +// IsSyncing returns true if the L2 execution engine is syncing with L1. +func (p *L2SyncProgress) IsSyncing() bool { if p.SyncProgress == nil { return false } @@ -510,6 +537,19 @@ func (c *Client) GetProtocolStateVariables(opts *bind.CallOpts) (*struct { return GetProtocolStateVariables(c.TaikoL1, opts) } +// GetLastVerifiedBlock gets the last verified block from TaikoL1 contract. +func (c *Client) GetLastVerifiedBlock(ctx context.Context) (struct { + BlockId uint64 //nolint:stylecheck + BlockHash [32]byte + StateRoot [32]byte + VerifiedAt uint64 +}, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + + return c.TaikoL1.GetLastVerifiedBlock(&bind.CallOpts{Context: ctxWithTimeout}) +} + // GetL2BlockInfo fetches the L2 block information from the protocol. func (c *Client) GetL2BlockInfo(ctx context.Context, blockID *big.Int) (bindings.TaikoDataBlockV2, error) { ctxWithTimeout, cancel := CtxWithTimeoutOrDefault(ctx, defaultTimeout) @@ -669,10 +709,12 @@ func (c *Client) checkSyncedL1SnippetFromAnchor( log.Info("Check synced L1 snippet from anchor", "blockID", blockID, "l1Height", l1Height) block, err := c.L2.BlockByNumber(ctx, blockID) if err != nil { + log.Error("Failed to fetch L2 block", "blockID", blockID, "error", err) return false, err } parent, err := c.L2.BlockByHash(ctx, block.ParentHash()) if err != nil { + log.Error("Failed to fetch L2 parent block", "blockID", blockID, "parentHash", block.ParentHash(), "error", err) return false, err } @@ -680,6 +722,7 @@ func (c *Client) checkSyncedL1SnippetFromAnchor( block.Transactions()[0], ) if err != nil { + log.Error("Failed to parse L1 snippet from anchor transaction", "blockID", blockID, "error", err) return false, err } @@ -722,7 +765,7 @@ func (c *Client) getSyncedL1SnippetFromAnchor( ) { method, err := encoding.TaikoL2ABI.MethodById(tx.Data()) if err != nil { - return common.Hash{}, 0, 0, err + return common.Hash{}, 0, 0, fmt.Errorf("failed to get TaikoL2.Anchor method by ID: %w", err) } var ok bool @@ -759,7 +802,7 @@ func (c *Client) getSyncedL1SnippetFromAnchor( args := map[string]interface{}{} if err := method.Inputs.UnpackIntoMap(args, tx.Data()[4:]); err != nil { - return common.Hash{}, 0, 0, err + return common.Hash{}, 0, 0, fmt.Errorf("failed to unpack anchor transaction calldata: %w", err) } l1Height, ok = args["_anchorBlockId"].(uint64) diff --git a/packages/taiko-client/pkg/rpc/utils.go b/packages/taiko-client/pkg/rpc/utils.go index a7c998b8aa2..16c36f81271 100644 --- a/packages/taiko-client/pkg/rpc/utils.go +++ b/packages/taiko-client/pkg/rpc/utils.go @@ -23,6 +23,21 @@ var ( BlockMaxTxListBytes uint64 = (params.BlobTxBytesPerFieldElement - 1) * params.BlobTxFieldElementsPerBlob ) +// GetProtocolConfigs gets the protocol configs from TaikoL1 contract. +func GetProtocolConfigs( + taikoL1Client *bindings.TaikoL1Client, + opts *bind.CallOpts, +) (bindings.TaikoDataConfig, error) { + var cancel context.CancelFunc + if opts == nil { + opts = &bind.CallOpts{Context: context.Background()} + } + opts.Context, cancel = CtxWithTimeoutOrDefault(opts.Context, defaultTimeout) + defer cancel() + + return taikoL1Client.GetConfig(opts) +} + // GetProtocolStateVariables gets the protocol states from TaikoL1 contract. func GetProtocolStateVariables( taikoL1Client *bindings.TaikoL1Client, diff --git a/packages/taiko-client/pkg/utils/txmgr_selector.go b/packages/taiko-client/pkg/utils/txmgr_selector.go new file mode 100644 index 00000000000..1224bff4588 --- /dev/null +++ b/packages/taiko-client/pkg/utils/txmgr_selector.go @@ -0,0 +1,62 @@ +package utils + +import ( + "time" + + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +var ( + defaultPrivateTxMgrRetryInterval = 5 * time.Minute +) + +// TxMgrSelector is responsible for selecting the correct transaction manager, +// it will choose the transaction manager for a private mempool if it is available and works well, +// otherwise it will choose the normal transaction manager. +type TxMgrSelector struct { + txMgr *txmgr.SimpleTxManager + privateTxMgr *txmgr.SimpleTxManager + privateTxMgrFailedAt *time.Time + privateTxMgrRetryInterval time.Duration +} + +// NewTxMgrSelector creates a new TxMgrSelector instance. +func NewTxMgrSelector( + txMgr *txmgr.SimpleTxManager, + privateTxMgr *txmgr.SimpleTxManager, + privateTxMgrRetryInterval *time.Duration, +) *TxMgrSelector { + retryInterval := defaultPrivateTxMgrRetryInterval + if privateTxMgrRetryInterval != nil { + retryInterval = *privateTxMgrRetryInterval + } + + return &TxMgrSelector{ + txMgr: txMgr, + privateTxMgr: privateTxMgr, + privateTxMgrFailedAt: nil, + privateTxMgrRetryInterval: retryInterval, + } +} + +// Select selects a transaction manager based on the current state. +func (s *TxMgrSelector) Select() (*txmgr.SimpleTxManager, bool) { + // If there is no private transaction manager, return the normal transaction manager. + if s.privateTxMgr == nil { + return s.txMgr, false + } + + // If the private transaction manager has failed, check if it is time to retry. + if s.privateTxMgrFailedAt == nil || time.Now().After(s.privateTxMgrFailedAt.Add(s.privateTxMgrRetryInterval)) { + return s.privateTxMgr, true + } + + // Otherwise, return the normal transaction manager. + return s.txMgr, false +} + +// RecordPrivateTxMgrFailed records the time when the private transaction manager has failed. +func (s *TxMgrSelector) RecordPrivateTxMgrFailed() { + now := time.Now() + s.privateTxMgrFailedAt = &now +} diff --git a/packages/taiko-client/pkg/utils/txmgr_selector_test.go b/packages/taiko-client/pkg/utils/txmgr_selector_test.go new file mode 100644 index 00000000000..92ff69d1d07 --- /dev/null +++ b/packages/taiko-client/pkg/utils/txmgr_selector_test.go @@ -0,0 +1,29 @@ +package utils + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/stretchr/testify/require" +) + +var ( + testTxMgr = &txmgr.SimpleTxManager{} + testSelector = NewTxMgrSelector(testTxMgr, nil, nil) +) + +func TestNewTxMgrSelector(t *testing.T) { + require.Equal(t, defaultPrivateTxMgrRetryInterval, testSelector.privateTxMgrRetryInterval) +} + +func TestSelect(t *testing.T) { + txMgr, isPrivate := testSelector.Select() + require.NotNil(t, txMgr) + require.False(t, isPrivate) +} + +func TestRecordPrivateTxMgrFailed(t *testing.T) { + require.Nil(t, testSelector.privateTxMgrFailedAt) + testSelector.RecordPrivateTxMgrFailed() + require.NotNil(t, testSelector.privateTxMgrFailedAt) +} diff --git a/packages/taiko-client/pkg/utils/utils.go b/packages/taiko-client/pkg/utils/utils.go new file mode 100644 index 00000000000..7c7ca2845aa --- /dev/null +++ b/packages/taiko-client/pkg/utils/utils.go @@ -0,0 +1,153 @@ +package utils + +import ( + "bytes" + "compress/zlib" + "crypto/rand" + "errors" + "fmt" + "io" + "math" + "math/big" + "os" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/joho/godotenv" + "github.com/modern-go/reflect2" + "golang.org/x/exp/constraints" +) + +// LoadEnv loads all the test environment variables. +func LoadEnv() { + currentPath, err := os.Getwd() + if err != nil { + log.Debug("Failed to get current path", "error", err) + } + path := strings.Split(currentPath, "/taiko-client") + if len(path) == 0 { + log.Debug("Not a taiko-client repo") + } + if godotenv.Load(fmt.Sprintf("%s/taiko-client/integration_test/.env", path[0])) != nil { + log.Debug("Failed to load test env", "current path", currentPath, "error", err) + } +} + +// RandUint64 returns a random uint64 number. +func RandUint64(max *big.Int) uint64 { + if max == nil { + max = new(big.Int) + max.SetUint64(math.MaxUint64) + } + num, _ := rand.Int(rand.Reader, max) + + return num.Uint64() +} + +// RandUint32 returns a random uint32 number. +func RandUint32(max *big.Int) uint32 { + if max == nil { + max = new(big.Int) + max.SetUint64(math.MaxUint32) + } + num, _ := rand.Int(rand.Reader, max) + return uint32(num.Uint64()) +} + +// IsNil checks if the interface is empty. +func IsNil(i interface{}) bool { + return reflect2.IsNil(i) +} + +// Min return the minimum value of two integers. +func Min[T constraints.Integer](a, b T) T { + if a < b { + return a + } + return b +} + +// Compress compresses the given txList bytes using zlib. +func Compress(txList []byte) ([]byte, error) { + var b bytes.Buffer + w := zlib.NewWriter(&b) + defer w.Close() + + if _, err := w.Write(txList); err != nil { + return nil, err + } + + if err := w.Close(); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// Decompress decompresses the given txList bytes using zlib. +func Decompress(compressedTxList []byte) ([]byte, error) { + r, err := zlib.NewReader(bytes.NewBuffer(compressedTxList)) + if err != nil { + return nil, err + } + defer r.Close() + + b, err := io.ReadAll(r) + if err != nil { + if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { + return nil, err + } + } + + return b, nil +} + +// GWeiToWei converts gwei value to wei value. +func GWeiToWei(gwei float64) (*big.Int, error) { + if math.IsNaN(gwei) || math.IsInf(gwei, 0) { + return nil, fmt.Errorf("invalid gwei value: %v", gwei) + } + + // convert float GWei value into integer Wei value + wei, _ := new(big.Float).Mul( + big.NewFloat(gwei), + big.NewFloat(params.GWei)). + Int(nil) + + if wei.Cmp(abi.MaxUint256) == 1 { + return nil, errors.New("gwei value larger than max uint256") + } + + return wei, nil +} + +// EtherToWei converts ether value to wei value. +func EtherToWei(ether float64) (*big.Int, error) { + if math.IsNaN(ether) || math.IsInf(ether, 0) { + return nil, fmt.Errorf("invalid ether value: %v", ether) + } + + // convert float GWei value into integer Wei value + wei, _ := new(big.Float).Mul( + big.NewFloat(ether), + big.NewFloat(params.Ether)). + Int(nil) + + if wei.Cmp(abi.MaxUint256) == 1 { + return nil, errors.New("ether value larger than max uint256") + } + + return wei, nil +} + +// WeiToEther converts wei value to ether value. +func WeiToEther(wei *big.Int) *big.Float { + return new(big.Float).Quo(new(big.Float).SetInt(wei), new(big.Float).SetInt(big.NewInt(params.Ether))) +} + +// WeiToGWei converts wei value to gwei value. +func WeiToGWei(wei *big.Int) *big.Float { + return new(big.Float).Quo(new(big.Float).SetInt(wei), new(big.Float).SetInt(big.NewInt(params.GWei))) +} diff --git a/packages/taiko-client/pkg/utils/utils_test.go b/packages/taiko-client/pkg/utils/utils_test.go new file mode 100644 index 00000000000..6c3da5f33de --- /dev/null +++ b/packages/taiko-client/pkg/utils/utils_test.go @@ -0,0 +1,49 @@ +package utils_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + + "github.com/taikoxyz/taiko-mono/packages/taiko-client/internal/testutils" + "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/utils" +) + +func TestEncodeDecodeBytes(t *testing.T) { + b := testutils.RandomBytes(1024) + + compressed, err := utils.Compress(b) + require.Nil(t, err) + require.NotEmpty(t, compressed) + + decompressed, err := utils.Decompress(compressed) + require.Nil(t, err) + + require.Equal(t, b, decompressed) +} + +func TestGWeiToWei(t *testing.T) { + wei, err := utils.GWeiToWei(1.0) + require.Nil(t, err) + + require.Equal(t, big.NewInt(params.GWei), wei) +} + +func TestEtherToWei(t *testing.T) { + wei, err := utils.EtherToWei(1.0) + require.Nil(t, err) + + require.Equal(t, big.NewInt(params.Ether), wei) +} + +func TestWeiToEther(t *testing.T) { + eth := utils.WeiToEther(big.NewInt(params.Ether)) + require.Equal(t, new(big.Float).SetUint64(1), eth) +} + +func TestWeiToGWei(t *testing.T) { + gwei := utils.WeiToGWei(big.NewInt(params.GWei)) + require.Equal(t, new(big.Float).SetUint64(1), gwei) +} diff --git a/packages/taiko-client/scripts/gen_swagger_json.sh b/packages/taiko-client/scripts/gen_swagger_json.sh index 59dce311dbd..6d3a297c984 100755 --- a/packages/taiko-client/scripts/gen_swagger_json.sh +++ b/packages/taiko-client/scripts/gen_swagger_json.sh @@ -1,3 +1,3 @@ #/bin/sh -swag init -g api.go -d prover/server --pd +swag init -g server.go -d driver/soft_blocks --pd