6.4 KiB
Contributor's Guide
Development Setup
Prerequisites
Sys development
- Rust — the pinned toolchain version is in
rust-toolchain.tomland will be installed automatically byrustup. - Compiled circuit libraries (
.afiles andwitness_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 viabrew 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(hookrev).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:
- Partial link (
ld -r): merges all circuit-specific.ofiles into a single relocatable object.fr.ois excluded — it contains only field arithmetic with no circuit-specific calls and is safe to deduplicate across circuits. - Symbol localization: demotes every global symbol to local except the two public FFI entry
points (
$(PROJECT)_generate_witnessand$(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:
-
Create and push a tag in the format
vX.Y.Z:git tag v1.2.3 -m "Release v1.2.3" git push --tags -
This will automatically trigger the
.github/workflows/build_circuits.ymlworkflow. -
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
.libextension 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:
-
Review the changelog
Ensure that all relevant changes are clearly listed and properly formatted. -
Confirm the pre-release checklist
Verify that all required steps have been completed, then remove the checklist from the release notes. -
Mark the release as published
- Uncheck “This is a pre-release.”
- Publish the release (removing the Draft state).