Aleo for Agents is offline at present

blog·2026-10-21·12 min read

Deep Dive: Why Aleo Proving Keys Should Share the SRS Instead of Owning It

A proving key should describe a circuit. It should not quietly carry another private copy of a large, process-wide cryptographic dataset.

That distinction matters in snarkVM because a conventional proving-key load can deserialize SRS powers into the key's committer. Load several keys and the process may retain several copies of points that were identical on disk, identical in memory, and supposed to be shared.

The inclusion proving key is the sharpest example. Its committer key owns about 100 MB of SRS points when loaded through the older path. Saving that memory requires more than changing a serializer. The application must obtain the right SRS powers, verify the proving-key file, parse the key against the shared SRS, and install process-wide material before another code path initializes the old copy-owning version.

snarkVM PRs #3565, #3560, and #3559 split those jobs across explicit APIs. I think that is the right architecture. Cryptographic parsing belongs in snarkVM. Download retries, cache directories, container images, and artifact provenance belong to the application operating the prover.

The ownership problem

Aleo uses a universal structured reference string, or SRS, as common cryptographic material from which circuit-specific proving data can be derived. A proving key still contains circuit-specific structures, but some of its committer data can refer to powers already present in the universal SRS.

Two loading models follow from that arrangement.

In the owning model, deserializing a proving key reconstructs all the points represented in its file. The resulting object is self-contained. That is convenient because the caller supplies one blob and gets one usable key. Convenience gets expensive when ten proving keys each retain overlapping subsets of the same SRS.

In the sharing model, the SRS is loaded once. A proving key is then read against it with ProvingKey::from_bytes_le_with_srs, allowing the committer key to share those powers rather than retain another copy.

The cryptography does not change. Proofs are not smaller, constraints do not disappear, and verification rules stay put. Object ownership changes.

That can sound like ordinary deduplication. It is stricter than a cache. A cache avoids repeating work when two callers ask for the same complete object. Shared SRS loading changes the internal representation of each proving key so common powers have one backing allocation even while many distinct keys remain live.

Consider a worker that keeps credits transfer keys, translation keys, and the inclusion key warm. A file cache may stop repeated downloads, yet resident memory can still balloon after deserialization. Sharing attacks the resident-memory side of the problem.

Why loading became application policy

Older parameter-loading code had to answer questions far outside cryptographic parsing:

  • Is network access allowed in this build?
  • Where should downloaded files live?
  • Which process owns retry and timeout policy?
  • How does an operator verify an artifact copied into an offline image?

A library can choose defaults for those questions, but defaults become hidden policy. Hidden policy is especially awkward in browsers, air-gapped provers, reproducible containers, and services with their own artifact stores.

PR #3565 moves snarkvm-parameters toward a sans-I/O design. Without the filesystem feature, a native build makes no network or filesystem calls and does not link ureq. The application supplies SRS powers bytes. snarkVM parses and validates the cryptographic material it receives.

That split is clean:

text
Application                         snarkVM
-----------                         -------
Select network and SRS degree       Parse powers-file metadata
Fetch or open artifact              Check whether it covers a requested range
Apply cache policy                  Add validated powers to the SRS
Enforce size and memory budgets     Reject malformed or incompatible bytes
Record provenance                   Build keys against shared powers

PowersFile, missing_files_for, and add_powers_file support incremental SRS growth. A caller can ask which files are needed for a target range, obtain only those files, and add them to the in-memory SRS. The library does not need to know whether the bytes came from an HTTPS mirror, an OCI layer, IndexedDB, a removable drive, or a test fixture.

Sans-I/O is sometimes treated as an aesthetic preference. Here it is an operational boundary. A cryptographic crate that opens sockets can surprise a restricted worker, complicate dependency review, and make tests depend on remote availability. A parser that consumes bytes is easier to test and easier to embed.

There is a cost. Applications now have more work to do. Someone must decide where bytes come from, how long they remain cached, and what happens when a required powers file is absent. I still prefer that burden to a low-level crate making those decisions invisibly.

File identity before parsing

Caller-managed I/O creates a security question: how does the application know it loaded the intended proving key?

For shipped credits.aleo keys, the expected checksum depends on both the network and the function. A generic SHA-256 check in application code is insufficient unless the application also has a trustworthy table mapping every function and network to its expected digest.

PR #3559 adds Network::verify_credits_proving_key_file. The caller reads a candidate file by whatever mechanism its deployment permits, then asks the selected network implementation to check those bytes against the expected file identity. The method performs no I/O.

Only after that check should the caller parse the key against the shared SRS:

text
bytes = application.read(candidate)
network.verify_credits_proving_key_file(function, bytes)
proving_key = ProvingKey::from_bytes_le_with_srs(bytes, shared_srs)
process.install(function, proving_key)

The sketch is intentionally conceptual rather than copy-paste Rust. Exact signatures can change while the PR series settles, but the order should not.

Verification before parsing has two useful properties. Corrupt or mislabelled files fail before they create a large object, and the source of the bytes remains irrelevant to snarkVM. An object-store fetch and a file baked into a container pass through the same cryptographic checks.

Do not confuse file verification with proof verification. The former establishes that a supplied artifact matches the key file expected for a named network and function. The latter checks a generated proof against a verifying key. Both matter, at different boundaries.

A production manifest should record more than a filename. I would include the network, function locator, artifact digest, byte length, required SRS range, snarkVM revision, and deployment generation. Agents can inspect that manifest before assigning work rather than discovering an incompatible artifact halfway through proving.

The inclusion-key trap

The inclusion proving key needs extra treatment because it lives behind a process-wide OnceLock.

Before PR #3560, first use initialized that lock from shipped bytes. Even if an application had already loaded the universal SRS and knew how to parse the inclusion key against it, the application could not replace the value inside the lock. First access committed the process to the older representation.

Network::load_inclusion_proving_key lets the caller fill the slot first from its own verified file, read against the shared SRS. Startup order now carries meaning.

A sensible worker boot sequence looks like this:

  1. Read the deployment manifest and select the intended network.
  2. Grow the shared SRS until it covers every planned proving key.
  3. Read and verify caller-supplied network key files.
  4. Load the inclusion proving key before code can trigger lazy initialization.
  5. Admit proving jobs only after the parameter set is sealed.

Five steps are less cute than one lazy getter. They are also auditable.

The race deserves attention. If request handling begins while startup is still loading parameters, an innocent health check or background task may touch the inclusion key first. The OnceLock then contains the shipped, copy-owning form for the life of the process. There is no safe hot swap after other threads have borrowed it.

Treat parameter initialization as a startup barrier. Build the process, load the SRS, install global keys, then publish the worker as ready. Kubernetes readiness probes should report failure until that sequence completes. A serverless runtime with uncontrolled concurrent initialization needs a single initialization future or an equivalent gate.

About 100 MB for one key is enough to matter. On a dense prover host, repeated waste reduces the number of workers that fit without swapping. In a browser or constrained agent runtime, the same waste can decide whether proving starts at all.

Sharing changes the cache model

Disk caching and memory sharing solve different problems.

A disk cache answers, "Must I fetch these bytes again?" An in-memory SRS answers, "Must I retain these points again?" A loaded proving-key cache answers, "Must I parse or synthesize this circuit-specific key again?"

Collapsing those layers into one vague "key cache" makes capacity planning difficult. I would model them separately:

LayerIdentityMain costEviction policy
Powers filesNetwork, degree or range, digestDownload and diskContent-addressed, long-lived
Shared SRSNetwork and covered rangeResident memoryUsually process lifetime
Proving-key bytesFunction, network, digestDisk or object storageRelease-generation policy
Parsed proving keyFunction and program editionResident memory and parse timeLRU or planned warm set

The shared SRS should usually live as long as the worker. Shrinking it while proving keys still refer to its powers would defeat the ownership model and may be impossible by design. Parsed proving keys are better eviction candidates because they contain the circuit-specific portion and can be reconstructed from verified bytes plus the SRS.

Memory accounting needs the same separation. Summing proving-key file sizes will not predict resident set size. Serialized formats contain compressed representations, metadata, and repeated material. Measure peak RSS after loading the SRS, then after each parsed key, and again during a real proof.

Our earlier reproducible proving benchmark harness separates key synthesis from cold and warm proving. Add two more checkpoints for this architecture: SRS growth and key attachment. Otherwise a lower warm-proof median can hide a startup path that doubled memory.

A practical worker design

Suppose an operator runs a pool for credits.aleo transfers plus inclusion proofs. Each worker image contains a small manifest, while an internal artifact service stores immutable powers files and proving-key blobs by digest.

Startup can follow a strict state machine:

text
EMPTY
  -> MANIFEST_CHECKED
  -> SRS_READY
  -> FILES_VERIFIED
  -> GLOBAL_KEYS_INSTALLED
  -> READY

Any failure terminates the worker. Retrying inside a half-initialized process is risky because OnceLock state and partially grown SRS state may survive the failed attempt. Restarting from an empty process is boring and predictable.

Artifact fetching belongs outside the proving request path. An agent scheduler can inspect a job's execution-key requirements, compare them with the worker's manifest, and route the job to a warm worker. The execution-key preflight planner already gives operators a way to enumerate function and translation keys before proving. Pair that list with the SRS range required by those keys and the scheduler gains a real admission-control signal.

Prepared program contexts remain a separate optimization. The reusable proving-context project avoids repeating program parsing and import setup. Shared SRS loading avoids repeated ownership of common cryptographic powers. A worker doing sustained proving probably wants both.

One detail will bite teams that build autoscaling around file-cache hits: a warm node and a warm process are different things. The host may have every artifact in page cache while a new process still needs to parse the SRS and attach its proving keys. Record readiness latency in phases rather than publishing one startup number.

Portability gets better

Caller-managed parameter loading makes the same snarkVM core usable in environments with very different I/O rules.

A native service can fetch artifacts through its existing HTTP client, with the same mTLS, proxy, retry, and telemetry policy used elsewhere. An offline appliance can read signed release media. A browser can obtain bytes through fetch, a service worker, or IndexedDB without dragging a native HTTP stack into WASM. Tests can hand the parser fixed byte vectors and never touch the network.

Feature gating matters here. A native build without filesystem performs no filesystem or network access in the parameter crate, and ureq is absent. That reduces dependency surface as well as accidental I/O. Teams that ban outbound access can verify the property at build time rather than trusting a runtime flag.

Compared with systems that package universal setup material beside every circuit artifact, snarkVM's shared approach asks more from the loader and less from every live key. Compared with an opaque global downloader, it asks the application to expose its policy. Neither choice removes complexity. The better choice puts complexity where operators can see and test it.

What developers should change

Applications embedding snarkVM should stop treating parameter loading as a helper called on first proof. Make it part of deployment design.

Pin every artifact by digest. Verify credits key files through the network-specific API before parsing. Grow one shared SRS according to the planned workload. Install the inclusion key before any lazy access can win the race. Keep network fetches outside request execution, and report SRS plus parsed-key memory as separate metrics.

Agent builders should give parameter state a machine-readable form. A worker ought to answer which SRS ranges are present, which proving-key files were verified, which parsed keys are warm, and whether global initialization is complete. That lets an orchestrator reject an oversized job before it pushes the process into swap.

The old model made proving keys look self-contained. Operationally, they never were. Their correctness depended on network-specific expectations, setup material, and loader behavior. The new APIs put those dependencies in the open, while allowing the large common part to remain common.

That is a good trade. A little more startup code buys lower resident memory, deterministic offline operation, and a much cleaner security boundary between acquiring bytes and trusting them.