Aleo for Agents is offline at present

blog·2026-08-28·11 min read

Aleo This Week: Hard Limits on a Node's Untrusted Edges

The interesting Aleo work this week has almost nothing to do with new features. It is a cluster of changes that all answer the same question from different angles: what happens when a peer sends your node something deliberately shaped to hurt it?

Five pull requests, two repos, one theme. A node's untrusted edges are getting hard numeric limits instead of polite assumptions.

A u64 on the wire is not an allocation request

Start with the smallest diff and the biggest blast radius. snarkVM PR #3391 caps the eager capacity reserve in the CanonicalDeserialize implementation for Vec<T>.

Here is the shape of the problem. The deserializer reads a u64 length prefix, then calls reserve for that many elements before reading a single byte of actual payload. On a P2P path the length prefix is whatever the sender decided to write. So a message of a few dozen bytes can claim it contains a trillion field elements, and the receiving node dutifully asks the allocator for terabytes. The message never has to be valid. It never even has to finish.

The fix is boring in the best way: cap the eager reserve to a small hint and let the vector grow through push. Growth is then bounded by how many bytes the reader actually has, which is a number the node already controls through its frame limits. Amortized reallocation costs a little for genuinely large legitimate vectors. I will take that trade every day of the week, because the alternative is a resource-exhaustion primitive that any connected peer can fire for free.

I keep seeing this exact bug in serialization code across the industry, not just here. Length-prefixed formats invite it. The prefix looks like metadata, so it gets trusted like metadata, when it is the most attacker-controlled field in the entire message.

One detail about provenance: the investigation started with an operator report that connections between peer validators were reserving enormous buffers. That single report produced two separate fixes, which says something good about how the team chases symptoms back to causes.

Check, await, insert, and the gap in the middle

The second fix is a classic time-of-check-to-time-of-use race, and snarkOS PR #4399 closes it at the insertion point rather than the check point.

process_transmission_id_from_ping looked at the worker's ready queue, compared num_transmissions() against MAX_TRANSMISSIONS_PER_WORKER, and spawned an asynchronous fetch if there was room. The insertion that eventually followed did not re-check. Since the fetch is async, twenty concurrent pings can each observe a queue with one slot free, all pass the guard, and then all insert. The limit holds on paper and fails in practice.

A malicious validator needs nothing clever to exploit that. It advertises transmission IDs faster than the peer can drain its queue and watches the ready queue inflate past a bound the code believes it is enforcing. Memory grows. The worker's own accounting stops matching reality.

The correction is to enforce capacity where the mutation happens, inside the same critical section as the insert. That sounds obvious in hindsight. It usually is. What makes the pattern hard to catch in review is that the guard is present and it is correct at the moment it runs. The await between the guard and the write is invisible in the reviewer's mental model until somebody deliberately asks how many tasks can be in flight at once.

If you maintain async Rust and you have a capacity check anywhere upstream of an .await, go look at it now. I would bet money you have a sibling of this bug.

Peers that stop reading used to get forgiven forever

snarkOS PR #4381 fixes outbound writing, and the failure mode it describes is one of my favorite kinds: a timeout that fires correctly and then accomplishes nothing.

Two issues, both in write_to_stream. First, the timeout wrapped only part of the write path, so feed and flush were not both covered. A peer that accepted bytes into the buffer but stalled on the flush could hold the writer past the deadline. Second, and worse, when the timeout did fire it produced io::ErrorKind::TimedOut, which was absent from fatal_io_errors. The writer logged the error and carried on. The peer stayed connected. Every write attempt timed out, every timeout was logged, and the connection was never dropped.

That is a backpressure sink that costs the victim real resources on a schedule set by the attacker. It also produces exactly the kind of log noise that trains operators to ignore their logs.

Now both calls sit under the timeout, and a timed-out write is fatal, so the peer is disconnected. The change was extracted from the larger PR #4378, which I appreciate as a review practice. A minimal diff that fixes one concrete failure gets merged. A large refactor that fixes five things argues about itself for two weeks.

Two copies per event frame, now zero

snarkOS PR #4375 came out of the same buffer investigation as the snarkVM reserve cap. Serializing Data::Object through the Event codec was making two needless copies: one temporary byte buffer for the input, and one more from a freeze that split the data off the BytesMut the codec was supposed to be appending to.

The new strategy writes in place. Reserve a slot for the length, serialize the payload straight into the destination buffer, then backfill the length once you know it. No temporary allocation, no split, no freeze.

This one is a performance fix that doubles as a resource fix. Peak memory during serialization of a large event was previously some multiple of the frame size, and on a validator gateway pushing many concurrent outbound events, multiples matter. Cheaper broadcasts were already a theme in the August 14 digest when the gateway outbound queues got reworked. The codec was the next layer down.

Making a generic signature stop looking like a request

snarkVM PR #3387 is the one I would flag to anyone building on Aleo with off-chain signing, because it changes what Signature::sign will accept.

The rule added is narrow and specific: Signature::sign now refuses any message whose second element equals hash_psd2 applied to a single-element slice holding the first. That condition is the fingerprint of a Request::sign preimage. Without the check, a program or SDK integration that asks a user to sign an arbitrary message could be steered into producing a signature that also validates as an authorization for a function call. The user thinks they signed a login challenge. The bytes say they authorized a transition.

Domain separation is the standard defense, and it works by making the two message shapes structurally impossible to confuse. Aleo's request signing already puts a distinguishing hash in the second field, so the cheap version of the fix is to refuse to sign anything that happens to land in that shape. It is the urgent slice of the larger design in PR #3284.

I want to be honest about the tradeoff, because there is one. This is a rejection at the signing API, not a proof that no other message shape can collide with anything else. A full fix separates domains by construction, with an explicit tag committed inside the hash, so no accidental coincidence of field values can produce ambiguity. Refusing one known-bad shape is a patch. It is the right patch to ship immediately, and it is not the end of the work.

For application developers, the practical takeaway is to verify signatures inside your program against a digest you control, rather than treating a raw signature over user-supplied fields as an authorization:

leo
struct Claim {
    subject: address,
    purpose: field,
    nonce: field,
}

program sig_domain.aleo {
    @noupgrade
    constructor() {}

    fn attest(signer_addr: address, sig: signature, claim: Claim) -> field {
        let digest: field = BHP256::hash_to_field(claim);
        assert(sig.verify(signer_addr, digest));
        return digest;
    }
}

The purpose field is doing the work. Pick a distinct constant per action in your protocol, commit it inside the hashed struct, and a signature meant for one path cannot be replayed into another. Note that the struct sits outside the program block, which is where Leo 4.x wants it. Cheap to add on day one, painful to retrofit after your first signature-reuse incident.

Housekeeping that stops CI from lying

A few smaller merges worth a line each.

snarkVM PR #3390 pins the nightly toolchain that check-fmt formats with, and snarkOS PR #4411 ports the same change with an enum. Before this, install_rust_nightly resolved to whatever nightly existed on the morning the job ran, so a pull request that touched no formatting at all could go red because rustfmt's output moved overnight. Contributors got green locally and red on the branch with nothing to explain the difference. The snarkOS port verifies zero diff under cargo +nightly-2026-04-02 fmt --all -- --check.

snarkOS PR #4408 fixes EventCodec roundtrip tests that failed because any_block_response generated ConsensusVersion::V11, a version omitted on the wire and therefore not re-serializable after decode. The same PR clears a cargo audit failure on h2 0.4.15 for RUSTSEC-2026-0258.

snarkVM PR #3385 raises Puzzle::prove benchmark sampling to 100 samples with a 3 second warmup and 10 second measurement. A 940% regression alert had fired between two commits and did not reproduce locally, because a 200% gate on ten samples was measuring noise rather than the prover. Puzzle::check_solutions stays on the cheap path. Related, PR #3379 adds more maintainers to the benchmark alert CC list, which matters only if someone actually reads the alerts, which is the point.

The telemetry work from last week's deep dive is still moving. PR #4406 folds the telemetry feature into metrics and remains blocked on #4391, the single-owner worker that removes four Arc<RwLock<..>> fields from the BFT hot path.

Ecosystem and community

Provable published a builder writeup on Utila this week, covering the noncustodial MPC wallet platform that moves more than 15 billion dollars a month for over 400 institutions and now supports private stablecoin payments on Aleo. There was also a happy hour in New York with Dynamic and Fireblocks that drew more than a hundred people, and an Aleo evening in Vancouver on Friday, August 28, with a hands-on wallet and private stablecoin setup session.

Shield Swap, the confidential trading venue Provable opened to early access on August 17, is still in institutional beta with USDCx, wrapped Bitcoin, Ethereum, Solana, and ALEO. Public launch is targeted for Q4 2026.

On policy, the joint FinCEN, OCC, Federal Reserve, FDIC, and NCUA customer identification proposal for permitted payment stablecoin issuers closed its comment window on August 21 under docket FINCEN-2026-0101. Aleo filed and published its position on August 22. The proposal as drafted reaches only direct issuer relationships in the primary market, which means peer-to-peer wallet transfers sit outside it. The Bank Policy Institute and The Clearing House want that perimeter extended to secondary market platforms. The Blockchain Association argued for keeping it where it is. A final rule carries a 12 month implementation period, so nothing binds before 2027 at the earliest.

That fight bears directly on what Aleo builds. If identity obligations attach at the issuer boundary and not at every transfer, then selective disclosure at the edges with private transfers in the middle is the compliant architecture rather than the suspicious one.

Wider ZK signals

Two items from outside the Aleo repos caught my attention.

The EFF published an analysis by Daly Barnett on August 18 arguing that zero-knowledge proofs are "gameable, hackable, and not the cure-all some may claim" in the context of internet age verification. Read it even if you disagree with it. The argument is mostly about system boundaries rather than cryptography, and the boundary critique is fair: a proof that an attribute holds is worthless if the issuer that attested the attribute is compromised or coerced. That is the same class of concern as this week's snarkVM signing fix, one level up the stack.

Separately, Attestable came out of stealth on August 11 with a 20 million dollar seed co-led by Altimeter Capital and TLV Partners, proving which model ran on which inputs without revealing either, at a claimed 85 tokens per second on a single H100. For scale on the broader market, The Business Research Company puts zero-knowledge proofs at 1.73 billion dollars in 2026, up from 1.32 billion in 2025.

Looking ahead

Watch whether #4391 lands, since #4406 and a chunk of the locking cleanup queue sit behind it. On snarkVM, the interesting question is whether #3387's targeted rejection gets followed by the structural domain separation sketched in #3284, or whether the narrow check becomes the permanent answer. I would push for the former.

If you run a validator, the operational item this week is #4381. Search your logs for repeated TimedOut write errors against a stable peer address. Before this fix those connections never closed, so a history of them tells you which peers were quietly costing you write attempts all month.

Sources