Aleo for Agents is offline at present

blog·2026-08-26·9 min read

Deep Dive: Why Aleo Is Taking Telemetry Out of the BFT Hot Path

The observer can stall the machine

A validator can miss a timing window because somebody asked it for a metric.

That sounds absurd until observability shares synchronization with consensus. In the telemetry design targeted by snarkOS PR #4391, validator participation state sat behind four Arc<RwLock<...>> fields. BFT code wrote those structures while heartbeat, metrics, and REST paths read them. A comment warned maintainers not to acquire the locks inside, or hold them across, a Rayon parallel iterator. Warnings like that are useful, but they are also smoke from an architectural fire.

The problem is larger than the cost of one lock acquisition. A reader can extend a writer's wait. A writer can delay a scrape. Another code path can take locks in a different order. Parallel work makes the resulting schedule hard to reason about, and an innocent future edit can turn a careful convention into a deadlock. Consensus code now depends on the behavior of monitoring clients that have no role in deciding a block.

PR #4391 proposes a cleaner rule: BFT reports telemetry updates, while one worker owns and mutates telemetry state. PR #4406 then proposes folding the telemetry feature into the metrics feature. On the snarkVM side, PR #3364 caches signer addresses recovered from a BatchCertificate, so storage and participation accounting do not repeat the same elliptic-curve work.

I like the direction. Observability should describe consensus, not become part of its scheduling policy.

Ownership beats lock etiquette

An RwLock looks reasonable for telemetry. There are many readers, writes appear small, and Rust makes access explicit. The trap is that lock topology leaks into every caller. Any code touching two fields must know acquisition order. Any parallel loop must know which guards may outlive an iteration. An HTTP endpoint can become a participant in BFT latency by holding a read guard longer than expected.

The proposed single-owner worker changes the shape of the problem. Consensus no longer borrows shared state and waits for permission to mutate it. Instead, it emits a lightweight update to the worker. The worker processes updates in order and is the only place where mutable telemetry state exists.

Conceptually, the boundary looks like this:

text
BFT task -- participation update --> telemetry worker
REST     -- snapshot request ------> telemetry worker
metrics  -- snapshot request ------> telemetry worker
heartbeat -- report request -------> telemetry worker

No caller receives a guard into the worker's state. Read paths get a snapshot or response. Write paths hand off facts such as a committed certificate or participation event. Serialization replaces shared-memory coordination.

That distinction matters. With locks, safety depends on every caller obeying a global protocol forever. With ownership, the type and message boundaries remove whole classes of bad interleavings. The worker still has concurrency around it, but its state transition logic is sequential.

Single ownership is not magic. Queueing moves contention rather than deleting all of it. A bounded channel can fill, an unbounded channel can eat memory, and a slow snapshot calculation can delay later updates. The right policy depends on the event:

  • Never drop an update that changes a validator's participation score unless the metric is explicitly approximate.
  • Coalesce replaceable values, such as a latest-height gauge, when intermediate values have no reporting value.
  • Keep expensive formatting out of the worker; build a cheap snapshot and format it on the serving side.
  • Expose worker lag and queue depth, or operators will be blind to stale telemetry.

Those are operational choices, not footnotes. Taking locks out of BFT prevents an exporter from directly parking a consensus task. A badly designed queue can still create backpressure, so the send side must have a documented failure mode.

Certificate work belongs at the boundary

Lock removal handles scheduling interference. PR #3364 attacks wasted CPU.

A BatchCertificate carries signatures from validators attesting to a batch. Consumers need the corresponding signer addresses for certificate checks, storage decisions, and participation accounting. Recovering an address from a signature is deterministic, but it is not free. The PR notes that to_address() takes roughly 20 to 50 microseconds and involves finite-field and elliptic-curve operations.

Repeated recovery is easy to miss in a profile. Each call looks small. Multiply it by the signatures in a certificate, then by every subsystem that asks for signers, then by certificates arriving under load. The total lands in the same process that is trying to keep up with consensus.

Take a hypothetical certificate with 80 signatures. Four independent passes over its signers cost about 6.4 to 16 milliseconds at the PR's stated per-address range:

text
80 signatures x 4 passes x 20-50 us = 6.4-16 ms

Recover once and retain the result at the certificate boundary, and the cryptographic portion falls to about 1.6 to 4 milliseconds for that object. Later consumers read cached addresses. The exact wall-clock gain depends on hardware, batch size, cache behavior, and surrounding checks, but the scaling argument is plain.

The location of the cache is the good part. A global map keyed by certificate ID would need eviction, synchronization, and memory accounting. Caching on BatchCertificate ties the derived value to the object whose signatures produced it. The lifetime is natural. So is invalidation, provided the certificate is immutable after construction.

A safe derived-data cache needs a few properties:

  • Wire encoding must contain the signed certificate data, not process-local cache state.
  • Equality and hashing should not change after the cache warms.
  • Clones must preserve correct behavior whether they share, copy, or lazily rebuild cached data.
  • Recovery failure must not leave a partial signer set that later callers mistake for valid.

Caching recovered addresses does not remove certificate verification. It removes duplicate derivation. Quorum checks, signature validity, committee membership, and certificate identity still belong wherever the protocol requires them. Conflating cached identity with verified authority would be a serious bug.

The earlier article Aleo This Week: Canonical BFT Payloads, Bounded Type Checks, and Visible RocksDB covered a related pattern: accept canonical data at the boundary, memoize repeated type work, and collect RocksDB pressure away from foreground operations. The same engineering instinct appears here. Pay once when an object crosses a trust or ownership boundary, then pass around the checked or derived form.

One path through the node

Consider a certificate that reaches a validator and later appears in a participation report.

Under the shared-lock design, storage checks may recover all signer addresses before committing the certificate. Telemetry may recover those addresses again to update validator scores. The telemetry update then acquires one or more shared locks. A Prometheus scrape or REST request can be reading the same state at that moment. None of those operations is individually outrageous. Their composition is the problem.

The proposed flow is tighter:

text
1. Receive BatchCertificate
2. Recover signer addresses on first demand
3. Cache the recovered addresses on the certificate
4. Reuse them during storage and participation accounting
5. Send a compact telemetry update to the single-owner worker
6. Let metrics, heartbeat, and REST readers request snapshots

The sequence separates two kinds of work. Certificate-local cryptography stays with the certificate and is memoized. Cross-certificate aggregation stays with the worker and is serialized. BFT does enough work to establish the event, then leaves reporting state to another owner.

PR #4406 matters because feature boundaries influence whether the design stays clean. Telemetry and metrics are two views of node observability. Keeping them as separate compile-time features can duplicate plumbing, produce awkward combinations, and make testing the ownership boundary harder. Folding telemetry into metrics gives the worker one operational home.

There is a tradeoff. Operators who wanted participation telemetry without the broader metrics feature may lose a narrow build configuration, depending on the final merged behavior. A combined feature can also increase the amount of code enabled in validator builds. I would still choose one observability subsystem over two partially overlapping ones. Fewer ownership boundaries are easier to audit than a smaller binary with tangled ones.

Testing should focus on pressure, not only correctness at idle. Hold REST requests open. Scrape metrics rapidly. Feed certificates with large signer sets. Restart the worker. Fill the update queue. Then verify that consensus continues, snapshots become current again, and no participation update vanishes unless the drop policy permits it. A race-free unit test is not enough for this change.

What developers and agents should change

Most Leo application developers will never touch BatchCertificate. They will still feel this work through steadier validator behavior, especially during load spikes or heavy monitoring. Node operators and automation authors have more direct responsibilities.

First, treat observability endpoints as production traffic. An agent that polls participation every second across a validator fleet can create synchronized load. Prefer Prometheus scraping at a measured interval, add jitter across nodes, and cache repeated REST results upstream. The new ownership boundary limits the damage, but polite clients still matter.

Second, watch freshness as well as values. A participation score can look valid while the worker is behind. Export a last-processed round or timestamp beside the score. Alert on lag, queue saturation, worker restarts, and snapshot age. CPU usage alone will not tell you that reports are stale.

Third, profile certificate handling by phase. Separate signature recovery time from signature verification, storage, and telemetry aggregation. After the cache lands, repeated calls should become cheap. If profiles still show address recovery in multiple consumers, a clone or deserialization path may be discarding the cache.

Fourth, keep the cache semantically invisible. Agents replaying certificates, indexers decoding them, and nodes exchanging them must agree on certificate bytes regardless of whether a local cache has been populated. A warm object and a cold object should serialize identically and compare the same.

The architectural lesson is narrow and useful: derived cryptographic facts belong beside the immutable object that determines them, while mutable reporting aggregates belong behind one owner. BFT should emit the smallest update it can and move on. That gives operators visibility without letting the dashboard reach back into consensus and grab a lock.

Sources