Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 130 additions & 27 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,33 @@
path: |
output/libopenvm.a
output/libopenvm.bflat.manifest
output/openvm_entry.o
output/openvm.ld
output/openvm.bflat.manifest
if-no-files-found: error

platform-tests:
# platform/entry.S, the bflat entry point of the guest: _start for the
# OpenVM entry, and the 64-bit memcpy/memmove/memset, cross-compiled and run under
# qemu-user. The tests came here from bflat-riscv64 with the entry point.
name: Platform unit tests
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Install the RISC-V toolchain and qemu-user
run: |
sudo apt-get update
sudo apt-get install -y gcc-riscv64-linux-gnu qemu-user-static

- name: Build the platform files
run: platform/build.sh

- name: Run the unit tests
run: tests/platform/run.sh

link-guest:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
# The surface check in build-libopenvm proves the symbols exist. This proves
# they LINK: a real bflat guest, built for --libc openvm against the real
# linker script, calling all 21 entry points through direct P/Invoke. It is
Expand Down Expand Up @@ -124,20 +148,17 @@

- name: Build bflat and its native modules
working-directory: bflat-riscv64
# The modules leg is what produces lib/linux/riscv64/openvm/{script.ld,
# entrypoint.o} - the memory map this test is here to link against.
# The modules leg produces the lib/modules that the OpenVM platform
# requires. The platform itself - entry point and memory map - is in
# this repository's output/ (platform/build.sh), next to libopenvm,
# and --extlib reads both manifests from there.
run: |
./build.sh modules riscv64 perf 10
./build.sh bflat riscv64 perf 10

- name: Link the guest against libopenvm
run: |
set -e
# The layout copy of the linker script is what the link actually
# uses; print both so a stale layout is visible rather than
# mystifying.
sha256sum bflat-riscv64/src/bflat/bin/Debug/net10.0/lib/linux/riscv64/openvm/script.ld \
bflat-riscv64/src/bflat/modules/zkvm_openvm/script.ld
cp bflat-openvm/bflat-manifest.json bflat-openvm/output/libopenvm.bflat.manifest
bflat-riscv64/src/bflat/bin/Debug/net10.0/bflat build \
bflat-openvm/tests/integration/guest.cs \
Expand Down Expand Up @@ -257,10 +278,6 @@
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
NUGET_URL: ${{ inputs.publish == 'Staging' && 'https://int.nugettest.org' || 'https://www.nuget.org' }}
NUGET_SOURCE: ${{ inputs.publish == 'Staging' && 'https://apiint.nugettest.org' || 'https://api.nuget.org' }}/v3/index.json
outputs:
version: ${{ steps.version.outputs.version }}
steps:
Expand All @@ -278,8 +295,8 @@
- name: Move artifacts into the package project
run: |
mkdir -p src/runtime/runtimes/linux-riscv64/native
mv libopenvm.a src/runtime/runtimes/linux-riscv64/native/
mv libopenvm.bflat.manifest src/runtime/
mv libopenvm.a libopenvm.bflat.manifest openvm_entry.o openvm.ld openvm.bflat.manifest \
src/runtime/runtimes/linux-riscv64/native/

- name: Detect version
id: version
Expand All @@ -298,24 +315,108 @@
echo "Detected version $version"
echo "version=$version" >> $GITHUB_OUTPUT

- name: Inject openvm_ref and package_version into the manifest
# A run that is run again keeps its run number, and with it the
# version. Its publish jobs would replace the files of the release
# that has the version already. Start a new run instead.
- name: Check that the release does not exist
if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
if gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1 ; then
echo "::error::the release v$VERSION exists already: start a new run"
exit 1
fi

- name: Inject openvm_ref and the package version into the manifests
run: |
VERSION="${{ steps.version.outputs.version }}"

MANIFEST=src/runtime/libopenvm.bflat.manifest
jq --arg ref "$OPENVM_REF" --arg version "$VERSION" \
'. + {openvm_ref: $ref, package_version: $version}' \
"$MANIFEST" > "$MANIFEST.tmp"
mv "$MANIFEST.tmp" "$MANIFEST"
for MANIFEST in src/runtime/runtimes/linux-riscv64/native/*.bflat.manifest ; do
jq --arg ref "$OPENVM_REF" --arg version "$VERSION" \
'.version = $version | .metadata.openvm_ref = $ref' \
"$MANIFEST" > "$MANIFEST.tmp"
mv "$MANIFEST.tmp" "$MANIFEST"
done

- name: Pack
working-directory: src/runtime
run: |
dotnet pack Nethermind.OpenVM.Runtime.csproj -c release \
-p:Version=${{ steps.version.outputs.version }}

# The lists of the package entries, for the sign job (docs/signing.md
# in bflat-riscv64). This job has no key: the sign job signs the lists,
# and the publish job adds the signatures to the package.
- name: List the package entries
working-directory: src/runtime
run: python3 ../../scripts/sign_package.py list-nupkg ../../signature ./artifacts/package/release/*.nupkg

- name: Upload nupkg artifact
uses: actions/upload-artifact@v7
with:
name: nupkg-unsigned
path: src/runtime/artifacts/package/release/*.nupkg
if-no-files-found: error

- name: Upload the lists
uses: actions/upload-artifact@v7
with:
name: bflat-signature-lists
path: signature/*.sha256
if-no-files-found: error

# The private key is only in the sign job, which runs no build
# (docs/signing.md in bflat-riscv64).
sign:
name: Sign the package
if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No'
needs: build-nupkg
uses: NethermindEth/bflat-riscv64/.github/workflows/sign.yml@master
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
with:
lists: bflat-signature-lists
secrets:
BFLAT_SIGNING_KEY: ${{ secrets.BFLAT_SIGNING_KEY }}

publish-nupkg:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
name: Publish NuGet package
if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No'
needs: sign
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
NUGET_URL: ${{ inputs.publish == 'Staging' && 'https://int.nugettest.org' || 'https://www.nuget.org' }}
NUGET_SOURCE: ${{ inputs.publish == 'Staging' && 'https://apiint.nugettest.org' || 'https://api.nuget.org' }}/v3/index.json
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up .NET
uses: actions/setup-dotnet@v6

- name: Download nupkg artifact
uses: actions/download-artifact@v8
with:
name: nupkg-unsigned
path: nupkg

- name: Download the signatures
uses: actions/download-artifact@v8
with:
name: bflat-signatures
path: signature

# Each package must agree with the list that was signed, and the
# signature must be valid for the public key, before the push.
- name: Add the signatures
env:
BFLAT_SIGNING_PUBLIC_KEY: ${{ vars.BFLAT_SIGNING_PUBLIC_KEY }}
run: python3 scripts/sign_package.py embed signature nupkg/*.nupkg

- name: NuGet login
if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No'
uses: nuget/login@v1
id: login
with:
Expand All @@ -324,24 +425,21 @@
token-service-url: ${{ env.NUGET_URL }}/api/v2/token

- name: Publish NuGet package
if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No'
working-directory: src/runtime
run: |
dotnet nuget push ./artifacts/package/release/*.nupkg \
-k "${{ steps.login.outputs.NUGET_API_KEY }}" -s ${{ env.NUGET_SOURCE }} \
--skip-duplicate
dotnet nuget push nupkg/*.nupkg \
-k "${{ steps.login.outputs.NUGET_API_KEY }}" -s ${{ env.NUGET_SOURCE }}

- name: Upload nupkg artifact
uses: actions/upload-artifact@v7
with:
name: nupkg
path: src/runtime/artifacts/package/release/*.nupkg
path: nupkg/*.nupkg
if-no-files-found: error

publish-nupkg-github:
name: Publish to GitHub
if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No'
needs: build-nupkg
needs: [build-nupkg, publish-nupkg]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -361,12 +459,17 @@
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
# The commit that was built; the default is the head of the default branch.
target_commitish: ${{ github.sha }}
tag_name: v${{ needs.build-nupkg.outputs.version }}
name: Nethermind.OpenVM.Runtime ${{ needs.build-nupkg.outputs.version }}
files: |
nupkg/*.nupkg
libopenvm/libopenvm.a
libopenvm/libopenvm.bflat.manifest
libopenvm/openvm_entry.o
libopenvm/openvm.ld
libopenvm/openvm.bflat.manifest
body: |
OpenVM zkVM accelerators for bflat guests.

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ src/runtime/*.bflat.manifest
artifacts/
bin/
obj/

# Platform unit test builds (tests/platform/run.sh)
tests/platform/out/
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,46 @@ Dependencies are pinned to `develop-v2.1.0`, OpenVM's live RV64 line — not
`develop-v2.1.0-rv64` (an older snapshot of the same work) and not `main`,
which is still RV32 and rejects a 64-bit ELF outright.

## Platform

This package also defines the OpenVM target for bflat: `--libc openvm` selects
the platform that `platform/` describes, so bflat itself knows nothing about
OpenVM. `platform/build.sh` builds it into `output/` (`openvm_entry.o`, `openvm.ld` and `openvm.bflat.manifest`):

- `entry.S` is the guest's `_start` and the OpenVM halt protocol, plus the 64-bit `memcpy`/`memmove`/`memset`, the TERMINATE and PrintStr intrinsics.
- `entry.S` also implements the platform interface that bflat's modules call
(bflat itself has no code for any one zkVM): `zkvm_platform_exit` (`TERMINATE`) and `zkvm_platform_write` (`PrintStr`).
- the linker script is the OpenVM memory map;
- the platform manifest gives the instruction set, the ABI, the RyuJIT
options, the defines and the post-link steps, and lists the bflat modules
that the guest needs (format: `docs/manifest.md` in bflat-riscv64).

`--extlib` with a `.bflat.manifest` path reads every `*.bflat.manifest` in that
directory, so the platform and the bindings come together. A guest that does
not use the accelerators can be built with the platform files alone.
`tests/platform/run.sh` runs the unit tests of the entry point under qemu-user.

## Usage

```console
$ bflat build app.cs --os linux --arch riscv64 --libc openvm \
--extlib path/to/libopenvm.bflat.manifest
```

## Signature

The release workflow signs the `.nupkg` before it publishes it. The build
job lists the package entries (`scripts/sign_package.py list-nupkg`). The
sign job, the reusable workflow `sign.yml` of bflat-riscv64, signs the lists
with the GitHub Actions secret `BFLAT_SIGNING_KEY`; it is the only job that
has the key, and it runs no build. The publish job adds the signatures
(`scripts/sign_package.py embed`) and checks them with the variable
`BFLAT_SIGNING_PUBLIC_KEY`. bflat refuses a package with a bad signature, and an
unsigned package unless `--allow-unsigned` is given. A local
`.bflat.manifest` is not checked. See `docs/signing.md` in bflat-riscv64.
`scripts/sign_package.py` is a copy of the script in bflat-riscv64; keep it
the same.

## Building

```console
Expand Down Expand Up @@ -113,7 +146,7 @@ curves drag in OpenVM's guest runtime (`_start`, `mem*`, `sys_alloc_aligned`),
and `-Z build-std` compiles `compiler_builtins` for a target with no libm, so
it exports the entire C math surface as well. Every one of those collides with
the guest: musl owns the math surface and `mem*`,
`modules/zkvm_openvm/module.S` owns `_start`, and bflat's `rust_sys` module
`platform/entry.S` owns `_start`, and bflat's `rust_sys` module
wraps `sys_alloc_aligned` so OpenVM's bump allocator draws from pal's heap
instead of opening a second one.

Expand Down
15 changes: 9 additions & 6 deletions bflat-manifest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/NethermindEth/bflat-riscv64/master/src/bflat/bflat-manifest.schema.json",
"schema": 2,
"name": "libopenvm",
"package_version": "1.0.0",
"builds": [
"version": "1.0.0",
"description": "OpenVM accelerators (libopenvm) for bflat guests",
"when": {"platform": "openvm"},
"objects": [
{
"arch": "riscv64",
"os": "linux",
"libc": "openvm",
"static_lib": "libopenvm.a"
"file": "libopenvm.a",
"group": "pre-entry",
"note": "In front of the linker script, where bflat linked --extlib static libraries before schema 2."
}
]
}
5 changes: 4 additions & 1 deletion build/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ cp "${RUST_LIB}" "${OUTPUT_DIR}/libopenvm.a"
# symbols) plus bcmp.
#
# Every one of those collides with the guest: musl defines the math surface and
# mem*, modules/zkvm_openvm/module.S defines _start, and bflat's rust_sys
# mem*, platform/entry.S defines _start, and bflat's rust_sys
# module wraps sys_alloc_aligned so OpenVM's bump allocator draws from pal's
# heap rather than opening a second one. A duplicate-symbol error is the good
# outcome; the bad one is this archive silently winning over the math surface
Expand Down Expand Up @@ -173,6 +173,9 @@ echo "OpenVM custom-1 instructions: ${C1}"

cp "${ROOT_DIR}/bflat-manifest.json" "${OUTPUT_DIR}/libopenvm.bflat.manifest"

# The entry point, the memory map and the platform manifest (platform/).
AS="${AS}" "${ROOT_DIR}/platform/build.sh" "${OUTPUT_DIR}" || fail "platform build failed"

echo "Build completed"
echo "Output: ${OUTPUT_DIR}/libopenvm.a"
echo " ${OUTPUT_DIR}/libopenvm.bflat.manifest"
34 changes: 34 additions & 0 deletions platform/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
# Builds the OpenVM platform for bflat guests into the output directory (the
# first argument; default: output/ in the repository):
#
# - openvm_entry.o: entry.S assembled; the _start of the guest and the halt protocol
# - openvm.ld: the memory map (linker script)
# - openvm.bflat.manifest: the platform manifest (docs/manifest.md in bflat-riscv64)
#
# It needs only binutils for riscv64, not the bindings library, so bflat's
# own CI uses it to link guests for this platform. build/build.sh calls it
# for the package.
set -e

fail() { echo "$@" >&2; exit 1; }

PLATFORM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_DIR="${1:-$(cd "${PLATFORM_DIR}/.." && pwd)/output}"
AS="${AS:-riscv64-linux-gnu-as}"

command -v "${AS}" >/dev/null 2>&1 || fail "${AS} not found (apt install binutils-riscv64-linux-gnu)"
mkdir -p "${OUTPUT_DIR}"

# rv64ima and the soft-float ABI: the flags bflat used for its entry modules.
"${AS}" --march=rv64ima --mabi=lp64 "${PLATFORM_DIR}/entry.S" -o "${OUTPUT_DIR}/openvm_entry.o" \
|| fail "failed to assemble entry.S"
# Clear the float-ABI marker (e_flags, offset 0x30) so that ld.lld accepts the
# object against the guest's soft-float crt1.o.
printf '\x00' | dd of="${OUTPUT_DIR}/openvm_entry.o" bs=1 seek=48 count=1 conv=notrunc status=none

for f in openvm.ld openvm.bflat.manifest ; do
cp "${PLATFORM_DIR}/${f}" "${OUTPUT_DIR}/${f}" || fail "failed to copy ${f}"
done

echo "Platform: ${OUTPUT_DIR}/openvm_entry.o openvm.ld openvm.bflat.manifest"
Loading
Loading