diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..cc8300c56f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +build-static-ffi = "build --frozen --profile maxperf --package firewood-ffi --features ethhash,logger" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 61c0bfc206..0f613b81fe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,8 @@ updates: time: "05:00" timezone: "America/Los_Angeles" open-pull-requests-limit: 0 # Disable non-security version updates + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 0 # Disable non-security version updates diff --git a/.github/workflows/attach-static-libs.yaml b/.github/workflows/attach-static-libs.yaml index f09983f953..0e514e532c 100644 --- a/.github/workflows/attach-static-libs.yaml +++ b/.github/workflows/attach-static-libs.yaml @@ -63,7 +63,7 @@ jobs: run: cargo fetch --locked --verbose - name: Build for ${{ matrix.target }} - run: cargo build --frozen --profile maxperf --features ethhash,logger --target ${{ matrix.target }} -p firewood-ffi + run: cargo build-static-ffi --target ${{ matrix.target }} - name: Upload binary uses: actions/upload-artifact@v4 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3274a7a81f..f7c74b6bdf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -244,6 +244,25 @@ jobs: # cgocheck2 is expensive but provides complete pointer checks run: GOEXPERIMENT=cgocheck2 TEST_FIREWOOD_HASH_MODE=firewood go test -race ./... + ffi-nix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@786fff0690178f1234e4e1fe9b536e94f5433196 #v20 + - uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 #v13 + - name: Test build equivalency between Nix and Cargo + run: bash -x ./ffi/test-build-equivalency.sh + - name: Test Go FFI bindings + working-directory: ffi + # - cgocheck2 is expensive but provides complete pointer checks + # - use hash mode ethhash since the flake builds with `--features ethhash,logger` + # - run golang outside a nix shell to validate viability without the env setup performed by a nix shell + run: | + GOLANG="nix run $PWD#go" + cd result/ffi + GOEXPERIMENT=cgocheck2 TEST_FIREWOOD_HASH_MODE=ethhash ${GOLANG} test ./... + shell: bash + firewood-ethhash-differential-fuzz: needs: build runs-on: ubuntu-latest diff --git a/ffi/.gitignore b/ffi/.gitignore index 4e24ab4c75..ecc17f0fc9 100644 --- a/ffi/.gitignore +++ b/ffi/.gitignore @@ -1,2 +1,5 @@ dbtest _obj + +# Nix output +result diff --git a/ffi/flake.lock b/ffi/flake.lock new file mode 100644 index 0000000000..e95d44ffe9 --- /dev/null +++ b/ffi/flake.lock @@ -0,0 +1,146 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1758758545, + "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", + "owner": "ipetkov", + "repo": "crane", + "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "golang": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "dir": "nix/go", + "lastModified": 1760457933, + "narHash": "sha256-/OztRdmXd3cL6ycrzRYAgFkPTv9v5WWlcdtVlERaRKI=", + "owner": "ava-labs", + "repo": "avalanchego", + "rev": "edf67505fee7c95837c2220467de86fea0efc860", + "type": "github" + }, + "original": { + "dir": "nix/go", + "owner": "ava-labs", + "ref": "edf67505fee7c95837c2220467de86fea0efc860", + "repo": "avalanchego", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1759735786, + "narHash": "sha256-a0+h02lyP2KwSNrZz4wLJTu9ikujNsTWIC874Bv7IJ0=", + "rev": "20c4598c84a671783f741e02bf05cbfaf4907cff", + "revCount": 810859, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.810859%2Brev-20c4598c84a671783f741e02bf05cbfaf4907cff/0199bc43-02e2-7036-8e2c-e43f6d6b4ede/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2505.%2A.tar.gz" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1758589230, + "narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=", + "rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0", + "revCount": 810308, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.810308%2Brev-d1d883129b193f0b495d75c148c2c3a7d95789a0/01997816-a6f6-7040-8535-2ae74ed9bd44/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2505.%2A.tar.gz" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "golang": "golang", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1758854041, + "narHash": "sha256-kZ+24pbf4FiHlYlcvts64BhpxpHkPKIQXBmx1OmBAIo=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "02227ca8c229c968dbb5de95584cfb12b4313104", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/ffi/flake.nix b/ffi/flake.nix new file mode 100644 index 0000000000..e2853cc0c9 --- /dev/null +++ b/ffi/flake.nix @@ -0,0 +1,140 @@ +{ + # To test with arbitrary firewood versions (alternative to firewood-go-ethhash): + # - Install nix: https://github.com/DeterminateSystems/nix-installer?tab=readme-ov-file#install-nix + # - Clone firewood locally at desired version/commit + # - Build: `cd ffi && nix build` + # - In your Go project: `go mod edit -replace github.com/ava-labs/firewood-go-ethhash/ffi=/path/to/firewood/ffi/result/ffi` + + description = "Firewood FFI library and development environment"; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2505.*.tar.gz"; + rust-overlay.url = "github:oxalica/rust-overlay"; + crane.url = "github:ipetkov/crane"; + flake-utils.url = "github:numtide/flake-utils"; + golang.url = "github:ava-labs/avalanchego?dir=nix/go&ref=f10757d594eedf0f016bc1400739788c542f005f"; + }; + + outputs = { self, nixpkgs, rust-overlay, crane, flake-utils, golang }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + inherit (pkgs) lib; + + go = golang.packages.${system}.default; + + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rustfmt" "clippy" ]; + }; + + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + # Extract crate info from Cargo.toml files + ffiCargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); + workspaceCargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml); + + src = lib.cleanSourceWith { + src = craneLib.path ./..; + filter = path: type: + (lib.hasSuffix "\.md" path) || + (lib.hasSuffix "\.go" path) || + (lib.hasSuffix "go.mod" path) || + (lib.hasSuffix "go.sum" path) || + (lib.hasSuffix "firewood.h" path) || + (craneLib.filterCargoSources path type); + }; + + commonArgs = { + inherit src; + strictDeps = true; + dontStrip = true; + + # Build only the firewood-ffi crate + pname = ffiCargoToml.package.name; + version = workspaceCargoToml.workspace.package.version; + + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + + # Force sequential build of vendored jemalloc to avoid race conditions + # that cause non-deterministic symbol generation on x86_64 + # MAKEFLAGS only affects make invocations (jemalloc), not cargo parallelism + # See: https://github.com/NixOS/nixpkgs/issues/380852 + MAKEFLAGS = "-j1"; + } // lib.optionalAttrs pkgs.stdenv.isDarwin { + # Set macOS deployment target for Darwin builds + MACOSX_DEPLOYMENT_TARGET = "13.0"; + }; + + cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { + # Use cargo alias defined in .cargo/config.toml + cargoBuildCommand = "cargo build-static-ffi"; + }); + + firewood-ffi = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + # Use cargo alias defined in .cargo/config.toml + cargoBuildCommand = "cargo build-static-ffi"; + + # Disable tests - we only need to build the static library + doCheck = false; + + # Install the static library and header + postInstall = '' + # Create a package structure compatible with FIREWOOD_LD_MODE=STATIC_LIBS + mkdir -p $out/ffi + cp -R ./ffi/* $out/ffi/ + mkdir -p $out/ffi/libs/${pkgs.stdenv.hostPlatform.config} + cp target/maxperf/libfirewood_ffi.a $out/ffi/libs/${pkgs.stdenv.hostPlatform.config}/ + + # Run go generate to switch CGO directives to STATIC_LIBS mode + cd $out/ffi + HOME=$TMPDIR GOTOOLCHAIN=local FIREWOOD_LD_MODE=STATIC_LIBS ${go}/bin/go generate + ''; + + meta = with lib; { + description = "C FFI bindings for Firewood, an embedded key-value store"; + homepage = "https://github.com/ava-labs/firewood"; + license = { + fullName = "Ava Labs Ecosystem License 1.1"; + url = "https://github.com/ava-labs/firewood/blob/main/LICENSE.md"; + }; + platforms = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + }; + }); + in + { + packages = { + inherit firewood-ffi; + default = firewood-ffi; + }; + + apps.go = { + type = "app"; + program = "${go}/bin/go"; + }; + + devShells.default = craneLib.devShell { + inputsFrom = [ firewood-ffi ]; + + packages = with pkgs; [ + firewood-ffi + rustToolchain + go + ]; + + shellHook = '' + # Ensure golang bin is in the path + GOBIN="$(go env GOPATH)/bin" + if [[ ":$PATH:" != *":$GOBIN:"* ]]; then + export PATH="$GOBIN:$PATH" + fi + + # Force sequential build of vendored jemalloc for reproducibility + export MAKEFLAGS="-j1" + ''; + }; + }); +} diff --git a/ffi/go.mod b/ffi/go.mod index e3cfa3fc93..be46486ab6 100644 --- a/ffi/go.mod +++ b/ffi/go.mod @@ -2,6 +2,11 @@ module github.com/ava-labs/firewood/ffi go 1.24 +// Changes to the toolchain version should be replicated in: +// - ffi/go.mod (here) +// - ffi/flake.nix (update golang.url to a version of avalanchego's nix/go/flake.nix that uses the desired version) +// - ffi/tests/eth/go.mod +// - ffi/tests/firewood/go.mod toolchain go1.24.9 require ( diff --git a/ffi/test-build-equivalency.sh b/ffi/test-build-equivalency.sh new file mode 100755 index 0000000000..5fc4cc57b8 --- /dev/null +++ b/ffi/test-build-equivalency.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Always work from the repo root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +# Define paths to libraries (relative to repo root) +NIX_LIB="ffi/result/lib/libfirewood_ffi.a" # Default path for the nix build +CARGO_LIB="target/maxperf/libfirewood_ffi.a" + +# Create temporary directory and ensure cleanup on exit +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +echo "Building with cargo (using nix dev shell)..." +nix develop ./ffi#default --command bash -c "cargo fetch --locked --verbose && cargo build-static-ffi" + +echo "Building with nix..." +cd ffi && nix build .#firewood-ffi && cd .. + +echo "" +echo "=== File Size Comparison ===" +ls -lh "$CARGO_LIB" "$NIX_LIB" + +echo "" +echo "=== Symbol Count Comparison ===" +NIX_SYMBOLS=$(nm "$NIX_LIB" | wc -l) +CARGO_SYMBOLS=$(nm "$CARGO_LIB" | wc -l) +echo "Nix build: $NIX_SYMBOLS symbols" +echo "Cargo build: $CARGO_SYMBOLS symbols" +if [ "$NIX_SYMBOLS" -eq "$CARGO_SYMBOLS" ]; then + echo "✅ Symbol counts match" +else + echo "❌ Symbol counts differ" +fi + +echo "" +echo "=== Relocation Count Comparison ===" + +# Determine os-specific reloc config +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + RELOC_CMD="otool -rv" + RELOC_PATTERN='[A-Z_]+_RELOC_[A-Z0-9_]+' +else + # Linux + RELOC_CMD="readelf -r" + RELOC_PATTERN='R_[A-Z0-9_]+' +fi + +$RELOC_CMD "$NIX_LIB" > "$TMPDIR/nix-relocs.txt" +$RELOC_CMD "$CARGO_LIB" > "$TMPDIR/cargo-relocs.txt" + +NIX_RELOCS=$(wc -l < "$TMPDIR/nix-relocs.txt") +CARGO_RELOCS=$(wc -l < "$TMPDIR/cargo-relocs.txt") +echo "Nix build: $NIX_RELOCS relocation entries" +echo "Cargo build: $CARGO_RELOCS relocation entries" +if [ "$NIX_RELOCS" -eq "$CARGO_RELOCS" ]; then + echo "✅ Relocation counts match" +else + echo "❌ Relocation counts differ" +fi + +echo "" +echo "=== Relocation Type Comparison ===" + +# Use grep with -E for better portability (avoid -P which isn't available on macOS) +grep -Eo "$RELOC_PATTERN" "$TMPDIR/nix-relocs.txt" | sort | uniq -c > "$TMPDIR/nix-reloc-types.txt" +grep -Eo "$RELOC_PATTERN" "$TMPDIR/cargo-relocs.txt" | sort | uniq -c > "$TMPDIR/cargo-reloc-types.txt" + +if diff "$TMPDIR/nix-reloc-types.txt" "$TMPDIR/cargo-reloc-types.txt" > /dev/null; then + echo "✅ Relocation types match" +else + echo "❌ Relocation types differ" + diff "$TMPDIR/nix-reloc-types.txt" "$TMPDIR/cargo-reloc-types.txt" +fi + +echo "" +echo "=== Relocation Type Distribution ===" +cat "$TMPDIR/nix-reloc-types.txt" + +echo "" +echo "=== Summary ===" +if [ "$NIX_SYMBOLS" -eq "$CARGO_SYMBOLS" ] && [ "$NIX_RELOCS" -eq "$CARGO_RELOCS" ] && diff "$TMPDIR/nix-reloc-types.txt" "$TMPDIR/cargo-reloc-types.txt" > /dev/null; then + echo "✅ Builds are equivalent - both using maxperf profile" +else + echo "❌ Builds differ" + exit 1 +fi