一键导入
project-developer-guide
Development conventions and practices for this codebase. Use when writing PureScript code, tests, or FFI in this project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Development conventions and practices for this codebase. Use when writing PureScript code, tests, or FFI in this project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Tools for analyzing OCaml mina source code to support translation. Use when you need to understand types, field assignments (Fp vs Fq), functor instantiations, or the structure of OCaml circuit code before translating to PureScript.
Iteratively build out the PureScript step prover for the Pickles `Simple_chain` test by diffing transcript logs against the OCaml fixture, fixing one divergence per iteration, until the traces match byte-for-byte or a fundamental obstruction is hit. The loop is the test infrastructure built in commits 0292b512..5389ac7b.
Add new Kimchi gate circuits to the JSON comparison tests that verify PureScript circuits produce identical constraint systems to OCaml. Use when adding a new gate circuit or debugging a circuit mismatch.
Expert guidance on the Pickles recursive proof system (2-cycle recursion). Use when working with Step/Wrap verifiers, cross-field arithmetic (Type1/Type2 shifting), polynomial commitment batching (IPA), or translating Pickles logic from Mina's OCaml implementation.
Translate OCaml snarky/kimchi circuits from mina into PureScript. Use when translating code from mina/src/lib/snarky, mina/src/lib/pickles, or related crypto libraries.
| name | project-developer-guide |
| description | Development conventions and practices for this codebase. Use when writing PureScript code, tests, or FFI in this project. |
These are the established conventions and practices for this codebase.
All PureScript dependencies are installed with full source code in .spago/p/. When uncertain about a function's behavior, signature, or available instances — look it up.
.spago/p/prelude-7.0.0/src/...
.spago/p/transformers-6.1.0/src/...
.spago/p/foldable-traversable-7.0.0/src/...
Use whatever tool fits the situation:
Do not guess at function signatures or behaviors. Do not rely on potentially stale knowledge. The source is right there.
All testing uses spec, quickcheck, and spec-quickcheck. We do not write one-off unit tests or example-based tests.
Tests assert properties over randomly generated inputs. This is appropriate for the domain — cryptographic primitives and circuit correctness are about invariants holding universally, not about specific examples passing.
Consult .spago/p/ for these libraries when needed:
spec — test organization and assertionsquickcheck — property-based testing, Arbitrary instancesspec-quickcheck — integration between the twoFor testing arithmetic circuits, use packages/snarky-test-utils. Do not invent new testing infrastructure.
The core pattern is CircuitSpec:
type CircuitSpec f c r m a avar b =
{ builtState :: CircuitBuilderState c r
, solver :: SolverT f c m a b
, checker :: Checker f c
, testFunction :: a -> Expectation b -- pure reference function
, postCondition :: PostCondition f c r
}
The test framework:
testFunction to get expected outputThe utilities in snarky-test-utils are used throughout the codebase. If you need a testing pattern, it's almost certainly already there.
Cryptographic primitives come from Rust via packages/crypto-provider (the snarky-crypto node module). The FFI follows a three-layer architecture:
foreign import has a corresponding .js filesnarky-cryptoWhen FFI returns flat/unstructured data (like [x0, y0, x1, y1, ...]), transform it in the JS layer into properly structured records that match domain semantics.
Why:
Vector 16 (LrPair f) instead of Array f{ l :: AffinePoint f, r :: AffinePoint f } immediately conveys meaningExample: Rust returns lr pairs as flat coordinates [l0.x, l0.y, r0.x, r0.y, l1.x, ...]
// In .js file — parse into structured records
export const proofOpeningLr = (proof) => {
const flat = snarky.proofOpeningLrFlat(proof);
const pairs = [];
for (let i = 0; i < flat.length; i += 4) {
pairs.push({
l: { x: flat[i], y: flat[i + 1] },
r: { x: flat[i + 2], y: flat[i + 3] }
});
}
return pairs;
};
-- In .purs file — clean typed interface
foreign import proofOpeningLr :: Proof g f -> Array { l :: AffinePoint f, r :: AffinePoint f }
The JS layer acts as a marshalling layer between native representation and the PureScript type system.
foreign import declarations with proper typesforeign import data for opaque Rust objectsExample: See packages/pickles/test/Test/Pickles/ProofFFI.{purs,js} for a comprehensive example of this pattern.
Many operations exist in two forms:
Pure functions are typically thin wrappers around Rust FFI, not PureScript implementations. The Rust code (especially o1labs/proof-systems) provides the ground truth.
Why? This is cryptography. Complex arithmetic sequences must match protocols exactly. Since we're defining circuits that compute these protocols, we need "one foot in truth" — the pure reference function must come from a known-correct implementation.
The snarky-test-utils pattern ensures circuits match their pure counterparts:
Circuit output == Pure function output (backed by Rust)
If these disagree, the test fails. This is how we know circuits are correct.
m ParameterThe Snarky c t m monad has three type parameters:
c — constraint type (e.g., KimchiConstraint f)t — state type for circuit buildingm — the advice monad for providing witness dataCircuits sometimes need data that can't be computed from circuit variables alone. For example:
This data must be "conjured up" by the prover during witness generation. The m parameter is how we abstract over this.
1. Define a typeclass for your advice:
-- Simple example from snarky-test-utils/src/Test/Snarky/Circuit/Factors.purs
class Monad m <= FactorM f m where
factor :: F f -> m { a :: F f, b :: F f }
2. Use it in your circuit via exists:
factorsCircuit :: forall t m f c. FactorM f m => CircuitM f c t m => FVar f -> Snarky c t m Unit
factorsCircuit n = do
{ a, b } <- exists do
nVal <- read n -- read the circuit variable's concrete value
lift $ factor @f nVal -- call into the advice monad
-- Now a and b are circuit variables (FVar f)
-- The prover provided their values, the circuit constrains them
c1 <- equals_ n =<< mul_ a b
assert_ c1
3. Provide instances for different phases:
-- For proving: actually compute the witness
instance PrimeField f => FactorM f Gen where
factor n = do
a <- arbitrary `suchThat` \a -> a /= one && a /= n
pure { a, b: n / a }
-- For compilation: crash (should never be called)
instance FactorM f Effect where
factor _ = throw "unhandled request: Factor"
| Phase | Monad | Advice behavior |
|---|---|---|
| Compile | Effect or Identity-based | Crashes — advice should not be requested during compilation |
| Prove | Gen, ReaderT Ref Effect, etc. | Provides actual witness data |
Use circuitSpec' (not circuitSpecPure') when your circuit needs advice:
circuitSpec' 100 randomSampleOne -- randomSampleOne :: Gen ~> Effect
{ builtState: s
, checker: eval
, solver: solver
, testFunction: satisfied_
, postCondition: postCondition
}
gen
The second argument is a natural transformation m ~> Effect that runs the advice monad.
See packages/example/ for a complete worked example:
src/Snarky/Example/Circuits.purs — circuits using AccountMapM and MerkleRequestMtest/Test/Snarky/Example/Monad.purs — TransactionM (proving) and TransferCompileM (compilation) instancestest/Test/Snarky/Example/Circuits.purs — testing with circuitSpec'Use advice when your circuit needs witness data that:
Do not use advice for values that can be computed purely from circuit inputs — just compute them directly.
In the context of circuits, proofs, and cryptographic verification, data is almost always statically sized. Use PureScript's type-level sized containers instead of dynamic collections.
| Dynamic (avoid) | Static (prefer) | Package |
|---|---|---|
Array a | Vector n a | sized-vector |
BigInt (unconstrained) | SizedF n f | snarky |
Vector n a is just Array a at runtimeEven when the underlying JavaScript/Rust type is dynamically sized (e.g., Array), use static types on the PureScript side:
-- The JS returns an Array, but we know it's always 15 elements
foreign import proofWitnessEvals :: Proof g f -> Vector 15 (PointEval f)
-- The challenges are always IPA_ROUNDS long (e.g., 16 for SRS size 2^16)
foreign import proofBulletproofChallenges :: ... -> Array f -- Convert to Vector d f
The JS wrapper can return a plain array; the PureScript foreign import declares the static type. If sizes don't match, you'll get a runtime error — which is the correct behavior (it indicates a bug in the Rust/JS layer or a misunderstanding of the protocol).
Before writing code, investigate:
Then use the appropriate type-level natural.
-- FFI returns Array (dynamic)
challengesArray :: Array f
challengesArray = proofBulletproofChallenges proverIndex { proof, publicInput }
-- Convert to Vector (static) — fails if length doesn't match
challenges :: Vector 16 f
challenges = unsafePartial $ fromJust $ Vector.toVector challengesArray
The unsafePartial is acceptable here because a length mismatch indicates a protocol violation, not normal program flow.
| Principle | Practice |
|---|---|
| Uncertain about a library function? | Look it up in .spago/p/ |
| Writing tests? | Property-based with quickcheck, use snarky-test-utils for circuits |
| Need crypto primitives? | FFI from crypto-provider, follow three-layer pattern |
| Implementing a circuit? | Test against pure function; pure function wraps Rust |
| Reimplementing crypto in PureScript? | Don't. Wrap the Rust. |
| Circuit needs external witness data? | Use the advice pattern with exists and a custom typeclass |
| Data has known size? | Use Vector n a or SizedF n f, not Array |