From d13ad01f428876f518d6a1b4e394c922f2310107 Mon Sep 17 00:00:00 2001 From: lihe Date: Wed, 15 Jul 2026 17:45:20 +0800 Subject: [PATCH 1/6] build(stylus): add cdylib crate-type so leafage can dlopen libstylus leafage-evm loads the stylus FFI at runtime via libloading (LEAFAGE_ARB_STYLUS_LIB), which needs a shared object; the crate previously built only lib + staticlib. The prover_ffi re-export already funnels free_rust_bytes into the linked output, so the cdylib exports the full stylus_* + free_rust_bytes symbol set with no further changes. --- crates/stylus/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stylus/Cargo.toml b/crates/stylus/Cargo.toml index e91a9dbf7..df10164fd 100644 --- a/crates/stylus/Cargo.toml +++ b/crates/stylus/Cargo.toml @@ -45,4 +45,4 @@ rayon = ["prover/rayon"] cc_brotli = ["brotli/cc_brotli"] [lib] -crate-type = ["lib", "staticlib"] +crate-type = ["lib", "staticlib", "cdylib"] From d67b07aa54e2fec4789666aac33169cc98184c66 Mon Sep 17 00:00:00 2001 From: lihe Date: Tue, 21 Jul 2026 15:14:00 +0800 Subject: [PATCH 2/6] build(stylus): publish libstylus.so as a COPY-only image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leafage-evm dlopens libstylus.so to execute Stylus contracts, but nothing built it for Linux and its image shipped without it, so every Stylus call fell back to a revert instead of running the WASM. Adds a Dockerfile that builds the library and a workflow that publishes it to public ECR as `libstylus:` (amd64 + arm64). The image is a scratch layer holding just the .so — consumers do `COPY --from`, it is not meant to run. Two things worth keeping in view: - The builder sits on bookworm (glibc 2.36) because leafage-evm runs on ubuntu:24.04 (glibc 2.39), and a library built against an older glibc loads on a newer one but not the reverse. If that runtime base moves, this one has to stay at or below it. - The build asserts the five symbols leafage resolves at runtime (stylus_activate/compile/call/target_set, free_rust_bytes), so ABI drift fails the build instead of failing at dlopen time in production. Only the Rust workspace is copied into the build context; the Go tree is not involved. --- .github/workflows/build-libstylus.yml | 108 ++++++++++++++++++++++++++ Dockerfile.libstylus | 43 ++++++++++ 2 files changed, 151 insertions(+) create mode 100644 .github/workflows/build-libstylus.yml create mode 100644 Dockerfile.libstylus diff --git a/.github/workflows/build-libstylus.yml b/.github/workflows/build-libstylus.yml new file mode 100644 index 000000000..09257a31b --- /dev/null +++ b/.github/workflows/build-libstylus.yml @@ -0,0 +1,108 @@ +name: "Build libstylus" + +# Publishes libstylus.so as a COPY-only image so leafage-evm can pull the native +# Stylus runtime into its own image at a pinned revision. Kept separate from the +# nitro node build: it shares only the Rust workspace, takes a different base, +# and is released on its own cadence. +on: + workflow_dispatch: + pull_request: + branches: + - 'main' + paths: + - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'rust-toolchain.toml' + - 'Dockerfile.libstylus' + - '.github/workflows/build-libstylus.yml' + +env: + AWS_REGION: us-east-1 + REGISTRY: public.ecr.aws/b2h7a5c4/chaintable + IMAGE: libstylus + DOCKERFILE: Dockerfile.libstylus + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + arch: amd64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.runner }} + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + clean: true + # crates/tools/wasmer and brotli are submodules the build links against. + submodules: recursive + + - name: Set env + run: | + echo "REVISION=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v5 + with: + file: ${{ env.DOCKERFILE }} + context: . + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ matrix.arch }}-${{ env.REVISION }} + provenance: false + + merge-manifest: + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set env + run: | + echo "REVISION=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Merge manifest + run: | + docker buildx imagetools create \ + --tag ${REGISTRY}/${IMAGE}:${REVISION} \ + ${REGISTRY}/${IMAGE}:amd64-${REVISION} \ + ${REGISTRY}/${IMAGE}:arm64-${REVISION} diff --git a/Dockerfile.libstylus b/Dockerfile.libstylus new file mode 100644 index 000000000..5e298aa9d --- /dev/null +++ b/Dockerfile.libstylus @@ -0,0 +1,43 @@ +# libstylus.so — the native Stylus runtime that leafage-evm dlopens to execute +# WASM contracts (via LEAFAGE_ARB_STYLUS_LIB). The result is a COPY-only image: +# it has no entrypoint and is never meant to run. Consumers pull the library out +# with `COPY --from=: /libstylus.so ...`. +# +# The builder is pinned to bookworm (glibc 2.36) deliberately: leafage-evm runs +# on ubuntu:24.04 (glibc 2.39), and a library built against an older glibc loads +# on a newer one but not the other way round. If leafage-evm's runtime base ever +# moves, this base must stay at or below it. +ARG RUST_VERSION=1.93 + +FROM rust:${RUST_VERSION}-bookworm AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + cmake \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src + +# Only what the Rust workspace needs — every workspace member lives under +# crates/, and the Go tree plays no part in this build. +COPY rust-toolchain.toml Cargo.toml Cargo.lock ./ +COPY scripts/build-brotli.sh scripts/ +COPY brotli brotli +COPY crates crates + +# crates/brotli links the C static libs, so they have to be built first; +# without this the stylus build fails on `brotlienc-static not found`. +RUN ./scripts/build-brotli.sh -l + +RUN cargo build --release --lib -p stylus + +# The symbols leafage resolves at runtime. Checking them here turns an ABI drift +# into a failed build rather than a failed dlopen in production. +RUN for sym in stylus_activate stylus_compile stylus_call stylus_target_set free_rust_bytes; do \ + nm -D target/release/libstylus.so | grep -q " ${sym}\$" \ + || { echo "libstylus.so is missing exported symbol: ${sym}" >&2; exit 1; }; \ + done + +FROM scratch +COPY --from=builder /src/target/release/libstylus.so /libstylus.so From 1c6d150f6868a6793103a38dcec3806017af446e Mon Sep 17 00:00:00 2001 From: lihe Date: Tue, 21 Jul 2026 15:25:09 +0800 Subject: [PATCH 3/6] build(stylus): tag libstylus by the source commit, not the PR merge commit --- .github/workflows/build-libstylus.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-libstylus.yml b/.github/workflows/build-libstylus.yml index 09257a31b..4231f08b4 100644 --- a/.github/workflows/build-libstylus.yml +++ b/.github/workflows/build-libstylus.yml @@ -42,6 +42,10 @@ jobs: uses: actions/checkout@v4 with: clean: true + # Tag by the source commit rather than the PR's merge commit, so the + # published revision maps to something that exists in the history. + # Empty on workflow_dispatch, where the triggering ref is used. + ref: ${{ github.event.pull_request.head.sha }} # crates/tools/wasmer and brotli are submodules the build links against. submodules: recursive @@ -81,6 +85,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set env run: | From f7c0ab5ce6e94fa33dafd3b2d3277572f48d7473 Mon Sep 17 00:00:00 2001 From: lihe Date: Tue, 21 Jul 2026 15:35:39 +0800 Subject: [PATCH 4/6] build(stylus): strip debuginfo from libstylus.so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release profile builds with debuginfo, leaving the library at ~237MB. That would land whole in leafage-evm's image for no benefit — nothing consumes the DWARF sections there. --strip-debug keeps the dynamic symbol table, which is all leafage resolves against, and the symbol assertion now runs after the strip so it covers what actually ships. --- Dockerfile.libstylus | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile.libstylus b/Dockerfile.libstylus index 5e298aa9d..c1f3bd010 100644 --- a/Dockerfile.libstylus +++ b/Dockerfile.libstylus @@ -32,8 +32,14 @@ RUN ./scripts/build-brotli.sh -l RUN cargo build --release --lib -p stylus -# The symbols leafage resolves at runtime. Checking them here turns an ABI drift -# into a failed build rather than a failed dlopen in production. +# The release profile carries debuginfo, which leaves the library around 237MB +# and would land whole in every consumer's image. --strip-debug drops only the +# DWARF sections; the dynamic symbol table leafage resolves against survives. +RUN strip --strip-debug target/release/libstylus.so + +# The symbols leafage resolves at runtime, checked *after* stripping so the +# assertion covers what actually ships. Turns ABI drift into a failed build +# rather than a failed dlopen in production. RUN for sym in stylus_activate stylus_compile stylus_call stylus_target_set free_rust_bytes; do \ nm -D target/release/libstylus.so | grep -q " ${sym}\$" \ || { echo "libstylus.so is missing exported symbol: ${sym}" >&2; exit 1; }; \ From 782878f1e359b294f3e28118a59c989512bfb763 Mon Sep 17 00:00:00 2001 From: lihe Date: Tue, 21 Jul 2026 23:31:33 +0800 Subject: [PATCH 5/6] ci(stylus): publish libstylus only on releases --- .github/workflows/build-libstylus.yml | 50 ++++++++------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build-libstylus.yml b/.github/workflows/build-libstylus.yml index 4231f08b4..4a25c01d1 100644 --- a/.github/workflows/build-libstylus.yml +++ b/.github/workflows/build-libstylus.yml @@ -1,27 +1,22 @@ name: "Build libstylus" -# Publishes libstylus.so as a COPY-only image so leafage-evm can pull the native -# Stylus runtime into its own image at a pinned revision. Kept separate from the -# nitro node build: it shares only the Rust workspace, takes a different base, -# and is released on its own cadence. +# Publishes libstylus.so as a COPY-only image for each GitHub Release so +# leafage-evm can pull the native Stylus runtime at the same source tag. on: - workflow_dispatch: - pull_request: - branches: - - 'main' - paths: - - 'crates/**' - - 'Cargo.toml' - - 'Cargo.lock' - - 'rust-toolchain.toml' - - 'Dockerfile.libstylus' - - '.github/workflows/build-libstylus.yml' + release: + types: + - published + +concurrency: + group: ${{ github.workflow }}-${{ github.event.release.tag_name }} + cancel-in-progress: false env: AWS_REGION: us-east-1 REGISTRY: public.ecr.aws/b2h7a5c4/chaintable IMAGE: libstylus DOCKERFILE: Dockerfile.libstylus + REVISION: ${{ github.event.release.tag_name }} jobs: build: @@ -42,17 +37,10 @@ jobs: uses: actions/checkout@v4 with: clean: true - # Tag by the source commit rather than the PR's merge commit, so the - # published revision maps to something that exists in the history. - # Empty on workflow_dispatch, where the triggering ref is used. - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.release.tag_name }} # crates/tools/wasmer and brotli are submodules the build links against. submodules: recursive - - name: Set env - run: | - echo "REVISION=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -80,18 +68,8 @@ jobs: runs-on: ubuntu-latest needs: build permissions: - contents: read id-token: write steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Set env - run: | - echo "REVISION=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -109,6 +87,6 @@ jobs: - name: Merge manifest run: | docker buildx imagetools create \ - --tag ${REGISTRY}/${IMAGE}:${REVISION} \ - ${REGISTRY}/${IMAGE}:amd64-${REVISION} \ - ${REGISTRY}/${IMAGE}:arm64-${REVISION} + --tag "${REGISTRY}/${IMAGE}:${REVISION}" \ + "${REGISTRY}/${IMAGE}:amd64-${REVISION}" \ + "${REGISTRY}/${IMAGE}:arm64-${REVISION}" From 65af5eafdfa4811e94d180ce0284f61f177e4e87 Mon Sep 17 00:00:00 2001 From: lihe Date: Thu, 23 Jul 2026 11:34:31 +0800 Subject: [PATCH 6/6] ci(stylus): publish latest libstylus manifest --- .github/workflows/build-libstylus.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-libstylus.yml b/.github/workflows/build-libstylus.yml index 4a25c01d1..443a92934 100644 --- a/.github/workflows/build-libstylus.yml +++ b/.github/workflows/build-libstylus.yml @@ -1,7 +1,7 @@ name: "Build libstylus" # Publishes libstylus.so as a COPY-only image for each GitHub Release so -# leafage-evm can pull the native Stylus runtime at the same source tag. +# leafage-evm can pull the native Stylus runtime by release tag or latest. on: release: types: @@ -88,5 +88,6 @@ jobs: run: | docker buildx imagetools create \ --tag "${REGISTRY}/${IMAGE}:${REVISION}" \ + --tag "${REGISTRY}/${IMAGE}:latest" \ "${REGISTRY}/${IMAGE}:amd64-${REVISION}" \ "${REGISTRY}/${IMAGE}:arm64-${REVISION}"