All source files should include a copyright header. New files should start with // Copyright YYYY The Binius Developers, where YYYY is the current year. When modifying an existing file, add the copyright line if one referencing "The Binius Developers" is not already present.
Many code formatting and style rules are enforced using rustfmt and Clippy. The remaining sections document conventions that cannot be enforced with automated tooling.
You can run the formatter and linter with
$ cargo fmt
$ cargo clippy --all --all-features --tests --benches --examples -- -D warningsPre-commit hooks are configured to run rustfmt. The codebase is formatted with a nightly
version of cargo fmt because stable doesn't support all of the rustfmt options we use. To run it, you can use:
$ pre-commit run rustfmt --all-files
$ cargo +nightly-2026-01-01 fmt # see .pre-commit-config.yaml for the exact nightly version checked by CIWe follow guidance from the rustdoc book. The "Documenting components" section is quite prescriptive. To copy verbatim:
It is recommended that each item's documentation follows this basic structure:
[short sentence explaining what it is] [more detailed explanation] [at least one code example that users can copy/paste to try it] [even more advanced explanations if necessary]
Documentation and commit messages should be written in the present tense. For example,
❌ This function will return the right answer
✅ This function returns the right answer
❌ Fixed the bug in the gizmo
✅ Fix the bug in the gizmo
Every crate must have module-level documentation in lib.rs using //! comments. This documentation should include:
- One sentence summary: What this crate does
- When to use: In what situations should someone reach for this crate
- Key types: Brief list of the main types/traits with one-line descriptions
- Usage example: A minimal working example (where practical)
- Related crates: How this crate relates to others in the workspace
All crates should enable #![warn(missing_crate_level_docs)] to ensure crate-level documentation exists.
Example crates with good module documentation: binius-field, binius-frontend, binius-spartan-frontend.
Include code examples for:
- Public functions that are part of the main API
- Types that users will construct directly
- Non-obvious behavior or edge cases
Examples in documentation are tested by cargo test --doc, so they also serve as regression tests.
This codebase biases towards longer, more descriptive names for identifiers. This extends to the names of generic type parameters.
In idiomatic Rust code, generic parameters are often identified by single letters or short, capitalized abbreviations. We tend to prefer more descriptive, CamelCase identifiers for type parameters, especially for methods that have more than one or two type parameters. There are some exceptions for common type parameters that have single-letter abbreviations. They are:
Findicates aFieldparameterPindicates aPackedFieldparameter
If a function or struct is generic over multiple types implementing those traits, the type names should start with the
single-letter abbreviation. For example, a function that is parameterized by multiple fields may name them
F, FSub, FDomain, FEncode, etc., where FSub is a subfield of F, FDomain is a field used as an evaluation
domain, and FEncode is used as the field of an encoding matrix.
If an identifier is defined in a module and is unambiguous in the context of that module, it does not need to
duplicate the module name into the identifier. For example, we have many protocols defined in binius_core::protocols
that expose a prove and verify method. Because they are namespaced within the protocol modules, for example the
sumcheck module, these identifiers do not need to be named sumcheck_verify and sumcheck_prove. The caller has the
option of referring to these functions as sumcheck::prove / sumcheck::verify or renaming the imported symbol, like
use sumcheck::prove as sumcheck_prove.
Prefer functional style over imperative style with mutable variables. Use iterator combinators (map, filter, fold,
etc.) instead of loops with mutable state. Exceptions are allowed when algorithms have substantial mutable state that
would be awkward to express functionally.
Good examples:
// Use fold instead of mutable accumulator
let result = items.iter().fold(initial, |acc, &item| {
compute_next(acc, item)
});
// Use iter::zip instead of .iter().zip()
let pairs: Vec<_> = iter::zip(&vec_a, &vec_b)
.map(|(&a, &b)| process(a, b))
.collect();
// Use successors for generating sequences
let powers = iter::successors(Some(x), |&prev| Some(prev * x))
.take(n)
.collect();Poor examples:
// Avoid mutable state when fold is clearer
let mut result = initial;
for &item in items.iter() {
result = compute_next(result, item);
}
// Avoid .iter().zip() - use iter::zip() instead
let pairs: Vec<_> = vec_a.iter()
.zip(&vec_b)
.map(|(&a, &b)| process(a, b))
.collect();
// Avoid imperative loops for sequence generation
let mut powers = Vec::new();
let mut current = x;
for _ in 0..n {
powers.push(current);
current = current * x;
}Don't call unwrap in library code. Either throw or propagate an Err or call expect, leaving an explanation of why
the code will not panic. Unwrap is fine in test code.
Example from the Substrate style guide:
let mut target_path =
self.path().expect(
"self is instance of DiskDirectory;\
DiskDirectory always returns path;\
qed"
);Verifier code is optimized for simplicity, security, and readability, whereas prover code is optimized for performance. This naturally means there are different conventions and standards for verifier and prover code. Some notable differences are
- Prover code often uses packed fields; verifier code should only use scalar fields.
- Prover code often uses subfields; verifier code should primarily use a single field.
- Prover code often uses Rayon for multithreaded parallelization; verifier code should not use Rayon.
- Prover code can use complex data structures like hash maps; verifier code prefer direct-mapped indexes.
- Prover code can use more experimental dependencies; verifier code should be conservative with dependencies.
The codebase uses different error-handling strategies depending on the abstraction level and trust boundary.
Most internal code should use assertions and documented precondition contracts rather than returning Result types.
This simplifies internal APIs and makes code easier to reason about. Functions should document their preconditions and
use assert!, debug_assert!, or expect() to enforce them.
/// Evaluates the polynomial at the given point.
///
/// # Preconditions
/// - `point.len()` must equal the number of variables in the polynomial
fn evaluate(&self, point: &[F]) -> F {
assert_eq!(point.len(), self.n_vars());
// ...
}Errors should be returned when unchecked external input could cause a panic. The key distinction is:
-
Verifier: The high-level input is the proof. The verifier cannot trust the proof, so it must return
VerificationErrorfor invalid proofs rather than panicking. This is the boundary where untrusted data enters. -
Prover: The high-level input is the witness. The prover may assume the witness is satisfying. If the witness is invalid, the prover code may panic. This is acceptable because the caller is responsible for providing a valid witness.
Error types should only be used at high-level interfaces:
binius_prover::Prover- returns errors only for system-level failures (not invalid witnesses)binius_verifier::Verifier- returnsVerificationErrorfor invalid proofs- Similar interfaces in spartan modules
Below these interfaces, code should use precondition contracts. This keeps internal APIs simple and pushes validation to the boundaries where untrusted data enters the system.
We use plenty of useful crates from the Rust ecosystem. When including a crate as a dependency, be sure to assess:
- Is it widely used? You can see when it was published and total downloads on
crates.io. - Is it maintained? If the documentation has an explicit deprecation notice or has not been updated in a long time, try to find an alternative.
- Is it developed by one person or an organization?
The project welcomes first time contributions from developers who want to learn more about Binius64 and make an impact on the open source cryptography community.
If you are new to the project and don't know where to start, you can look for open issues labeled
good first issue or add
test coverage for existing code. Adding unit tests is a great way to learn how to interact with the codebase, make a
meaningful contribution, and maybe even find bugs!
On the other hand, we do not accept typo fix PRs from first-time contributors. These are not significant enough to justify the additional work for maintainers nor any potential benefits, tangible or intangible, one might get from being listed as a contributor to the repo.