logos-blockchain-circuits/CONTRIBUTING.md
2026-05-29 12:13:44 +02:00

6.4 KiB

Contributor's Guide

Development Setup

Prerequisites

Sys development

  • Rust — the pinned toolchain version is in rust-toolchain.toml and will be installed automatically by rustup.
  • Compiled circuit libraries (.a files and witness_generator.dat) — see rust/README.md for how to provide them.

Building circuits

  • llvm-objcopy — required for symbol isolation when building circuit static libraries. On macOS, install via brew install llvm (LLVM 20+ required).

Pre-Commit

pre-commit covers most of the lints required by CI. It's not mandatory — you can run checks however you like — but it's the easiest way to catch issues before pushing.

Installation

pre-commit install

After this, they will be run automatically on every commit.

Running Manually

To run the checks manually against all files:

pre-commit run --all-files

Maintenance

Rust Toolchain

When bumping the stable toolchain, update channel in rust-toolchain.toml. The comment there lists every other place that must be updated in sync (nightly version, CI workflows, pre-commit hooks).

Tool Versions

taplo, cargo-deny, and cargo-machete are pinned in two places that must stay in sync:

  • .pre-commit-config.yaml (hook rev)
  • .github/workflows/lint.yml (cargo install --version)

Building

For a full walkthrough of the CI build steps, from .circom source to release artifacts, see docs/build-pipeline.md.

Symbol Isolation in Circuit Libraries

The Problem

Each circuit (PoQ, PoL, PoC, Signature) is compiled into a static archive (libpoq.a, libpol.a, etc.). All archives share the same symbols, compiled from the same source files but with different constant values per circuit (e.g. get_size_of_witness() returns 18149 for PoQ and 20531 for PoL). When two or more circuit libraries are linked into the same binary, the linker silently picks the first definition it encounters for each symbol and discards the rest without any sort of error or warning. The result is that one circuit's constants end up hardwired into functions shared by both circuits, corrupting witness parsing. In practice: the wrong get_size_of_witness() value causes loadCircuit to compute an incorrect buffer size, pu32 walks off the end of the buffer, reads garbage as a length field, and the subsequent memcpy reads past the stack guard page, which results in a SIGSEGV.

The Fix

The Makefile uses a two-step process to hide all circuit-specific symbols before archiving:

  1. Partial link (ld -r): merges all circuit-specific .o files into a single relocatable object. fr.o is excluded — it contains only field arithmetic with no circuit-specific calls and is safe to deduplicate across circuits.
  2. Symbol localization: demotes every global symbol to local except the two public FFI entry points ($(PROJECT)_generate_witness and $(PROJECT)_generate_witness_from_files). Local symbols are invisible to the final linker, so each archive retains a private copy.

llvm-objcopy vs GNU objcopy

llvm-objcopy is required on Linux. GNU objcopy only changes the binding of COMDAT signature symbols to local, confusing the linker's deduplication logic and causing "relocation refers to symbol in discarded section" errors. llvm-objcopy additionally clears the GRP_COMDAT flag, turning affected sections into regular non-COMDAT sections. Slightly larger binary, no linker errors.

macOS

Uses llvm-objcopy (from brew install llvm, LLVM 20+).

Mach-O prepends _ to every C symbol, so --keep-global-symbol arguments must include the leading _. The Makefile's SYM_PREFIX variable handles this automatically.

Windows

Uses GNU's objcopy (from MinGW binutils).

GNU's objcopy works correctly on COFF, mapping local binding to storage class C_STAT. The ELF GRP_COMDAT problem doesn't apply: COFF COMDAT is per-section rather than group-based.

FFI Maintenance

PUBLIC_SYMS is hardcoded to $(PROJECT)_generate_witness and $(PROJECT)_generate_witness_from_files in the Makefile. If the public FFI API ever changes — entry points renamed or new ones added — update that variable, otherwise the affected symbols will be localized and linking will fail.


Releasing

Triggering a Release Build

To trigger a release build:

  1. Create and push a tag in the format vX.Y.Z:

    git tag v1.2.3 -m "Release v1.2.3"
    git push --tags
    
  2. This will automatically trigger the .github/workflows/build_circuits.yml workflow.

  3. Once the workflow finishes, the generated artifacts will be attached to a new release.

Pull Requests will also generate artifacts, which may be found on the job's page, but won't generate a new release.

Generated artifacts

For each supported platform (Linux x86_64, Linux aarch64, macOS aarch64, Windows x86_64), a release artifact is generated:

logos-blockchain-circuits-{version}-{os}-{arch}.tar.gz — a complete bundle containing all components needed to generate and verify proofs for all circuits.

Bundle Structure:

logos-blockchain-circuits-{version}-{os}-{arch}/
├── lib/
│   └── libgmp.a
├── {circuit}/              (poc/, pol/, poq/, signature/)
│   ├── include/
│   ├── lib{circuit}.a
│   ├── proving_key.zkey
│   ├── verification_key.json
│   └── witness_generator.dat
├── prover[.exe]
├── verifier[.exe]
└── VERSION

On Windows, static libraries use the .lib extension instead of .a (e.g. pol.lib, gmp.lib).

The proving keys are generated using the Hermez Powers of Tau ceremony — see docs/build-pipeline.md § Step 2.

Publishing

Releases are marked as Draft and Pre-Release to ensure the changelog and pre-release steps are manually reviewed before going public. Before publishing:

  1. Review the changelog
    Ensure that all relevant changes are clearly listed and properly formatted.

  2. Confirm the pre-release checklist
    Verify that all required steps have been completed, then remove the checklist from the release notes.

  3. Mark the release as published

    • Uncheck “This is a pre-release.”
    • Publish the release (removing the Draft state).