|
purify
C++ Purify implementation with native circuit and BPP support
|
Warning: This project is a work in progress and is pending thorough review. Expect changes, incomplete areas, and rough edges.
This repository contains a C++ port of the purify.py reference implementation, plus native circuit construction and BPP-backed benchmarking on top of secp256k1-zkp.
Canonical URLs:
Upstream reference material is available under reference/ as optional git submodules.
A repo-local technical note describing Purify and the two proving paths implemented here is available at docs/paper/purify-paper.pdf. The published copy lives at https://judica.org/purify/paper/ and the site landing page lives at https://judica.org/purify/. API documentation is published at https://judica.org/purify/docs/.
The LaTeX source lives at docs/paper/purify-paper.tex, and you can rebuild the PDF locally with:
CI also compiles the PDF and publishes it through the Pages site. The standalone ci workflow still uploads the PDF as a workflow artifact for inspection.
include/: public library headers intended for downstream consumerssrc/: compiled support sources plus private headerscli/: CLI entrypoint plus private CLI-only runtime wiring for purify_cppbench/: benchmark entrypoint for bench_purifyreference/: local guide plus optional reference submodules for upstream purify and the benchmark fork of secp256k1-zkpthird_party/secp256k1-zkp: cryptographic backend as a git submodulethird_party/nanobench: benchmark harness as a git submoduleInitialize the library dependency submodule first:
Configure and build:
Top-level builds enable the CLI, benchmark, and docs targets by default. When this repository is added to another CMake project via add_subdirectory(...), only the library target is enabled by default. Top-level builds also enable the regression test target by default.
For multi-config generators such as Xcode, build benchmarks with --config Release.
Add the repository as a git submodule, then wire it into the parent CMakeLists.txt:
Downstream code should include the library headers from the stable public include root:
If you want the bundled CLI, benchmark, or docs targets while consuming the project as a subdirectory, enable them explicitly before add_subdirectory(...):
To produce a pruned source export for downstream integrations, use:
Supported --include-extras flags are:
minified: trim third-party vendored trees to only the dependency closure needed by the exported sourcestests: include tests/, verification/, and the local reference-test patch treeextras: include the optional CLI, benchmark, fuzz, and docs support filesFlags can be combined with commas or pipes, for example:
The vendored export always excludes the optional reference/ subtrees, including the jonasnick reference checkout. Pruned exports also auto-disable missing optional CMake targets by default, while still failing fast if a caller explicitly enables a target whose source tree is absent.
Benchmarks require the additional nanobench submodule:
Then build bench_purify normally from the top-level project, or enable PURIFY_BUILD_BENCH before add_subdirectory(...) in a parent project.
Build and run the regression suite with:
To compare the generated verifier circuit against the checked-out Python reference implementation, initialize the reference submodule and enable the extra regression:
For a sanitizer-enabled debug build:
For a Clang ASan/UBSan integer-focused debug build:
For a Linux/GCC 32-bit debug build with -ftrapv enabled:
For a closer approximation of Bitcoin Core's i686 debug lane, use Clang 32-bit with libstdc++ debug mode, extra hardening flags, and the benchmark target enabled:
For Valgrind memcheck plus ctgrind-style secret-flow checks:
The Valgrind constant-time lane is a negative test, not a proof. It now splits into dedicated checks for the fixed-round secret divider, the hardened ladder core, the ladder-plus-affine-normalization path, and the constant-time field inverse path. It also covers a valid packed-secret subset end to end through unchecked secret unpack plus both generator multiplications, marking secret bytes undefined before entry and failing if Memcheck sees secret-dependent control flow or memory addresses on those paths.
For bounded model checking of the pure C wide-integer helpers with CBMC:
The CBMC harnesses in verification/cbmc/ now cover:
src/core/uint.csrc/core/field.c and src/core/curve.c under an explicit verification-only bridge stubThe field and curve proofs are intentionally not a proof of the production secp256k1-zkp backend. They are a proof that the local Purify arithmetic and curve logic are internally consistent under a small-field model that exercises the same algorithms.
Generate API documentation with Doxygen:
The generated HTML entrypoint is build/docs/html/index.html.
Optional reference material can be fetched separately:
The vendor-tags workflow keeps fresh moving tags on master for the main vendored export variants, including master-vendored-min, master-vendored-min-tests, and master-vendored-min-extras.
The purify_cpp binary provides:
Example:
bench_purify measures:
Run it with:
If bench_purify is launched from a non-Release CMake configuration, it prints a warning before running. The benchmark target forces release optimization flags, but the intended path is still a full Release build.
Optional flags:
Example output excerpt from the default benchmark configuration on a Macbook Air M4 16GB:
Nanobench now groups related rows by explicit unit, so the output is split into separate tables such as:
| ns/circuit | circuit/s | err% | total | purify |
|---|---|---|---|---|
| 28,132,875.00 | 35.55 | 1.7% | 0.14 | verifier_circuit.native.build |
| 732,113.07 | 1,365.91 | 10.7% | 0.06 | :wavy_dash: verifier_circuit.template.instantiate_native (Unstable with ~14.6 iters. Increase minEpochIterations to e.g. 146) |
| 367,025.65 | 2,724.61 | 5.4% | 0.06 | :wavy_dash: verifier_circuit.template.instantiate_packed (Unstable with ~29.0 iters. Increase minEpochIterations to e.g. 290) |
| ns/template | template/s | err% | total | purify |
|---|---|---|---|---|
| 26,308,917.00 | 38.01 | 1.1% | 0.13 | verifier_circuit.template.build |
| ns/evaluation | evaluation/s | err% | total | purify |
|---|---|---|---|---|
| 3,257,541.67 | 306.98 | 0.9% | 0.06 | verifier_circuit.template.evaluate_partial |
| 20,213.51 | 49,471.85 | 0.6% | 0.05 | verifier_circuit.template.evaluate_final |
| ns/cache | cache/s | err% | total | purify |
|---|---|---|---|---|
| 25,737,834.00 | 38.85 | 1.5% | 0.13 | puresign_legacy.message_proof_cache.build |
| ns/proof | proof/s | err% | total | purify |
|---|---|---|---|---|
| 263,514,542.00 | 3.79 | 1.1% | 1.38 | experimental_circuit.legacy_bp.prove |
| 40,884,667.00 | 24.46 | 1.8% | 0.21 | experimental_circuit.legacy_bp.verify |
| 482,518,750.00 | 2.07 | 0.7% | 2.44 | experimental_circuit.bppp_zk_norm_arg.prove |
| 53,572,666.00 | 18.67 | 0.4% | 0.27 | experimental_circuit.bppp_zk_norm_arg.verify |
| 83,220,083.00 | 12.02 | 0.0% | 0.42 | bppp.norm_arg.prove |
| 10,959,459.00 | 91.25 | 0.4% | 0.05 | bppp.norm_arg.verify |
| ns/resource_set | resource_set/s | err% | total | purify |
|---|---|---|---|---|
| 82,586,208.00 | 12.11 | 0.3% | 0.41 | experimental_circuit.legacy_bp_backend_resources.create |
| ns/nonce | nonce/s | err% | total | purify |
|---|---|---|---|---|
| 702,825.00 | 1,422.83 | 0.0% | 0.05 | puresign_legacy.nonce.prepare |
| 317,086,083.00 | 3.15 | 3.0% | 1.68 | puresign_legacy.nonce.prepare_with_proof |
| 303,744,208.00 | 3.29 | 3.4% | 1.66 | puresign_legacy.nonce.prepare_with_proof_cached_template |
| 553,057,042.00 | 1.81 | 2.0% | 2.97 | puresign_plusplus.nonce.prepare_with_proof |
| 523,149,250.00 | 1.91 | 0.5% | 2.66 | puresign_plusplus.nonce.prepare_with_proof_cached_template |
| 66,666,083.00 | 15.00 | 1.7% | 0.34 | puresign_legacy.nonce.verify_proof |
| 40,534,958.00 | 24.67 | 0.6% | 0.20 | puresign_legacy.nonce.verify_proof_cached_template |
| 81,794,834.00 | 12.23 | 0.5% | 0.41 | puresign_plusplus.nonce.verify_proof |
| 56,843,542.00 | 17.59 | 0.2% | 0.28 | puresign_plusplus.nonce.verify_proof_cached_template |
| ns/signature | signature/s | err% | total | purify |
|---|---|---|---|---|
| 778,245.80 | 1,284.94 | 0.4% | 0.05 | puresign_legacy.signature.sign |
| 316,064,416.00 | 3.16 | 0.1% | 1.58 | puresign_legacy.signature.sign_with_proof |
| 284,111,375.00 | 3.52 | 0.4% | 1.53 | puresign_legacy.signature.sign_with_proof_cached_template |
| 534,007,334.00 | 1.87 | 0.8% | 2.69 | puresign_plusplus.signature.sign_with_proof |
| 506,387,791.00 | 1.97 | 0.7% | 2.74 | puresign_plusplus.signature.sign_with_proof_cached_template |
| 29,724.02 | 33,642.83 | 25.3% | 0.06 | :wavy_dash: puresign_legacy.signature.verify (Unstable with ~352.8 iters. Increase minEpochIterations to e.g. 3528) |
| 75,070,333.00 | 13.32 | 7.2% | 0.43 | :wavy_dash: puresign_legacy.signature.verify_with_proof (Unstable with ~1.0 iters. Increase minEpochIterations to e.g. 10) |
| 38,522,875.00 | 25.96 | 1.2% | 0.19 | puresign_legacy.signature.verify_with_proof_cached_template |
| 79,021,916.00 | 12.65 | 0.2% | 0.41 | puresign_plusplus.signature.verify_with_proof |
| 54,723,916.00 | 18.27 | 0.7% | 0.28 | puresign_plusplus.signature.verify_with_proof_cached_template |
The benchmark output still uses the bppp labels emitted by the secp256k1-zkp backend. In this repository's terminology, the implementation is against BPP.