<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Luis Soares on Medium]]></title>
        <description><![CDATA[Stories by Luis Soares on Medium]]></description>
        <link>https://medium.com/@luishrsoares?source=rss-5176d5a50f58------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*94RGU3zeMX78vwKHv4sqtQ.png</url>
            <title>Stories by Luis Soares on Medium</title>
            <link>https://medium.com/@luishrsoares?source=rss-5176d5a50f58------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 01:03:03 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@luishrsoares/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Your DAG Mempool Is a RAM Problem]]></title>
            <link>https://blog.blockmagnates.com/your-dag-mempool-is-a-ram-problem-a4ced4ac6da0?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/a4ced4ac6da0</guid>
            <category><![CDATA[rust-programming-language]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Sat, 18 Jul 2026 08:20:39 GMT</pubDate>
            <atom:updated>2026-07-20T14:11:44.086Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4iKrDE2CT4os2zwv8j2ENQ.png" /></figure><p>Lock-free traversal, epoch-based reclamation, and the memory engineering hiding inside modern consensus</p><p>There’s a moment in every DAG-based consensus implementation where the protocol paper stops helping you.</p><p>The paper tells you how Narwhal disseminates batches, how Bullshark orders them, how Mysticeti collapses certification into direct voting. What the paper does not tell you is what happens at round 40,000 when your validator is holding two million vertices in memory, three thread pools are hammering the same graph, and your p99 commit latency just tripled because the garbage collector took a lock at the wrong moment.</p><p>Traditional blockchains made this someone else’s problem. In a sequential chain, the bottleneck lives in network propagation or disk I/O — you gossip a block, you validate it, you append it, you mostly forget about it. The hot working set is small. The memory model is “append and forget.”</p><p>DAG-based protocols deliberately broke that model. By decoupling data dissemination from ordering, they moved the throughput ceiling off the network and onto the machine. Which means the battleground is now system RAM, cache lines, and CPU concurrency — and the difference between a validator that sustains 100k+ TPS and one that falls over under load is not the consensus rules. It’s memory engineering.</p><p>This article is about that layer. We’ll build up the actual in-memory architecture of a DAG node in Rust: the concurrent structures, the lock contention problem, epoch-based reclamation, watermark garbage collection, false sharing, atomic memory ordering, and the zero-copy discipline that keeps deserialization off the hot path. All code compiles against real crates (crossbeam-epoch, dashmap, bytes) and is representative of what production DAG nodes actually do.</p><h3>1. The Shift: From Sequential Blocks to a “Living” Graph</h3><h3>Append-and-forget vs. the interlocking web</h3><p>In a classical sequential chain, block <em>N+1</em> references exactly one parent: block <em>N</em>. Once a block is buried under enough confirmations, its in-memory representation is dead weight — you flush it to disk and evict it. The working set at any moment is the tip of the chain plus a mempool of loose transactions. A few hundred megabytes, comfortably.</p><p>A DAG mempool is a different organism. Every vertex (a “block” produced by one validator in one round) references <em>2f+1</em> parents from the previous round — one per validator in the quorum that it heard from. With a committee of, say, 100 validators, each round adds up to 100 new vertices, and each vertex holds ~67 parent references. The graph is not a line; it’s a dense, continuously growing lattice. And critically, it’s <em>live</em>: the consensus logic doesn’t just look at the tip — it repeatedly traverses causal histories backwards through the graph to decide commit order.</p><p>That traversal requirement is the whole game. You cannot evict a vertex just because it’s old; you can only evict it once the protocol <em>proves</em> it can never again influence a commit decision. Until that proof lands, the vertex must stay resident, reachable, and safely readable by multiple threads.</p><p>So the graph lives in RAM, it grows every 50–200ms, and everything reads it concurrently. Welcome to the problem.</p><h3>The three structures that must live in RAM</h3><p>Strip any production DAG node down and you find three core in-memory structures.</p><p><strong>The DAG map.</strong> The vertex metadata index — the skeleton of the graph. Keyed by (round, author) or by vertex digest, holding parent references, the author&#39;s signature, timestamps, and a <em>reference</em> to the payload (not the payload itself). This is the structure the consensus thread traverses, the ingress threads insert into, and the GC thread prunes. It is the single most contended piece of memory in the entire node.</p><p>rust</p><pre>use bytes::Bytes;<br>use std::collections::BTreeMap;<br><br>pub type Round = u64;<br>pub type AuthorId = u16;          // index into the committee, not a pubkey<br>pub type Digest = [u8; 32];<br><br>/// Metadata-only vertex. Note what is NOT here: the transactions.<br>pub struct Vertex {<br>    pub round: Round,<br>    pub author: AuthorId,<br>    pub digest: Digest,<br>    /// Digests of 2f+1 parents from round - 1.<br>    pub parents: Vec&lt;Digest&gt;,<br>    /// Digests of the batches this vertex certifies.<br>    /// The payloads live in the BatchStore, still serialized.<br>    pub batch_refs: Vec&lt;Digest&gt;,<br>    pub signature: [u8; 64],<br>    pub received_at_ns: u64,<br>}<br><br>/// The naive shape of the three structures. We will spend the rest<br>/// of this article tearing this apart and rebuilding it.<br>pub struct NaiveNodeState {<br>    /// (1) DAG map: the graph skeleton.<br>    pub dag: BTreeMap&lt;(Round, AuthorId), Vertex&gt;,<br>    /// (2) Batch store: digest -&gt; raw serialized payload bytes.<br>    pub batches: BTreeMap&lt;Digest, Bytes&gt;,<br>    /// (3) Pending transactions: submitted by clients, not yet<br>    /// pulled into a batch by our worker.<br>    pub pending: Vec&lt;Bytes&gt;,<br>}</pre><p><strong>The batch store.</strong> Raw transaction payloads, exactly as they arrived off the wire. In Narwhal’s architecture, workers stream batches of transactions and the primary only ever handles their digests. The payload bytes sit in the batch store as opaque Bytes buffers — deliberately <em>not</em> deserialized (we&#39;ll come back to why in Section 5). This is where the actual gigabytes live. A vertex is a few hundred bytes; the batches it references can be megabytes.</p><p><strong>The pending transactions map.</strong> The front door: client-submitted transactions that haven’t been sealed into a batch yet. It’s the smallest of the three but has the nastiest write pattern — thousands of small concurrent inserts per second from RPC handler threads, drained in bursts by the batch builder.</p><p>Do the arithmetic on the DAG map alone. 100 validators × 1 vertex per round × ~500 bytes of metadata × a retention window of 50,000 rounds before GC ≈ 2.5 GB of <em>metadata</em>, before a single transaction payload. Add batches at, say, 500 KB per vertex and the batch store dwarfs everything. The retention window is not a tuning knob you get to pick freely — it’s dictated by how far back the ordering rule may need to look, plus how far behind your slowest honest peer is allowed to lag before it must fall back to state sync.</p><h3>The threat: uncertified DAGs and the equivocation DoS</h3><p>Narwhal-style <em>certified</em> DAGs have a built-in throttle: a vertex only enters the DAG after 2f+1 validators sign it into a certificate, and honest validators sign at most one vertex per (round, author). A Byzantine validator who equivocates — producing two different vertices for the same round — simply can’t get both certified. The certificate layer acts as an admission filter for your memory.</p><p>Uncertified designs like Mysticeti removed that layer on purpose. Skipping explicit certification cuts latency dramatically — blocks go straight into the DAG and certification happens <em>implicitly</em> through the voting patterns of later blocks. The performance win is real. But look at what it does to your memory model: <strong>any validator can now inject vertices into your DAG map at wire speed, and equivocating vertices both land in RAM</strong> — because until the ordering logic resolves which one gets committed, you cannot know which is safe to drop.</p><p>A Byzantine validator exploiting this doesn’t need to break the safety of the protocol. It just needs to produce conflicting blocks as fast as its NIC allows and watch your heap grow. If your pruning is lazy, heuristic, or best-effort, this is an equivocation DoS: the attacker weaponizes your own retention logic against your allocator. The node doesn’t fork — it OOMs, which for a validator on a liveness clock is the same thing as being faulty.</p><p>The defense has to be <em>aggressive, deterministic pruning</em>: hard per-author caps on uncommitted vertices per round, and a garbage collection rule that is derived from protocol state — not from memory pressure. “Prune when RAM is 80% full” is how you turn a memory bug into a consensus divergence, because two honest nodes under different memory pressure would disagree about which vertices still exist. The GC boundary must be a pure function of committed state, identical on every honest node. We’ll formalize that as the watermark rule in Section 3.</p><p>But before we can <em>delete</em> anything safely, we need to survive <em>reading and writing</em> the graph concurrently. That’s the real nightmare.</p><h3>2. The Concurrency Nightmare: Traversal vs. Mutation</h3><h3>Who is touching the graph, and when</h3><p>Map the threads that hit the DAG map on a production node during a single 100ms window:</p><p><strong>Ingress threads (writers, many).</strong> One per peer connection, or a pool draining a network runtime. Each verified vertex from a peer becomes an insert into the DAG map. At 100 validators and sub-second rounds, that’s hundreds to thousands of inserts per second, arriving in bursts, from different threads.</p><p><strong>The consensus thread (reader, long-lived traversals).</strong> This is the thread that runs the ordering rule. In Bullshark terms: when a leader vertex gets enough support, walk its causal history — every vertex reachable through parent links that hasn’t been committed yet — topologically sort it, and emit the result as the next commit. That walk touches <em>thousands</em> of vertices spread across many rounds. It’s not a point lookup; it’s a graph traversal holding references to a large slice of the structure for hundreds of microseconds at a time.</p><p><strong>The GC thread (deleter).</strong> After a commit advances the low watermark, old vertices are garbage. Something has to actually remove them from the map and free their memory, ideally without pausing anyone.</p><p><strong>RPC/query threads (readers, point lookups).</strong> Peers ask for vertices they’re missing; local services inspect the graph. Random-access reads, all day.</p><p>So the access pattern is: many concurrent point-writes, plus a huge scanning reader, plus a deleter, all against one shared structure, with sub-millisecond latency budgets. This is close to a worst-case scenario for shared-memory concurrency — and it’s the <em>steady state</em> of a DAG node, not a corner case.</p><h3>The cost of just taking a lock</h3><p>The obvious first move is the standard-library one:</p><pre>use std::collections::HashMap;<br>use std::sync::RwLock;<br><br>pub struct LockedDag {<br>    inner: RwLock&lt;HashMap&lt;Digest, Vertex&gt;&gt;,<br>}<br><br>impl LockedDag {<br>    pub fn insert(&amp;self, v: Vertex) {<br>        // Every ingress thread serializes here.<br>        self.inner.write().unwrap().insert(v.digest, v);<br>    }<br>    pub fn causal_history(&amp;self, root: &amp;Digest) -&gt; Vec&lt;Digest&gt; {<br>        // Read lock held for the ENTIRE traversal.<br>        let dag = self.inner.read().unwrap();<br>        let mut stack = vec![*root];<br>        let mut visited = Vec::new();<br>        while let Some(d) = stack.pop() {<br>            if let Some(v) = dag.get(&amp;d) {<br>                visited.push(d);<br>                stack.extend(v.parents.iter().copied());<br>            }<br>        }<br>        visited // topological ordering elided for brevity<br>    }<br>}</pre><p>This is correct. It is also how you cap your node at a fraction of its potential throughput, for three compounding reasons.</p><p><strong>Reason 1: the traversal poisons the write path.</strong> causal_history holds the read lock for the full walk — potentially hundreds of microseconds over thousands of vertices. Under RwLock semantics, writers wanting the lock during that window either wait (writer starvation) or, with writer-priority implementations, <em>block all subsequent readers</em> to avoid starving — so one long traversal plus one queued writer stalls every RPC read on the node. Either way, your ingress pipeline develops a heartbeat: it freezes every time consensus thinks.</p><p><strong>Reason 2: contention turns parking into your workload.</strong> A contended lock isn’t just “wait your turn.” When a thread fails to acquire a contended Mutex/RwLock, after a brief spin it parks: a futex syscall, a context switch out (~1–5μs of direct cost, plus cache pollution that&#39;s often worse), and a context switch back when woken. Under heavy contention the wake-up itself is a stampede — several threads wake, one wins, the rest park again. Profile a hot locked node and you&#39;ll find a startling fraction of CPU time attributed not to hashing, not to signature verification, but to futex_wait and scheduler overhead. The machine is busy negotiating access to memory instead of using it.</p><p><strong>Reason 3: the critical section inflates under load.</strong> The more vertices in the map, the longer each traversal, the longer the lock is held, the deeper the writer queue, the more parked threads, the more context switches. Lock contention is superlinear in load. This is why locked designs benchmark fine at 5k TPS and collapse at 50k: you didn’t hit a wall, you climbed an exponential.</p><p>A quick illustrative microbenchmark shape (Criterion, 16 threads on a modern desktop CPU, 1M-entry map, 90/10 read/write mix with periodic full traversals) tells the story every time you run a variant of it:</p><p>StructurePoint ops/sec (aggregate)p99 traversal stall on writersRwLock&lt;HashMap&gt;low millionshundreds of μsMutex&lt;HashMap&gt;worseworseSharded locks (DashMap-style)~order of magnitude betterstill stalls on cross-shard scansLock-free reads + EBRtens of millions~none (readers never block)</p><p>Exact numbers vary by hardware and mix — run your own with Criterion before believing anyone’s table, including this one. The <em>shape</em> does not vary: the locked designs fall off a cliff exactly when a long reader coexists with hot writers. Which is the permanent condition of a DAG node.</p><p>Sharding the lock (what DashMap does — N shards, each its own RwLock) genuinely helps the point-op path and is a perfectly good answer for the <em>pending transactions map</em>, where operations are point inserts and drains. It does not save the DAG map, because the consensus traversal is inherently cross-shard: a causal history walk touches vertices in every shard, so you&#39;re back to either locking shards in sequence (inconsistent snapshot risk, deadlock discipline) or holding many shard locks at once (hello, convoy).</p><p>For the structure that gets traversed, you have to stop locking readers entirely. Which raises the question that makes this whole domain interesting: <strong>if readers never take locks, how does the GC thread ever know it’s safe to free memory?</strong></p><p>That question has a beautiful answer.</p><h3>3. Lock-Free Traversal and Epoch-Based Reclamation</h3><h3>Dropping the locks</h3><p>The property we need is precise: <strong>readers proceed at full speed with zero coordination, writers make progress without blocking readers, and memory is reclaimed only when provably unobserved.</strong> In the Rust ecosystem, this is the territory of crossbeam-epoch (the foundational epoch-based reclamation library), crossbeam-skiplist (a lock-free ordered map built on it — attractive for DAGs because (round, author) keys give you free range scans per round), and papaya (a modern lock-free hash map whose read path is wait-free and which is practically built for read-heavy workloads like ours).</p><p>Before reaching for any of them, understand the mechanism underneath, because the mechanism is where the engineering judgment lives.</p><p>The core problem of lock-free deletion: thread A is traversing the graph and holds a raw reference to vertex V. Thread GC decides V is garbage, removes it from the map, and frees it. Thread A dereferences freed memory. In C++ this is a use-after-free and a CVE; Rust&#39;s borrow checker can&#39;t save you either, because across threads with raw shared structures you&#39;re necessarily in unsafe territory (or trusting a crate that is). Locks solved this by <em>preventing the overlap</em>. Lock-free structures solve it by <em>permitting the overlap and deferring the free</em>.</p><h3>Epoch-based reclamation in one mental model</h3><p>EBR splits deletion into two phases, and the split is the entire trick:</p><p><strong>Logical unlink</strong> — atomically remove the vertex from the data structure (a compare-and-swap on the pointer that leads to it). After this instant, no <em>new</em> reader can find the vertex. Readers already holding a reference are unaffected: the memory is still valid, still readable. The vertex is unreachable-going-forward but alive.</p><p><strong>Physical deallocation</strong> — actually free the memory. This is <em>deferred</em> until the runtime can prove that every reader who could possibly have seen the old pointer has finished.</p><p>The proof mechanism is the epoch clock. A global epoch counter ticks upward. Every thread that wants to read the structure first <em>pins</em> itself — announcing “I am reading, as of epoch E” in a thread-local slot (a cheap operation: one atomic store, no contention, no syscall). When it finishes, it unpins. Garbage from a logical unlink is stamped with the epoch at which it was retired. The reclaimer’s rule: garbage retired in epoch E may be physically freed only once every currently-pinned thread has advanced past E. If all readers have observed a later epoch, then none of them can be holding a reference stamped E — they weren’t pinned when it was reachable, or they’ve since unpinned. QED, free the memory.</p><p>Readers pay one atomic operation to enter, one to leave, and <em>nothing per vertex visited</em>. A traversal over 10,000 vertices under a lock pays for the lock’s contention the entire time; under EBR it pays for two atomics total. That is the asymmetry that buys you your throughput back.</p><p>Here’s the mechanism made concrete — a deliberately minimal DAG map as an open-addressed index of atomic pointers, showing exactly where pinning, unlinking, and deferred destruction happen:</p><p>rust</p><pre>use crossbeam_epoch::{self as epoch, Atomic, Guard, Owned, Shared};<br>use std::sync::atomic::Ordering;<br><br>/// A vertex node as it lives inside the lock-free index.<br>pub struct VertexNode {<br>    pub vertex: Vertex,<br>    /// Intrusive next-pointer for hash-bucket chaining.<br>    next: Atomic&lt;VertexNode&gt;,<br>}<br><br>pub struct LockFreeDag {<br>    /// Fixed bucket array of lock-free chains. Production maps<br>    /// (papaya, crossbeam-skiplist) resize; the reclamation story<br>    /// is identical, so we keep the sketch honest and simple.<br>    buckets: Box&lt;[Atomic&lt;VertexNode&gt;]&gt;,<br>    mask: usize,<br>}<br><br>impl LockFreeDag {<br>    fn bucket(&amp;self, d: &amp;Digest) -&gt; &amp;Atomic&lt;VertexNode&gt; {<br>        let h = u64::from_le_bytes(d[0..8].try_into().unwrap());<br>        &amp;self.buckets[(h as usize) &amp; self.mask]<br>    }<br><br>    /// INGRESS PATH - lock-free insert (push-front, CAS loop).<br>    pub fn insert(&amp;self, vertex: Vertex) {<br>        let guard = epoch::pin();<br>        let bucket = self.bucket(&amp;vertex.digest);<br>        let mut node = Owned::new(VertexNode {<br>            vertex,<br>            next: Atomic::null(),<br>        });<br>        loop {<br>            let head = bucket.load(Ordering::Acquire, &amp;guard);<br>            node.next.store(head, Ordering::Relaxed);<br>            match bucket.compare_exchange(<br>                head, node,<br>                Ordering::Release,   // publish: vertex contents happen-before this<br>                Ordering::Acquire,<br>                &amp;guard,<br>            ) {<br>                Ok(_) =&gt; return,<br>                Err(e) =&gt; node = e.new, // lost the race; retry with our node back<br>            }<br>        }<br>    }<br><br>    /// CONSENSUS PATH - lock-free read. The caller&#39;s `guard` proves<br>    /// we&#39;re pinned; the returned reference is valid for the guard&#39;s<br>    /// lifetime even if GC logically unlinks the vertex meanwhile.<br>    pub fn get&lt;&#39;g&gt;(&amp;self, d: &amp;Digest, guard: &amp;&#39;g Guard) -&gt; Option&lt;&amp;&#39;g Vertex&gt; {<br>        let mut cur = self.bucket(d).load(Ordering::Acquire, guard);<br>        while let Some(node) = unsafe { cur.as_ref() } {<br>            if &amp;node.vertex.digest == d {<br>                return Some(&amp;node.vertex);<br>            }<br>            cur = node.next.load(Ordering::Acquire, guard);<br>        }<br>        None<br>    }<br><br>    /// GC PATH - logical unlink now, physical free later.<br>    pub fn remove(&amp;self, d: &amp;Digest, guard: &amp;Guard) -&gt; bool {<br>        let bucket = self.bucket(d);<br>        loop {<br>            // Find the node and its predecessor link.<br>            let mut prev_link = bucket;<br>            let mut cur = prev_link.load(Ordering::Acquire, guard);<br>            let target = loop {<br>                match unsafe { cur.as_ref() } {<br>                    None =&gt; return false, // not present<br>                    Some(n) if &amp;n.vertex.digest == d =&gt; break cur,<br>                    Some(n) =&gt; {<br>                        prev_link = &amp;n.next;<br>                        cur = prev_link.load(Ordering::Acquire, guard);<br>                    }<br>                }<br>            };<br>            let target_ref = unsafe { target.as_ref().unwrap() };<br>            let next = target_ref.next.load(Ordering::Acquire, guard);<br>            // (1) LOGICAL UNLINK: one CAS swings the predecessor past us.<br>            // From this instant, no new traversal can reach `target`.<br>            if prev_link<br>                .compare_exchange(target, next,<br>                    Ordering::AcqRel, Ordering::Acquire, guard)<br>                .is_ok()<br>            {<br>                // (2) DEFERRED PHYSICAL FREE: do NOT drop now - the<br>                // consensus thread may be mid-traversal holding &amp;target.<br>                // This registers the node as epoch-stamped garbage;<br>                // crossbeam frees it only after every thread pinned at<br>                // (or before) this epoch has unpinned.<br>                unsafe { guard.defer_destroy(target) };<br>                return true;<br>            }<br>            // CAS failed: a concurrent insert/remove touched this chain.<br>            // Retry from the bucket head. (A full production list uses<br>            // pointer tagging à la Harris to also handle a concurrent<br>            // remove of `prev` - see crossbeam-skiplist for the real thing.)<br>        }<br>    }<br>}</pre><p>And the consensus thread’s traversal — the thing that used to freeze the world — becomes:</p><pre>/// Walk the causal history of `root`. No locks, no writer stalls.<br>/// Cost of safety: one pin at entry, one unpin at drop. That&#39;s it.<br>pub fn causal_history(dag: &amp;LockFreeDag, root: Digest) -&gt; Vec&lt;Digest&gt; {<br>    let guard = epoch::pin();                    // one atomic op<br>    let mut stack = vec![root];<br>    let mut out = Vec::with_capacity(4096);<br>    while let Some(d) = stack.pop() {<br>        if let Some(v) = dag.get(&amp;d, &amp;guard) {   // zero coordination per hop<br>            out.push(d);<br>            stack.extend(v.parents.iter().copied());<br>        }<br>        // A miss here is fine and expected: the parent was GC&#39;d below<br>        // the watermark, which — as we&#39;re about to prove — means it<br>        // was already committed and cannot matter to this decision.<br>    }<br>    out<br>}   // guard drops -&gt; unpin -&gt; our epoch no longer restrains the reclaimer</pre><p>One discipline the code enforces implicitly and your team must enforce culturally: <strong>guards bound reclamation latency.</strong> A pinned thread freezes the epoch for <em>everyone’s</em> garbage. Hold a guard across an .await point or a disk write and you&#39;ve built a memory leak with extra steps — retired vertices pile up in the epoch bags, unfree-able, while your allocator wonders what happened. Pin, traverse, drop. Guards are for memory access, never for I/O.</p><h3>Watermark garbage collection: when is removal allowed?</h3><p>EBR answers “how do we free safely.” It deliberately does not answer “<em>what</em> are we allowed to remove.” That answer must come from the protocol, and it must be deterministic — recall the equivocation-DoS lesson: if pruning depends on local conditions, honest nodes diverge.</p><p>The standard construction is a pair of watermarks derived purely from committed state:</p><p><strong>Low watermark (</strong><strong>gc_round).</strong> Defined as latest_committed_round - GC_DEPTH, where GC_DEPTH is a protocol constant. The protocol guarantees the ordering rule will never again need to <em>read</em> any vertex at or below this round. Why can it guarantee that? Because commits in Bullshark-family protocols finalize causal histories: once a leader at round R is committed, every vertex in its causal history is either committed with it or was skipped by the deterministic ordering rule — and every <em>future</em> commit decision depends only on vertices above the already-ordered prefix. GC_DEPTH adds slack for votes and support links that reference slightly older rounds. Below gc_round, a vertex is either ordered (its effects are in the output stream, durably logged) or dead (unreachable from anything that can still be committed). Either way: prunable, and <em>identically prunable on every honest node</em>, because it&#39;s a pure function of the commit sequence they all agree on.</p><p><strong>High watermark.</strong> The admission bound on the other side: reject or park vertices too far <em>above</em> your latest round (a peer racing ahead, or a Byzantine author fabricating future rounds). Without it, an attacker streams round-1-billion vertices into your map and your GC — which only ever advances from the bottom — never touches them. The two watermarks together turn the DAG map from an unbounded structure into a sliding window: admission caps the top, commits advance the bottom, and maximum resident memory becomes committee_size × window_height × avg_vertex_footprint — a number you can compute, provision, and alert on. If you cannot write that formula for your node, you do not have a memory model; you have a hope.</p><p>The GC thread then writes itself:</p><p>rust</p><pre>use std::sync::atomic::{AtomicU64, Ordering};<br><br>pub struct Watermarks {<br>    /// Highest committed round, advanced by the consensus thread.<br>    pub committed: AtomicU64,<br>    /// Everything at or below this round is prunable.<br>    pub gc_round: AtomicU64,<br>}<br><br>pub const GC_DEPTH: u64 = 50;<br>impl Watermarks {<br>    /// Called by the consensus thread after each commit.<br>    pub fn on_commit(&amp;self, round: Round) {<br>        self.committed.fetch_max(round, Ordering::AcqRel);<br>        self.gc_round<br>            .fetch_max(round.saturating_sub(GC_DEPTH), Ordering::Release);<br>    }<br>}<br><br>/// The GC loop: protocol decides WHAT (watermark),<br>/// EBR decides WHEN (epoch proof). Never confuse the two.<br>pub fn gc_pass(<br>    dag: &amp;LockFreeDag,<br>    per_round_index: &amp;RoundIndex,   // (round -&gt; digests), maintained on insert<br>    wm: &amp;Watermarks,<br>    last_pruned: &amp;mut Round,<br>) {<br>    let horizon = wm.gc_round.load(Ordering::Acquire);<br>    while *last_pruned &lt; horizon {<br>        *last_pruned += 1;<br>        let guard = epoch::pin();<br>        for digest in per_round_index.drain_round(*last_pruned) {<br>            // Logical unlink now; crossbeam frees the memory only<br>            // once no pinned traversal can still observe it.<br>            dag.remove(&amp;digest, &amp;guard);<br>        }<br>        drop(guard);<br>        // Re-pin per round: keeps our own pin short so we never<br>        // become the thread that stalls global reclamation.<br>    }<br>}</pre><p>Notice the layering, because it’s the actual design insight of this whole section: <strong>the watermark is a consensus-level proof that no <em>future logical</em> operation needs the vertex; the epoch is a memory-level proof that no <em>current physical</em> reader holds it.</strong> Logical safety and physical safety, separately proven, composed. Skip the first and you break the protocol. Skip the second and you break the heap. Production incidents in this class of system are almost always one of these two proofs being assumed instead of established.</p><h3>4. Memory Alignment and Atomics: Fighting the Hardware</h3><p>Dropping locks removes <em>software</em> contention. Your reward is a clearer view of the <em>hardware</em> contention that was hiding underneath. Two topics dominate: false sharing and memory ordering.</p><h3>False sharing: contention without sharing</h3><p>CPU cores don’t read bytes from memory; they read cache lines — 64 bytes at a time on x86–64 and most ARM server parts. Cache coherence (MESI and friends) operates on whole lines: when core A writes to a line, every other core’s copy of that <em>entire line</em> is invalidated, forcing a re-fetch on next access.</p><p>Now look at an innocently-written metrics struct sitting next to your DAG:</p><pre>// LOOKS harmless. Benchmarks like a lock.<br>pub struct DagMetrics {<br>    pub vertices_ingested: AtomicU64,   // written by ingress threads<br>    pub vertices_committed: AtomicU64,  // written by consensus thread<br>    pub vertices_pruned: AtomicU64,     // written by GC thread<br>    pub bytes_resident: AtomicU64,      // written by GC thread<br>}</pre><p>Four AtomicU64s is 32 bytes — all four counters share one cache line. Every time an ingress thread bumps vertices_ingested, it yanks the line away from the consensus thread&#39;s core in exclusive state; when consensus bumps vertices_committed, it yanks it back. The threads <em>never touch the same variable</em>, yet the coherence protocol makes them fight as if they did. This is false sharing: hardware-level contention between logically independent data, and it happily survives your migration to lock-free code — atomics <em>are</em> the mechanism it feeds on. In profiles it shows up as mysteriously high cycles on lock xadd instructions and elevated L2/L3 miss counters, with no lock in sight.</p><p>The fix is padding each independently-written hot variable to its own cache line, which crossbeam packages as CachePadded:</p><pre>use crossbeam_utils::CachePadded;<br><br>pub struct DagMetrics {<br>    pub vertices_ingested: CachePadded&lt;AtomicU64&gt;,  // own 64B line<br>    pub vertices_committed: CachePadded&lt;AtomicU64&gt;, // own 64B line<br>    pub vertices_pruned: CachePadded&lt;AtomicU64&gt;,    // own 64B line<br>    pub bytes_resident: CachePadded&lt;AtomicU64&gt;,     // own 64B line<br>}</pre><p>The same reasoning applies <em>inside</em> your vertex node. Recall VertexNode: it holds cold immutable metadata (digest, parents, signature) and a hot mutable link (next, CAS-target during inserts and unlinks). If a frequently-CAS&#39;d atomic shares a line with fields the consensus thread is reading during traversal, every ingress CAS invalidates the traversal&#39;s cached copy of the metadata next to it. Order fields by temperature and align the boundaries:</p><pre>#[repr(C)] // field order is a promise, not a suggestion<br>pub struct VertexNode {<br>    // --- Cold, read-mostly: packed together, shared read-only lines<br>    //     replicate freely across every core&#39;s cache. ---<br>    vertex: Vertex,<br>    // --- Hot, CAS-contended: quarantined on its own line so writer<br>    //     traffic can&#39;t invalidate the metadata above. ---<br>    next: CachePadded&lt;Atomic&lt;VertexNode&gt;&gt;,<br>}</pre><p>You pay for this in memory (padding is real bytes — pad the per-instance-thousands structures judiciously, pad the global singletons freely) and you verify it empirically: perf c2c on Linux exists precisely to catch cache-line ping-pong, and it will show you the exact addresses fighting.</p><h3>Memory ordering: buying exactly as much synchronization as you need</h3><p>Every atomic operation in Rust takes an Ordering, and the choice is a price menu. SeqCst buys a global total order over all SeqCst ops — almost never needed, and on weakly-ordered ARM it costs full barriers. Relaxed buys atomicity only — the operation happens indivisibly, but with no promises about when <em>other</em> memory writes become visible around it. In between sits the pair that runs the world&#39;s fast concurrent code: Release on the writer, Acquire on the reader.</p><p>The contract: everything written <em>before</em> a Release store is visible <em>after</em> the Acquire load that observes it. It&#39;s a one-way synchronization edge between exactly two participants — precisely shaped like a hand-off.</p><p>And a DAG node is <em>made of</em> hand-offs. The defining one is ingress-to-consensus: the ingress thread fills a VertexNode (plain writes: digest, parents, signature — no atomics needed, nobody can see it yet) and then <em>publishes</em> it with one CAS on the bucket head. That publication is the moment the vertex legally exists for other threads</p><pre>// INGRESS (writer) — inside LockFreeDag::insert:<br>//   plain writes to node.vertex fields ...        (1)<br>//   bucket.compare_exchange(head, node,<br>//       Ordering::Release, ...)                    (2) publish<br>// CONSENSUS (reader) - inside LockFreeDag::get:<br>//   let cur = bucket.load(Ordering::Acquire, g);   (3) observe<br>//   ... read node.vertex.parents ...               (4)</pre><p>The Release at (2) paired with the Acquire at (3) guarantees that if the consensus thread sees the new head, it also sees every field written at (1) — fully, and in a consistent state. Weaken (2) to Relaxed and that guarantee evaporates: on x86 you&#39;ll probably get away with it (the ISA&#39;s strong ordering bails you out, silently, until the day you deploy on Graviton); on ARM the consensus thread can legitimately observe the pointer <em>before</em> the bytes it points to — a torn vertex, garbage parents, and a consensus divergence that reproduces once a week under load. Memory-ordering bugs are the only bug class where &quot;it passed on my machine&quot; is <em>architecturally</em> meaningless.</p><p>Where is Relaxed correct, then? Wherever you need atomicity without communication. The metrics counters above: nobody makes a control-flow decision inside a synchronization window based on vertices_ingested; it&#39;s scraped by an exporter eventually. fetch_add(1, Ordering::Relaxed) is the honest price. The watermark, by contrast, <em>is</em> a communication channel — GC reads it to decide what to destroy — so it runs Release/Acquire, as you saw in Watermarks::on_commit.</p><p>The discipline that scales: for every atomic in the codebase, be able to answer <em>“who is the writer, who is the reader, and what non-atomic data rides along with the signal?”</em> If data rides along → Release/Acquire. If nothing rides along → Relaxed, and a comment saying why. If you’re reaching for SeqCst, you’re usually confessing you haven’t answered the question.</p><h3>5. The Zero-Copy Serialization Tax</h3><p>There’s one more place throughput quietly dies, and it’s not contention — it’s copying and parsing bytes that nobody asked you to parse yet.</p><p>Follow a batch through a naive node: bytes arrive from the socket into a buffer; you deserialize the whole batch into Vec&lt;Transaction&gt; (allocating per transaction, copying every field); you hash it to verify the digest; you store the Vec. Later, consensus commits the vertex referencing it, and the execution layer iterates the transactions. Total: one full deserialization, thousands of small allocations, and multiple copies — for data whose only <em>consensus-relevant</em> property was its digest. The ordering layer of a DAG protocol never looks inside a transaction. It orders opaque bytes. Deserializing at ingress is doing the application layer&#39;s work, at the worst possible time, on the hottest path, for every batch <em>including the ones an equivocation check will throw away</em>.</p><p>The high-throughput discipline is <strong>deferred deserialization</strong>: parse exactly the envelope the current layer needs, keep everything else as reference-counted bytes, and let the payload cross the entire consensus pipeline untouched.</p><p>bytes::Bytes is the enabling primitive: a cheaply-cloneable, reference-counted, sliceable view into a buffer. slice() hands out a new Bytes pointing into the <em>same allocation</em> — no copy, just refcount arithmetic. So ingress parses the fixed-size header manually, slices the payload, and moves on:</p><pre>use bytes::{Buf, Bytes};<br><br>/// Wire layout (fixed header, then opaque payload):<br>///   [round: u64][author: u16][parent_count: u16]<br>///   [parents: 32B × parent_count][payload_len: u32][payload...]<br>pub struct WireVertex {<br>    pub round: Round,<br>    pub author: AuthorId,<br>    pub parents: Vec&lt;Digest&gt;,   // small, needed by consensus: parse it<br>    pub payload: Bytes,         // large, NOT needed yet: don&#39;t touch it<br>}<br><br>pub fn parse_envelope(mut buf: Bytes) -&gt; Result&lt;WireVertex, ParseError&gt; {<br>    if buf.len() &lt; 12 { return Err(ParseError::Truncated); }<br>    let round = buf.get_u64_le();<br>    let author = buf.get_u16_le();<br>    let parent_count = buf.get_u16_le() as usize;<br>    if parent_count &gt; MAX_PARENTS { return Err(ParseError::TooManyParents); }<br>    let mut parents = Vec::with_capacity(parent_count);<br><br>    for _ in 0..parent_count {<br>        if buf.len() &lt; 32 { return Err(ParseError::Truncated); }<br>        let mut d = [0u8; 32];<br>        buf.copy_to_slice(&amp;mut d);   // 32 bytes, fine<br>        parents.push(d);<br>    }<br><br>    if buf.len() &lt; 4 { return Err(ParseError::Truncated); }<br>    let payload_len = buf.get_u32_le() as usize;<br>    if buf.len() &lt; payload_len { return Err(ParseError::Truncated); }<br><br>    // THE moment of restraint: a refcount bump, not a memcpy.<br>    // These bytes will not be deserialized by any consensus code path.<br>    let payload = buf.slice(..payload_len);<br><br>    Ok(WireVertex { round, author, parents, payload })<br>}</pre><p>Everything downstream cooperates with the restraint. Digest verification hashes &amp;payload[..] directly — hashing doesn&#39;t require deserialization. The batch store maps Digest -&gt; Bytes — inserting is a refcount bump. Serving a peer&#39;s fetch request writes the same Bytes back to a socket — with writev/vectored I/O, the payload can travel NIC-to-RAM-to-NIC without ever being <em>understood</em>. The transactions get deserialized exactly once, in exactly one place: the execution layer, after commit, off the consensus hot path — and for batches that never commit (equivocations, skipped leaders), <em>never at all</em>. The attacker who floods you with garbage batches pays for your hash function, not your allocator.</p><p>Two honest costs, because zero-copy is a tax trade, not a free lunch. First, <strong>validation moves later</strong>: a batch full of malformed transactions passes ingress (its digest is valid!) and fails only at execution — your state machine must treat “committed batch contains garbage” as a defined, deterministic outcome (typically: skip the bad transaction, identically on every node), not a panic. Second, <strong>Bytes pins its backing buffer</strong>: a 4 MB network buffer stays fully resident as long as any 200-byte slice of it is alive, so batch-store eviction at the GC watermark must drop the <em>last</em> reference or your &quot;freed&quot; memory is an accounting fiction. Frameworks like rkyv push the same idea further — access serialized data in place through validated typed views, no deserialization step at all — worth evaluating once your envelope grows beyond what hand-parsing keeps auditable.</p><p>The principle generalizes past serialization and is worth stating as the design rule it is: <strong>at every layer, touch the minimum bytes the layer’s decision requires.</strong> Consensus decides ordering → it touches references, rounds, authors, digests. Execution decides state → it alone touches transaction internals. Every byte parsed above its layer of need is latency donated to no one.</p><h3>Conclusion: The Architecture Is the Code</h3><p>Walk back through what actually got decided in this article, because none of it was syntax.</p><p>We decided that in an uncertified DAG, pruning is a <em>consensus obligation</em>, not a memory optimization — and that any GC rule not derivable from committed state is a fork generator. We decided that a structure which is simultaneously point-written, range-traversed, and pruned cannot be guarded by locks at mainnet load, and paid for that decision with the full complexity of epoch-based reclamation: pin discipline, two-phase deletion, the separation of logical unlink from physical free. We decided which cache lines our threads are allowed to fight over, and spent real bytes of padding to end wars between counters that never shared data. We priced every atomic on the Release/Acquire menu instead of buying SeqCst insurance. And we decided that transaction bytes are opaque until execution — accepting later validation and pinned buffers as the fee for never copying what we don’t read.</p><p>Any one of these choices, taken alone, is a paragraph in a design doc. Taken together they <em>are</em> the system. Two teams can implement the same Bullshark paper, pass the same conformance tests, and land an order of magnitude apart in sustained TPS — not because one team writes better Rust line-by-line, but because one team made these calls deliberately and the other inherited defaults.</p><p>That’s the uncomfortable, liberating truth about performance engineering in 2026: the code itself is increasingly a commodity. An LLM will write you a correct RwLock&lt;HashMap&gt; DAG in thirty seconds, and it will be <em>fine</em> — right up until the round where it isn&#39;t, and nothing in the compiler, the type system, or the test suite will tell you why. Knowing that the watermark proof and the epoch proof are different proofs; knowing that your ARM deployment will surface the Relaxed publish that x86 forgave; knowing that the 50,000 TPS you&#39;re missing is sitting in futex_wait and a shared cache line — that knowledge doesn&#39;t ship in the model weights or the crate docs.</p><p>Follow me on <a href="https://x.com/luishsoares">X</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a4ced4ac6da0" width="1" height="1" alt=""><hr><p><a href="https://blog.blockmagnates.com/your-dag-mempool-is-a-ram-problem-a4ced4ac6da0">Your DAG Mempool Is a RAM Problem</a> was originally published in <a href="https://blog.blockmagnates.com/">Block Magnates</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[There Is No “Best” Cryptography: How Primitive Choices Define Your L1]]></title>
            <link>https://medium.com/coinmonks/there-is-no-best-cryptography-how-primitive-choices-define-your-l1-fc0db36544d6?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/fc0db36544d6</guid>
            <category><![CDATA[cryptography]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[software-architecture]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Sat, 27 Jun 2026 11:20:02 GMT</pubDate>
            <atom:updated>2026-06-29T07:47:03.546Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CYXkX9V9sXzSd48hcnwnTA.png" /></figure><h3>Hashing dictates the shape of your state. Signatures bound your validator set. Commitment schemes set your on-chain footprint. None of these are checkboxes — they are architecture, and every one of them is a trade-off you will live with for the life of the protocol.</h3><p>Ask an engineer new to protocol work which hash function or signature scheme is “the best” and you will get a confident answer. Ask someone who has shipped an L1 the same question and you will get a much longer pause, followed by the only honest reply: <em>it depends on what you’re optimizing for, and on what you’re willing to give up to get it.</em></p><p>That pause is the whole job.</p><p>Cryptographic primitives feel like settled, low-level building blocks — the kind of thing you pick once, import from a crate, and stop thinking about. They are not. The hash function you choose for your state trie determines how cheaply you can prove anything about that state later. The signature scheme your consensus uses sets a hard ceiling on how many validators your network can have. The polynomial commitment scheme behind your proofs decides whether verifying them on-chain costs a rounding error or a fortune in gas. These choices ripple outward through your data structures, your block propagation, your finality latency, your bridge design, and your fee market.</p><p>And critically, they conflict with each other. A primitive that is brilliant on one axis is almost always mediocre or terrible on another. The engineering is not in finding the best cryptography. It is in mapping your protocol’s <em>actual</em> dominant cost to the right corner of a multidimensional trade-off space — and accepting the bill everywhere else.</p><p>This piece walks that space.</p><h3>The constraint space</h3><p>Before talking about specific primitives, it helps to name the axes you are trading against. Every primitive you might pick occupies a point in roughly this space:</p><ul><li><strong>Native execution cost</strong> — cycles burned on a CPU to compute the thing. Matters for raw throughput: every full node runs this on every transaction.</li><li><strong>In-circuit cost</strong> — the number of constraints the primitive expands to inside an arithmetic circuit. This is the ZK axis, and it is <em>completely decoupled</em> from native cost. A primitive can be cheap on a CPU and ruinous in a SNARK.</li><li><strong>Size on the wire / in state</strong> — bytes per signature, per commitment, per proof. This is bandwidth during gossip and bloat in your state.</li><li><strong>Verifier cost</strong> — what it takes someone else (often a smart contract paying gas) to check the artifact. Frequently the single most important number for an L1.</li><li><strong>Setup and trust assumptions</strong> — does the scheme need a trusted setup ceremony, or is it transparent?</li><li><strong>Quantum resistance</strong> — is the security assumption broken by a sufficiently large quantum computer?</li><li><strong>Ecosystem entrenchment</strong> — how much existing tooling, wallet support, and standardization comes free if you pick the conventional option, and how much you forfeit if you don’t.</li></ul><p>Optimizing one axis pessimizes another. That is not a flaw in the cryptography; it is the structure of the field. Hold this picture in mind, because every section below is just a different slice through it.</p><h3>Hashing: the primitive that shapes your state</h3><p>Start with hashing, because it touches everything. Your hash function determines the structure of your Merkle tree or Patricia trie, the cost of computing a state root every block, and the cost of producing and verifying inclusion proofs. Ethereum’s choice of Keccak-256 propagates into the exact shape and gas cost of every storage operation on the network.</p><p>For years the only thing that mattered was native speed and collision resistance, and the answer was easy: SHA-256, Keccak, BLAKE3 — all fast, all hardware-friendly, all battle-tested. SHA-256 in particular benefits from dedicated CPU instructions; it is about as cheap as serious cryptography gets.</p><p>Then ZK changed the question. The moment you want to <em>prove</em> a statement about your state inside a SNARK — “this account had this balance,” “this transaction was included” — your hash function stops running on a CPU and starts running inside an arithmetic circuit over a prime field. And here the traditional hashes fall apart. Keccak and SHA-256 are built from bitwise operations: XORs, rotations, ANDs. Those are nearly free on a processor and astronomically expensive in a circuit, where every bit operation has to be re-expressed as field arithmetic. A single Keccak invocation can cost tens of thousands of constraints. Hash a Merkle path of any depth and your proving time detonates.</p><p>This is why algebraic hashes exist. Poseidon, Rescue, MiMC, and their relatives are designed from the field up to minimize multiplicative complexity inside a circuit. A Poseidon hash can be orders of magnitude cheaper in-circuit than Keccak. The catch — and there is always a catch — is the inverse trade-off:</p><ul><li>They are <strong>slower on a native CPU</strong>, sometimes dramatically, because the algebraic structure that makes them circuit-friendly makes them work harder outside one.</li><li>They are <strong>younger and less cryptanalyzed</strong>. Keccak has survived two decades of public attack. The newer algebraic hashes have survived far less, and some early parameter choices in the family have needed revision.</li></ul><p>So the decision “which hash secures my state trie” is, whether you realize it or not, a decision about how cheaply you will be able to prove things about that state for the rest of the protocol’s life. A chain that never touches ZK should take the fast, mature, hardware-accelerated option without hesitation. A chain built around succinct proofs of its own state has to pay the native-execution and maturity tax of an algebraic hash, or pay a far larger proving tax forever. There is no option that is cheap in both places. That is the first hard trade-off, and it sets the tone for all the others.</p><h3>Signatures: the primitive that bounds your validator set</h3><p>If hashing shapes your state, the signature scheme bounds your consensus. This is the least appreciated lever in protocol design, and arguably the one with the largest architectural blast radius.</p><p>The conventional choices optimize for compatibility and native speed:</p><ul><li><strong>ECDSA over secp256k1</strong> is the incumbent — Bitcoin, Ethereum accounts, virtually every wallet and hardware signer. Its superpower is pure ubiquity. Its weaknesses are real: signatures are not aggregatable, the scheme has historical malleability quirks, and it is painful to verify inside a circuit.</li><li><strong>EdDSA over Ed25519</strong> is the modern default for new chains that don’t need EVM account compatibility — deterministic, fast, with clean batch verification. Better engineering than ECDSA on almost every axis except entrenchment.</li><li><strong>Schnorr</strong> brings linearity, which unlocks key and signature aggregation through protocols like MuSig — the foundation of Bitcoin’s Taproot.</li></ul><p>But the primitive that genuinely changes what an L1 <em>can be</em> is <strong>BLS</strong>.</p><p>BLS signatures, built on pairing-friendly curves, have one property that the others lack at scale: native aggregation. You can take <em>n</em> signatures over the same message and combine them into a single signature, and take the <em>n</em> corresponding public keys and combine them into a single aggregate key. Verification then collapses into essentially one check, regardless of how many signers participated.</p><pre>// n signatures over the same message  →  one signature.<br>// n public keys                        →  one aggregate key.<br>// Consensus then verifies once, not n times.<br>let agg_sig = signatures.iter().copied().sum::&lt;Signature&gt;();<br>let agg_pk  = pubkeys.iter().copied().sum::&lt;PublicKey&gt;();</pre><pre>// A single pairing check stands in for verifying every attestation:<br>assert!(verify(agg_pk, message, agg_sig));</pre><p>This is not a micro-optimization. It is the reason a proof-of-stake network can have hundreds of thousands of validators all attesting every epoch without drowning in signature data. Without aggregation, each slot would carry a flood of independent signatures to gossip, store, and individually verify; bandwidth and verification cost would scale linearly with the validator count and the validator count would have to stay small. Aggregation breaks that coupling. The size of your validator set — and therefore a meaningful component of your decentralization story — is downstream of choosing an aggregatable signature scheme.</p><p>Naturally, the bill comes due elsewhere. BLS verification rests on <strong>pairing operations</strong>, which are heavy — far more expensive per check than an Ed25519 verification. The security assumptions are newer than ECDSA’s. And the scheme requires real care against rogue-key attacks, typically handled with proof-of-possession during validator registration. You trade cheap, simple, well-understood per-signature verification for expensive, subtle verification that aggregates beautifully. For a high-validator-count consensus layer, that trade is obviously worth it. For a chain that just needs users to sign transactions, it is obviously not. Same primitive, opposite verdicts — because the dominant cost is different.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vdxiJ0YB6xItsnAcdf5-zQ.png" /></figure><h3>Curves and pairings: the foundation nobody sees</h3><p>Underneath the signatures and most of the proof systems sits the elliptic curve, and the choice there quietly constrains everything above it.</p><p>The workhorse curves — secp256k1, Curve25519 — are fast and well-understood but do not support the bilinear pairings that BLS and many SNARKs need. For pairings you need a <em>pairing-friendly</em> curve, and that brings its own history lesson worth internalizing.</p><p>BN254 (also called alt-bn128) was the early favorite, with Groth16 proofs and an EVM precompile built around it. It was originally estimated at roughly 128-bit security. Then improved attacks on the underlying problem — the extended tower number field sieve and related work — forced the community to revise that estimate sharply downward, with conservative analyses landing well under 100 bits. That is a sobering reminder that the security level of a primitive is an <em>estimate against the best known attack</em>, not a constant, and that estimate can move under you.</p><p>The response was BLS12–381, engineered over a larger field to restore a comfortable ~128-bit margin, and it is now the de facto standard for consensus signatures and for KZG-based commitments. The cost is that operations over the larger field are slower, and verifying them somewhere they aren’t native — say, inside the EVM — is genuinely expensive.</p><p>Which is exactly where interoperability enters. If your chain ever wants to verify another chain’s BLS signatures or pairing-based proofs on-chain (light clients, bridges, restaking, cross-chain messaging), the curve choice on <em>both</em> sides drives the cost. Verifying secp256k1 recovery in the EVM is a cheap, native precompile. Verifying BLS12–381 operations in the EVM was, until recently, so costly in hand-rolled contract code as to be impractical — which is precisely why Ethereum’s Pectra upgrade shipped EIP-2537, a native precompile for BLS12–381 operations. That single precompile is what makes affordable on-chain verification of validator signatures, ZK proofs, and cross-chain attestations realistic. The lesson: when a primitive is too expensive in your VM, the escape hatch is to push it down into a precompile — and the <em>absence</em> of the right precompile silently dictates which interoperability designs are viable and which are not.</p><h3>Commitments and proof systems: the primitive that sets your on-chain footprint</h3><p>For any chain leaning on succinct proofs — rollups, privacy systems, validity bridges — the polynomial commitment scheme is the primitive that most directly determines on-chain cost. This is the richest trade-off space of all, and it is governed by a tension between four things: trusted setup, proof size, verifier cost, and quantum resistance. You cannot have all of them.</p><p><strong>KZG (Kate) commitments</strong> give you constant-size proofs — a few hundred bytes regardless of the statement — and very fast verification. This is why PLONK-family proofs are so compact (on the order of a few hundred bytes) and why Ethereum chose KZG for the blob commitments in EIP-4844 proto-danksharding. The price is a <strong>trusted setup</strong>: someone has to run a powers-of-tau ceremony, and the security rests on at least one participant having destroyed their secret. The community now mitigates this with enormous multi-party ceremonies, but the assumption never fully disappears. KZG also relies on pairings, inheriting the curve costs above.</p><p><strong>FRI</strong>, the machinery behind STARKs, takes the opposite stance. It needs <strong>no trusted setup</strong> — it is transparent — and because it is built only on collision-resistant hashing and Reed-Solomon proximity testing, it is plausibly <strong>post-quantum</strong>. The cost is proof size: STARK proofs run from tens to hundreds of kilobytes, far larger than a KZG opening. And because FRI leans so heavily on hashing, you are pushed straight back into the algebraic-hash trade-off from the first section — your prover’s performance now depends on a Poseidon-style hash being cheap in-circuit.</p><p><strong>Inner-product arguments (IPA)</strong>, as used in Bulletproofs and in Halo2’s original configuration, also need no trusted setup and avoid pairings, with logarithmic proof sizes. The catch lives in the verifier, whose cost grows with the circuit; the answer is recursion and accumulation, folding many proofs into one so the cost amortizes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b1Hsh3jfqymnGgJiOo-nFw.png" /></figure><p>What makes this space especially interesting for a protocol engineer is that the proof <em>frontend</em> and the commitment <em>backend</em> are increasingly decoupled. PLONKish arithmetization — the constraint model popularized by Halo2 — can sit on top of different commitment schemes. The same circuit description can be proven with a transparent, recursion-friendly IPA backend <em>or</em> with a KZG backend that yields tiny proofs and fast verification at the cost of a setup ceremony. That flexibility is itself the point of this article in miniature: the arithmetization is a fixed design decision, but the commitment scheme bolted underneath it is a <em>tunable trade-off</em> between proof size, verifier cost, and trust assumptions, and you choose it based on whether your dominant constraint is on-chain verification gas or ceremony-free transparency.</p><h3>The interoperability tax</h3><p>Step back and a pattern emerges. The “optimal” primitive in isolation and the “correct” primitive for a real network are frequently different things, and the gap between them is the cost of fitting into an ecosystem.</p><p>EVM compatibility is the sharpest example. The moment you commit to being EVM-compatible — and many new L1s do, because it inherits an entire universe of tooling, wallets, and developer muscle memory — you inherit Keccak-256 as your hash and secp256k1 as your account signature scheme. Both are excellent native primitives and both are hostile to ZK. If you <em>also</em> want succinct proofs or privacy features, you are now reconciling a ZK-unfriendly mandated boundary with a ZK-friendly internal design. The realistic answer is hybrid: keep Keccak and secp256k1 at the compatibility surface where the ecosystem demands them, and use algebraic hashes and circuit-friendly schemes for the internal machinery that actually gets proven. You pay for the seam, but you keep the ecosystem.</p><p>This is the deepest version of the trade-off. It is not even cryptographic — it is economic and social. The best primitive on a whiteboard loses to the adequate primitive that ten thousand developers already know how to use. Judgment here means knowing when conformance is worth more than optimality, and when it is worth breaking from the herd and paying the integration cost yourself.</p><h3>The quantum horizon</h3><p>One axis is easy to defer and dangerous to forget: quantum resistance. Most primitives in wide use today — ECDSA, EdDSA, BLS, KZG, anything resting on discrete-log or pairing assumptions — fall to a sufficiently capable quantum computer. The post-quantum replacements exist and are standardized: ML-DSA (Dilithium) and SLH-DSA (SPHINCS+) for signatures, ML-KEM (Kyber) for key encapsulation. The reason almost no L1 has switched is, predictably, a trade-off: post-quantum signatures are large. A Dilithium signature runs to a couple of kilobytes against Ed25519’s 64 bytes, and dropping that into every transaction is a brutal hit to state size and bandwidth.</p><p>So the live design decision today is rarely “switch now.” It is <strong>crypto-agility</strong>: architecting the protocol so the signature scheme, hash function, or commitment scheme can be <em>replaced</em> through governance without a hard fork that rewrites the world. The transparent, hash-based proof systems get a quiet head start here because their security already rests on assumptions believed to survive quantum attacks. For everyone else, the right move is to treat every primitive as swappable from day one — because the one certainty in this field is that today’s security estimate is provisional.</p><h3>A decision framework</h3><p>If there is a single discipline that ties all of this together, it is this: <strong>find your dominant cost center, optimize the primitive for that, and consciously accept the bill everywhere else.</strong></p><ul><li>If your binding constraint is <strong>raw native throughput</strong>, take the fast, mature, hardware-accelerated primitives — SHA-256/Keccak, Ed25519 — and don’t apologize for it.</li><li>If it is <strong>consensus at scale</strong>, the signature scheme is your lever; aggregation via BLS buys you a large validator set at the price of expensive pairing verification.</li><li>If it is <strong>succinct proofs and on-chain verification gas</strong>, you are choosing among commitment schemes, and the KZG-vs-transparent question dominates everything else.</li><li>If it is <strong>provability of your own state</strong>, the decision reaches all the way back to your hash function, and you pay the native and maturity cost of an algebraic hash to slash proving cost.</li><li>If it is <strong>ecosystem reach and interoperability</strong>, conformance beats optimality, and your job is to engineer the hybrid seams cleanly.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pj9FVQ-KWuIDx50ZRccJDg.png" /></figure><p>No row is wrong. Each is right for a different chain. The mistake is not picking the wrong corner of the table — it is failing to recognize that you are choosing a corner at all, and that the choice constrains your architecture for years.</p><h3>The judgment, not the cryptography</h3><p>The cryptography in this space is largely settled. The constructions are public, the libraries are mature, and you will rarely be the one inventing a new hash function. What is <em>not</em> settled, and never will be, is the mapping from a specific protocol’s goals to the right set of trade-offs — because that mapping changes with every project, every threat model, every ecosystem you have to live inside.</p><p>That is the part that doesn’t commoditize. Anyone can import a crate. Knowing <em>which</em> primitive’s cost you can afford to pay, and which you cannot, for <em>this</em> network — that is the work. Code is a commodity. Judgment is a career.</p><p>Follow me on <a href="https://x.com/luishsoares">X</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fc0db36544d6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/coinmonks/there-is-no-best-cryptography-how-primitive-choices-define-your-l1-fc0db36544d6">There Is No “Best” Cryptography: How Primitive Choices Define Your L1</a> was originally published in <a href="https://medium.com/coinmonks">Coinmonks</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[One Signature to Verify Them All]]></title>
            <link>https://medium.com/coinmonks/one-signature-to-verify-them-all-053db04eb253?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/053db04eb253</guid>
            <category><![CDATA[cryptography]]></category>
            <category><![CDATA[math]]></category>
            <category><![CDATA[blockchain]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Fri, 26 Jun 2026 18:02:19 GMT</pubDate>
            <atom:updated>2026-06-29T07:52:09.138Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4480OodZAAU5sasobC-e1g.png" /></figure><h3>BLS aggregation, the math that makes it work, and the things you simply can’t do with ECDSA or Schnorr</h3><p>Imagine a consensus protocol where 500,000 validators each attest to the same block. With ECDSA, that’s 500,000 signatures to gossip, store, and verify — roughly 32 MB of signature data per slot, half a million verifications. The chain would drown in its own cryptography.</p><p>Ethereum’s beacon chain handles exactly this scenario, and the artifact that lands on-chain is a <em>single</em> 96-byte signature plus a bitfield of who participated. That collapse — from hundreds of thousands of signatures down to one — is the headline feature of BLS, and it’s the thing that ECDSA cannot do at all and Schnorr can only do with a coordination protocol bolted on.</p><p>This piece walks through how BLS signatures work, the algebra behind aggregation, the one attack you have to defend against, and the use cases that are genuinely unique to the scheme. The Rust at the end compiles and runs.</p><h3>The one ingredient: a bilinear pairing</h3><p>Everything BLS does rests on a <em>pairing</em> — a map that takes two curve points and produces an element of a third group, with one magical property.</p><p>Take three groups of the same prime order r: G₁, G₂, and Gₜ. A pairing is a function</p><pre>e : G₁ × G₂ → Gₜ</pre><p>that is <strong>bilinear</strong>, meaning scalars can be slid across it freely:</p><pre>e([a]P, [b]Q) = e(P, Q)^(a·b)</pre><p>That single identity is the whole trick. It lets you “see” a multiplication that happened in the exponent of two <em>separate</em> points, without ever learning the scalars themselves. ECDSA and Schnorr live on ordinary elliptic curves where no such map exists; BLS lives on <em>pairing-friendly</em> curves where it does. In practice that curve is <strong>BLS12–381</strong>: a 381-bit prime field, embedding degree 12, around 128-bit security. Points in G₁ compress to 48 bytes, points in G₂ to 96 bytes.</p><h3>The scheme itself</h3><p>BLS is almost embarrassingly small once you have the pairing. I’ll use the <em>minimal-public-key</em> layout (public keys in G₁, signatures in G₂), which is the one Ethereum and the blst library use. Let g₁ be a fixed generator of G₁, and let H be a hash that maps an arbitrary message onto a point in G₂.</p><p><strong>Key generation.</strong> Pick a random scalar sk. The public key is a point:</p><pre>pk = [sk] g₁          (a point in G₁, 48 bytes)</pre><p><strong>Signing.</strong> Hash the message to a curve point and multiply by the secret key:</p><pre>σ = [sk] H(m)         (a point in G₂, 96 bytes)</pre><p>Note what’s <em>missing</em>: there is no nonce, no randomness, no per-signature k. For a given key and message there is exactly one valid signature. Hold onto that — it matters later.</p><p><strong>Verification.</strong> Check one pairing equation:</p><pre>e(g₁, σ) =? e(pk, H(m))</pre><p>Why it holds is one line of bilinearity:</p><pre>e(g₁, σ) = e(g₁, [sk] H(m)) = e(g₁, H(m))^sk<br>e(pk, H(m)) = e([sk] g₁, H(m)) = e(g₁, H(m))^sk   ✓</pre><p>The verifier never sees sk, yet the two sides only match if the signer knew it. That&#39;s the entire scheme.</p><h3>Aggregation: the part you can’t get elsewhere</h3><p>Here’s where the pairing earns its keep. Because a signature is just a point and points add, you can <strong>sum signatures</strong>. There are two cases worth separating, because they have different costs and different security footguns.</p><h3>Case 1 — many signers, the same message</h3><p>This is the validator scenario. Everyone signs the same block m. Add up all the signatures:</p><pre>σ_agg = σ₁ + σ₂ + … + σₙ = [sk₁ + sk₂ + … + skₙ] H(m)</pre><p>and add up all the public keys into one aggregate key:</p><pre>apk = pk₁ + pk₂ + … + pkₙ</pre><p>Verification is then a <em>single</em> pairing equation, no matter how many signers were involved:</p><pre>e(g₁, σ_agg) =? e(apk, H(m))</pre><p>A thousand signers, a million signers — verification is still two pairings after a handful of cheap point additions. The on-chain footprint is one 96-byte signature. <strong>No other signature scheme gives you constant-size, constant-verification-cost aggregation without the signers coordinating.</strong></p><h3>Case 2 — different signers, different messages</h3><p>Now suppose each signer signs their <em>own</em> message — think a batch of distinct transactions. You can still aggregate:</p><pre>σ_agg = σ₁ + σ₂ + … + σₙ      where σᵢ = [skᵢ] H(mᵢ)</pre><p>but verification now needs one pairing per distinct message:</p><pre>e(g₁, σ_agg) =? e(pk₁, H(m₁)) · e(pk₂, H(m₂)) · … · e(pkₙ, H(mₙ))</pre><p>That’s n + 1 pairings. The win here is <strong>bandwidth and storage</strong>, not verification CPU — you ship and store one signature instead of n. The same-message case (one bitfield, two pairings) is the one that&#39;s almost free.</p><h3>The rogue-key attack — read this before you ship</h3><p>The same-message aggregation apk = Σ pkᵢ has a famous hole. Suppose honest parties publish pk₁. A malicious party registering afterward can advertise</p><pre>pk₂ = [sk₂] g₁ − pk₁</pre><p>Now the aggregate key is apk = pk₁ + pk₂ = [sk₂] g₁, whose secret the attacker knows entirely. They can sign on behalf of the &quot;group&quot; alone and forge the honest party&#39;s participation. The honest party never signed anything.</p><p>There are three standard defenses, corresponding to the three IETF ciphersuites in draft-irtf-cfrg-bls-signature:</p><ol><li><strong>Proof of Possession (PoP).</strong> Each participant must publish a signature over their <em>own</em> public key, proving they hold the matching secret. You can’t register a key you derived as a difference of others’ keys, because you don’t know its discrete log. This is what Ethereum uses, and it keeps same-message aggregation cheap.</li><li><strong>Message augmentation.</strong> Sign pk ‖ m instead of m, binding every signature to its signer&#39;s key. No PoP registry needed, but you lose the pure same-message fast path.</li><li><strong>Distinct messages (the “basic” scheme).</strong> Require that all messages in an aggregate are distinct, which structurally rules out the attack.</li></ol><p>Pick one deliberately. The naive Σ pkᵢ with no defense is exploitable.</p><h3>What BLS can do that ECDSA and Schnorr can’t</h3><p>Aggregation alone isn’t the full story. Here is the part the title promised — capabilities that are <em>unique</em> or dramatically simpler with BLS.</p><p><strong>1. Non-interactive aggregation.</strong> This is the real differentiator. ECDSA has no native aggregation whatsoever. Schnorr <em>does</em> aggregate (MuSig2), but the signers must run an interactive protocol first — exchanging nonce commitments before anyone signs. BLS signers never talk to each other. Each signs independently, publishes, and <em>any third party</em> can aggregate the published signatures afterward, even years later. This is precisely why Ethereum can fold together attestations from validators who have no channel between them. With Schnorr you’d need every validator online and coordinating in a multi-round dance every slot.</p><p><strong>2. Unique, deterministic signatures → verifiable randomness.</strong> Because there’s no nonce, σ = [sk] H(m) is <em>the</em> signature — there is exactly one. ECDSA and Schnorr both inject a per-signature nonce, so a signer can produce many valid signatures for the same message and <em>choose</em> among them. Uniqueness turns a BLS signature into a Verifiable Random Function: the <a href="https://drand.love/">drand</a> beacon (the &quot;League of Entropy&quot;) signs each round number with a threshold key, and the unique resulting signature, hashed, is unbiasable public randomness. You cannot build this from ECDSA or Schnorr, because the signer could grind the nonce to skew the output. Determinism is a feature here, not a footnote.</p><p><strong>3. Dead-simple threshold signatures.</strong> BLS is linear in the key — σ_agg = [Σ skᵢ] H(m) — so Shamir secret sharing composes with it directly. A t-of-n set of partial signatures Lagrange-interpolates into the single group signature, and the result is <em>identical</em> no matter which t signers showed up. That canonical output is exactly what makes the VRF use work. Threshold ECDSA exists, but it&#39;s a heavyweight MPC affair (GG18/GG20 and friends) with multiple interactive rounds; threshold BLS is interpolation.</p><p><strong>4. Incremental aggregation in gossip networks.</strong> Aggregation is just point addition, so a node can keep folding new signatures into a running aggregate as they trickle in over the network — no need to collect them all first.</p><h3>The honest tradeoffs</h3><p>BLS is not free, and a balanced article says so.</p><ul><li><strong>Pairings are expensive.</strong> A single BLS verification (two pairings) is meaningfully slower than an ECDSA verify. The distinct-message aggregate verify is n+1 pairings, so it saves bytes, not CPU. Only the same-message fast-aggregate path is cheap regardless of n.</li><li><strong>Signing needs hash-to-curve</strong>, which is fiddlier to implement correctly than ECDSA’s hash-to-scalar.</li><li><strong>Bigger, gnarlier implementations</strong> and far less HSM / hardware support than secp256k1 or Ed25519.</li><li><strong>Pairing-friendly curves took a security haircut.</strong> Improved number-field-sieve attacks are why the curve is BLS12–381 (≈128-bit) and not something smaller — the original BLS12–381 security estimates were revised downward.</li><li><strong>Not quantum-resistant</strong> — like ECDSA and Schnorr, it rests on discrete log.</li></ul><h3>It runs</h3><p>Here’s a complete, compiling example against the blst crate (the same library Ethereum consensus clients use). It exercises a single signature, same-message aggregation over 1,000 signers, distinct-message aggregation, and tamper rejection.</p><p>rust</p><pre>use blst::min_pk::{AggregateSignature, PublicKey, SecretKey, Signature};<br>use blst::BLST_ERROR;<br><br>// Standard DST for Ethereum-style minimal-pubkey BLS.<br>const DST: &amp;[u8] = b&quot;BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_&quot;;<br><br>fn keypair(seed: &amp;[u8; 32]) -&gt; (SecretKey, PublicKey) {<br>    let sk = SecretKey::key_gen(seed, &amp;[]).expect(&quot;key_gen&quot;);<br>    let pk = sk.sk_to_pk();<br>    (sk, pk)<br>}<br><br>fn main() {<br>    // 1. One signer, one message.<br>    let (sk, pk) = keypair(&amp;[1u8; 32]);<br>    let msg = b&quot;settle block #42&quot;;<br>    let sig = sk.sign(msg, DST, &amp;[]);<br><br>    assert_eq!(sig.verify(true, msg, DST, &amp;[], &amp;pk, true), BLST_ERROR::BLST_SUCCESS);<br><br>    println!(&quot;signature: {} bytes, pubkey: {} bytes&quot;,<br>             sig.compress().len(), pk.compress().len());<br><br>    // 2. Same message, 1000 signers -&gt; ONE signature (fast_aggregate_verify).<br>    let n = 1000;<br>    let mut sks = Vec::new();<br>    let mut pks = Vec::new();<br><br>    for i in 0..n {<br>        let mut seed = [0u8; 32];<br>        seed[..8].copy_from_slice(&amp;(i as u64 + 7).to_le_bytes());<br>        let (s, p) = keypair(&amp;seed);<br>        sks.push(s);<br>        pks.push(p);<br>    }<br><br>    let block = b&quot;finalize epoch 17&quot;;<br>    let sigs: Vec&lt;Signature&gt; = sks.iter().map(|s| s.sign(block, DST, &amp;[])).collect();<br><br>    // Anyone can aggregate the published signatures - no coordination needed.<br>    let sig_refs: Vec&lt;&amp;Signature&gt; = sigs.iter().collect();<br>    let agg = AggregateSignature::aggregate(&amp;sig_refs, true).unwrap().to_signature();<br>    let pk_refs: Vec&lt;&amp;PublicKey&gt; = pks.iter().collect();<br><br>    assert_eq!(agg.fast_aggregate_verify(true, block, DST, &amp;pk_refs),<br>               BLST_ERROR::BLST_SUCCESS);<br><br>    println!(&quot;{} sigs on one message -&gt; {} bytes&quot;, n, agg.compress().len());<br><br>    // 3. Distinct messages, distinct signers (aggregate_verify).<br>    let (sk_a, pk_a) = keypair(&amp;[10u8; 32]);<br>    let (sk_b, pk_b) = keypair(&amp;[20u8; 32]);<br>    let (sk_c, pk_c) = keypair(&amp;[30u8; 32]);<br>    let (m_a, m_b, m_c): (&amp;[u8], &amp;[u8], &amp;[u8]) =<br>        (b&quot;alice-&gt;bob:5&quot;, b&quot;bob-&gt;carol:3&quot;, b&quot;carol-&gt;alice:1&quot;);<br>    let s_a = sk_a.sign(m_a, DST, &amp;[]);<br>    let s_b = sk_b.sign(m_b, DST, &amp;[]);<br>    let s_c = sk_c.sign(m_c, DST, &amp;[]);<br>    let agg2 = AggregateSignature::aggregate(&amp;[&amp;s_a, &amp;s_b, &amp;s_c], true)<br>        .unwrap().to_signature();<br>    let msgs: [&amp;[u8]; 3] = [m_a, m_b, m_c];<br>    let pks2 = [&amp;pk_a, &amp;pk_b, &amp;pk_c];<br><br>    assert_eq!(agg2.aggregate_verify(true, &amp;msgs, DST, &amp;pks2, true),<br>               BLST_ERROR::BLST_SUCCESS);<br><br>    // 4. Tampering is caught.<br>    let forged: [&amp;[u8]; 3] = [m_a, b&quot;bob-&gt;carol:3000&quot;, m_c];<br>    assert_ne!(agg2.aggregate_verify(true, &amp;forged, DST, &amp;pks2, true),<br>               BLST_ERROR::BLST_SUCCESS);<br>    println!(&quot;all checks passed.&quot;);<br>}</pre><p>Output:</p><pre>signature: 96 bytes, pubkey: 48 bytes<br>1000 sigs on one message -&gt; 96 bytes<br>all checks passed.</pre><p>A thousand signatures, one 96-byte artifact, verified against a thousand public keys with two pairings. That’s the whole pitch.</p><h3>The takeaway</h3><p>ECDSA gives you signatures. Schnorr gives you signatures that aggregate <em>if everyone coordinates first</em>. BLS gives you signatures that aggregate <strong>after the fact, by anyone, with no coordination at all</strong> — and as a bonus, because they’re unique and deterministic, they double as a source of verifiable randomness and slot cleanly into threshold schemes. That combination is why it underpins Ethereum consensus, drand, Chia, and most modern threshold-cryptography stacks.</p><p>The cost is real: pairings are slow, the curves are heavy, and the rogue-key attack will bite you if you skip proof-of-possession. But for any system where you’re collecting independent attestations at scale, nothing else comes close.</p><p>Follow me on <a href="https://x.com/luishsoares">X</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=053db04eb253" width="1" height="1" alt=""><hr><p><a href="https://medium.com/coinmonks/one-signature-to-verify-them-all-053db04eb253">One Signature to Verify Them All</a> was originally published in <a href="https://medium.com/coinmonks">Coinmonks</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Memory Model Is the Architecture: Account vs. eUTXO]]></title>
            <link>https://medium.com/coinmonks/the-memory-model-is-the-architecture-account-vs-eutxo-9200439b023a?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/9200439b023a</guid>
            <category><![CDATA[defi]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[utxo]]></category>
            <category><![CDATA[web3]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Tue, 16 Jun 2026 20:57:24 GMT</pubDate>
            <atom:updated>2026-06-18T15:41:41.004Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gmaYQwNtn1SS6u4V1slXRA.png" /></figure><p><em>A protocol engineer’s analysis of state representation, parallel execution, and the tradeoffs that don’t make it into the pitch deck.</em></p><p>The account-vs-UTXO debate is usually framed as developer ergonomics: mutable objects and method calls versus functional purity and immutable outputs. That framing buries the lede. The state model is the concurrency model. It determines how the runtime schedules execution, where contention surfaces, which classes of bug are <em>reachable</em> versus merely <em>avoidable</em>, and how lean a node’s working set stays after years of operation. Parallelism, MEV surface, L2 sequencer complexity, and state growth are all downstream of this one decision.</p><p>What follows is an analysis from the runtime’s perspective — including the points where the UTXO model’s advantages stop being free.</p><h3>1. State Representation: Global Trie vs. UTXO Set</h3><h3>The account model: keyed mutable storage</h3><p>An account-model chain maintains a single authenticated key-value store. On Ethereum that’s a Merkle-Patricia Trie keyed by keccak256(address), with each leaf holding (nonce, balance, storageRoot, codeHash). Contract storage is a second trie nested under storageRoot, keyed by keccak256(slot).</p><p>Execution is read-modify-write against this structure. The EVM resolves a SLOAD, mutates registers, and commits via SSTORE. Two properties fall out of this and drive everything later:</p><ul><li><strong>Authenticated reads/writes are not free.</strong> Each access is an O(log n) trie traversal over keccak-hashed paths with poor locality, so the cost is dominated by random NVMe/page-cache I/O, not the arithmetic. EIP-2929&#39;s cold/warm split (2100 vs. 100 gas for SLOAD) is a direct admission that the <em>first</em> touch of a slot is where the real cost lives.</li><li><strong>The write set is not known until execution.</strong> Storage slots can be computed at runtime (mapping keys, dynamic dispatch via DELEGATECALL). The runtime cannot know what a transaction will touch without running it. Hold onto this — it is the entire parallelism story.</li></ul><h3>The UTXO model: state as a consumed/created graph</h3><p>A UTXO ledger has no accounts, no balances, and no in-place mutation. State is exactly the set of Unspent Transaction Outputs. A transaction is a pure transition: it names a set of existing outputs to consume and defines a set of new outputs to create.</p><pre>tx : {UTXO_in} → {UTXO_out}    where every UTXO_in is removed from the set<br>                               and every UTXO_out is appended exactly once</pre><p>Each output carries a globally unique identifier id = H(txid ‖ output_index) and can be consumed at most once. The lineage of consumptions and creations forms a DAG. There is no &quot;balance to update,&quot; only prior state destroyed and new state minted. This is the same invariant Rust enforces at compile time — single ownership, single consumption, no shared mutable state — lifted to the ledger.</p><p>The practical consequence is that a transaction must <em>name its exact inputs by hash before broadcast</em>. That requirement is the source of nearly every other property in this article — good and bad.</p><h3>2. Concurrency: Where Parallelism Actually Comes From</h3><p>This is the most misunderstood part of the comparison, so be precise about it: <strong>parallel execution requires knowing the read/write set before you run the transaction.</strong> The account model and the UTXO model differ in <em>when</em> and <em>how</em> that set becomes known — not in some inherent capacity for concurrency.</p><h3>EVM: undeclared access forces optimism</h3><p>The EVM cannot know a transaction’s footprint a priori, so parallel EVM runtimes use Optimistic Concurrency Control. <strong>Block-STM</strong> (Aptos, and the approach several parallel-EVM efforts adopt) speculatively executes transactions in parallel, records read/write sets, and validates after the fact. On a detected conflict — two transactions writing the same DEX reserve slot — it aborts the dependent transaction and re-executes. Under low contention this is close to linear speedup. Under a hot slot (a popular pool, an NFT mint) it degrades toward sequential plus the wasted cost of aborted attempts and re-validation passes. The contention didn’t disappear; it moved into CPU thrash.</p><h3>The account model can be declarative</h3><p>It’s worth killing a common oversimplification: account models are not intrinsically un-parallelizable. <strong>Solana’s Sealevel</strong> parallelizes by requiring every transaction to declare its account access list (read-only vs. writable) up front; the scheduler then runs transactions with disjoint writable sets concurrently, no speculation needed. <strong>Sui</strong> does the same with its object model — transactions touching only <em>owned</em> objects skip consensus ordering entirely and parallelize trivially, while <em>shared</em> objects fall back to sequenced execution. Both are account/object models that bought parallelism by adopting the UTXO discipline of <em>declaring your footprint</em>. The lesson generalizes: the win was never “UTXO,” it was “declare your state access.”</p><h3>UTXO: disjointness by construction</h3><p>In a UTXO chain that declaration is mandatory and unforgeable — the inputs <em>are</em> the footprint. Two transactions conflict iff their input sets intersect, which is a set-membership check against consumed-output hashes (amortized O(1) per input via a hash set; scheduling N transactions is O(total inputs)). Disjoint input sets are conflict-free by construction, with no speculation and no rollback path to maintain. The scheduler gets a hard guarantee instead of a hopeful one.</p><p>That’s the genuine, defensible advantage: <strong>deterministic conflict detection with zero abort machinery.</strong> Keep it in mind for the next section, where the same mechanism becomes a liability.</p><h3>3. Programmability: The eUTXO Execution Context</h3><p>A bare UTXO pairs a value with a spending predicate (typically a signature check). It has no persistent memory and can’t enforce invariants across transactions. The Extended UTXO (eUTXO) model — formalized in the IOG/Cardano research line and used in spirit by object/UTXO hybrids like Fuel — adds programmable, <em>local</em> state to each output via three components:</p><ul><li><strong>Validator (script):</strong> the predicate, addressed by its hash. It returns a boolean; it does not compute outputs.</li><li><strong>Datum:</strong> an arbitrary data payload attached to the output. This is the output’s local memory.</li><li><strong>Redeemer:</strong> the caller-supplied argument provided when consuming the output (the analogue of calldata).</li></ul><p>Spending an output runs the validator over its local context:</p><pre>validate(Datum, Redeemer, ScriptContext) → Bool</pre><p>ScriptContext exposes the whole transaction — all inputs, all outputs, validity interval, signatories — so the script can constrain what the <em>resulting</em> transaction must look like. There is no global state read.</p><h3>Worked example: a constant-product pool</h3><p>In the account model, an AMM pool’s reserves (x, y) live in shared storage slots that every swap mutates in place. In eUTXO, the entire pool <em>is one UTXO</em> whose datum holds (x, y). A swap:</p><ol><li>Consumes the pool UTXO.</li><li>Supplies a redeemer (swap 10 X for Y).</li><li>The validator checks the invariant on the <em>new</em> output: it recomputes x&#39; · y&#39; ≥ k (with fees), verifies the produced pool UTXO carries the correct updated datum (1010, 990), and returns True.</li></ol><p>The old pool UTXO is destroyed and a new one with the updated datum is minted, atomically. Multi-step protocols are built by threading the datum through a chain of consume/create steps — state machines on a graph, never a global trie lookup. Note the inversion: the validator <em>verifies</em> a state transition the caller already computed; it doesn’t <em>produce</em> one.</p><h3>4. The Contention Tax: Where eUTXO Parallelism Collapses</h3><p>This is the section the optimistic version of this article leaves out, and it’s the one that matters most if you’re choosing an architecture to <em>ship</em> on.</p><p>Re-read the AMM example with the scheduler from §2 in mind. The entire pool is a single UTXO. A UTXO can be consumed exactly once. Therefore <strong>only one swap against that pool can land per block.</strong> Every other swap in the mempool named the same input; the moment one of them is included, the rest reference an output that no longer exists and are invalid by construction.</p><p>The property that made disjoint transactions trivially parallel makes <em>contended</em> transactions strictly serial — and worse than the EVM here, because there’s no “queue behind the current state” semantics, just instant invalidation. On a popular pool this is a throughput cliff, not a gentle degradation. This is not theoretical: it’s exactly why production Cardano DEXs (Minswap, SundaeSwap, and others) run <strong>batcher / off-chain aggregation</strong> layers. Users submit orders as their own UTXOs; a batcher collects them and folds them into the pool UTXO sequentially, one fold per block. You get back throughput, but you’ve reintroduced a privileged off-chain actor that orders and includes flow — which is precisely an MEV role (see §6).</p><p>The architectural escape hatches, and their costs:</p><ul><li><strong>Shard the state across many UTXOs</strong> (e.g., split liquidity into K parallel pool UTXOs). Restores K-way parallelism but fragments liquidity and pushes routing complexity to the client.</li><li><strong>Off-chain batching/solvers.</strong> Restores throughput, recentralizes ordering.</li><li><strong>Design around single-writer hotspots entirely.</strong> Often the right call, but it constrains what protocols are natural to build.</li></ul><p>The honest framing: eUTXO gives you <em>free parallelism on uncontended state and a hard serialization wall on contended state</em>. The account/STM model gives you <em>speculative parallelism that degrades continuously under contention</em>. Neither is strictly better; they fail differently, and you should pick the failure mode that matches your workload’s contention profile.</p><h3>5. Determinism: Construction-Time vs. Execution-Time Evaluation</h3><p>This is eUTXO’s cleanest and most underrated win, and it survives the §4 critique intact.</p><p>In the account model, a transaction is evaluated at <strong>execution time</strong>. The user signs against state S; the world mutates S → S&#39; while the transaction sits in the mempool; the transaction executes against S&#39;, fails its slippage check, reverts — and the user still pays gas for the cycles burned reaching the revert.</p><p>eUTXO transactions are evaluated at <strong>construction time</strong>. Inputs, outputs, and the resulting datum are fully specified before broadcast, so the on-chain client is a <em>verifier</em>, not a compute engine. It checks two things: do the named inputs still exist in the unspent set, and does each validator return True. If your targeted pool UTXO was consumed by someone else first, your transaction doesn&#39;t revert — it&#39;s structurally invalid and is dropped at the validation gate, before any script runs. You don&#39;t pay for failed computation. (Cardano&#39;s two-phase model is the practical embodiment: phase-1 checks structure/balance for free; phase-2 runs scripts, and only a phase-2 <em>script</em> failure forfeits posted collateral.)</p><h3>L2 derivation consequences</h3><p>For rollups this is a real simplification, not a slogan. An EVM sequencer must execute the full state transition to compute the post-state root before it can commit a block. A UTXO-based sequencer’s core job reduces to verifying input disjointness and signatures/scripts — the expensive transition computation was already done by the client that built the transaction. The heavy compute is pushed to the edge, and the L1→L2 derivation pipeline gets leaner and easier to make deterministic and fault-provable. The flip side is that wallet/client tooling must now do real work (UTXO selection, datum construction, fee balancing), which is where the eUTXO developer-experience complaints legitimately come from.</p><h3>6. Security: Reentrancy as a Structural Property — and What eUTXO Does Not Fix</h3><h3>Reentrancy is precluded, not patched</h3><p>Reentrancy exists in the account model because a contract can make an external call <em>before</em> committing its own state update, leaving a stale intermediate state for the callee to exploit. The defense is disciplined sequencing (checks-effects-interactions) and reentrancy guards — i.e., a coding practice you can forget.</p><p>In eUTXO there is no intermediate state to re-enter. State transition is atomic destruction-and-creation: the instant a transaction is validated, its input UTXO is consumed and gone; the updated UTXO does not exist until the transaction’s outputs are appended at the end. There is no point in the lifecycle where a contract holds a half-updated balance on the ledger that another execution context could observe and exploit. The vulnerability class isn’t mitigated — it’s structurally unrepresentable.</p><h3>Replay prevention without nonces</h3><p>The account model serializes an account’s transactions with an incrementing nonce, which couples otherwise-independent transactions: a stuck tx at nonce 5 blocks nonce 6. eUTXO needs no nonces — each output’s unique id = H(txid ‖ index) is consumed exactly once, so a rebroadcast fails because its inputs are gone. A wallet can fan out hundreds of independent transactions over disjoint UTXOs in parallel with no sequence head-of-line blocking. (The cost: you must manage a UTXO set and avoid accidentally double-spending your own outputs across concurrent transactions — a real client-side concern for high-frequency systems.)</p><h3>eUTXO does not eradicate MEV</h3><p>This is where the original needs correcting. MEV is extractable value from <em>ordering and inclusion</em>, and eUTXO has both. Whoever lands their transaction first against a contended UTXO (a pool, an oracle, an auction) wins; everyone else is invalidated. Block producers still choose ordering. And as §4 showed, the standard eUTXO scaling pattern <em>introduces</em> batchers/solvers who explicitly order user flow — a textbook MEV-extraction position. eUTXO changes the <em>shape</em> of MEV (front-running becomes “your input got consumed first” rather than “your trade executed at a worse price”) and removes the specific failure of paying gas for a doomed revert, but it does not remove the economic incentive or the producer’s ordering power. Claiming otherwise is the kind of thing a reviewer who’s run a Cardano batcher will catch immediately.</p><h3>7. State Growth and Pruning</h3><p>EVM state monotonically inflates: dead contracts, abandoned tokens, and dust persist in the active trie because the structure is interconnected and reachability is hard to reason about. State-expiry proposals have stalled for years for exactly this reason, and node operators eat the cost in RAM and NVMe provisioning.</p><p>The UTXO model prunes by construction. The active validation state is <em>only</em> the unspent set; a consumed output is removed and never consulted again to validate future transactions. The working set stays lean without a state-rent mechanism. Two caveats keep this honest: (1) the UTXO <em>set itself</em> can still grow if outputs are created faster than consumed — which is why Cardano enforces a min-ADA-per-UTXO to price storage and discourage dust, and (2) pruning the <em>active</em> set doesn’t eliminate the <em>historical</em> chain data a new node must process to sync. The advantage is real but it’s about working-set size, not total data.</p><h3>When to Choose What</h3><p>Reduce it to contention profile and execution semantics, not ideology:</p><ul><li><strong>Choose account/STM</strong> when state is densely shared and write-heavy at common hotspots, when you want maximum developer familiarity, and when you can tolerate speculative re-execution overhead under load. You’re betting on optimism degrading gracefully.</li><li><strong>Choose declarative-access (Solana/Sui-style)</strong> when you can require footprint declaration and your workload is mostly disjoint — you get UTXO-like scheduling guarantees while keeping object/account ergonomics.</li><li><strong>Choose eUTXO</strong> when you want construction-time determinism, structural elimination of an entire bug class, trivial parallelism on disjoint state, and cheap pruning — and when you can engineer around single-writer hotspots with sharding or batching rather than pretend they don’t exist.</li></ul><p>The account model optimized for the developer’s mental model first. The UTXO model optimizes for the runtime’s guarantees first and pushes complexity to the client. As throughput and determinism requirements rise, that trade keeps looking better — provided you go in knowing exactly where the serialization wall is, rather than discovering it in production.</p><p>Follow me on <a href="https://x.com/luishsoares">X</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9200439b023a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/coinmonks/the-memory-model-is-the-architecture-account-vs-eutxo-9200439b023a">The Memory Model Is the Architecture: Account vs. eUTXO</a> was originally published in <a href="https://medium.com/coinmonks">Coinmonks</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Architecting the MetaDEX: Inside Aerodrome’s ve(3,3) Engine and Anti-Dilution Math]]></title>
            <link>https://medium.com/@luishrsoares/architecting-the-metadex-inside-aerodromes-ve-3-3-engine-and-anti-dilution-math-3b3c1547ba52?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/3b3c1547ba52</guid>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[defi]]></category>
            <category><![CDATA[aerodrome-finance]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Sun, 14 Jun 2026 23:18:02 GMT</pubDate>
            <atom:updated>2026-06-14T23:18:02.659Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HfC1xqulP77_t7M2DK9Nog.png" /></figure><p>The fundamental flaw of early decentralized finance was the “mercenary capital” problem. First-generation AMMs relied entirely on inflationary token emissions to bootstrap liquidity. The economic loop was brutally short-sighted: Liquidity Providers (LPs) would deposit capital, harvest the DEX’s native ERC-20 token, and immediately dump it on the open market to realize their yield.</p><p>This created a deterministic death spiral. The constant sell pressure crashed the native token’s price, which mathematically lowered the yield (APR) of the pools. As yields dropped, mercenary LPs withdrew their capital, slippage increased, trading volume plummeted, and the protocol slowly starved to death.</p><p>To survive, a protocol needed to align the long-term incentives of the LPs, the token holders, and the traders. The solution emerged by combining the <strong>vote-escrowed (ve)</strong> time-locking mechanics pioneered by Curve Finance with the <strong>(3,3)</strong> anti-dilution game theory popularized by OlympusDAO.</p><p>While early iterations of this “ve(3,3)” model were theoretically sound, their smart contract implementations were heavily exploitable. Aerodrome Finance represents the most refined, battle-tested iteration of this architecture to date. It operates not just as an exchange, but as an economic middle-layer — a “MetaDEX” where external protocols rent liquidity rather than organically subsidizing it themselves.</p><p>Here is the architectural breakdown of the smart contracts, fixed-point math, and game theory that make that engine function.</p><h3>1. The “ve” (Vote-Escrow) Architecture</h3><p>In standard AMMs like Uniswap v2, liquidity providers (LPs) are mercenaries. They deposit capital, farm the protocol’s native ERC-20 emission token, and immediately dump it on the market to realize their yield, destroying the token’s price.</p><p>The “ve” model rewires this relationship at the contract level. It forces a timeline on the realization of value.</p><h3>Time-Weighted Voting Power (VotingEscrow.sol)</h3><p>Users must lock the native token (e.g., AERO or VELO) into the VotingEscrow contract to receive a non-transferable ERC-721 governance derivative, the veToken.</p><p>The protocol does not use a 1:1 mapping of locked tokens to voting power. Instead, voting power (V) is a linear function of both the amount of tokens locked (A) and the duration of the lock (tlock​), relative to the maximum allowable lock time (tmax​, typically 4 years).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vJYN7VDezOXsMDcjAM_W6Q.png" /></figure><p>If you lock 100 tokens for 4 years, your voting weight is 100. If you lock 100 tokens for 1 year, your voting weight is 25.</p><p>Because the remaining lock duration physically decreases every single block, the voting power naturally decays. To avoid updating the storage slots of thousands of users every block (which is impossible), the contract uses a heavily optimized <strong>Checkpointing System</strong>. Every time a user interacts with their lock, the contract records a bias (absolute voting power) and a slope (rate of decay per second). The protocol calculates current voting power dynamically by projecting the line forward from the last checkpoint.</p><h3>Gauge Control and Fee Accrual (Voter.sol &amp; Gauge.sol)</h3><p>In this architecture, LPs <em>do not</em> receive swap fees.</p><p>Instead, veToken holders interact with Voter.sol to cast their weights across various pool &quot;Gauges.&quot;</p><ul><li><strong>Emissions:</strong> At the epoch boundary, the Minter mints the weekly inflation and distributes it to the Gauge contracts strictly proportionally to the votes each gauge received. LPs stake in the Gauges to earn these emissions.</li><li><strong>Fee Accrual:</strong> 100% of the trading fees generated by the actual liquidity pools are stripped out of the pool and sent to a FeesVotingReward contract. These fees are exclusively claimable by the specific veToken holders who voted for that exact pool.</li></ul><h3>2. The “(3,3)” Game Theory and Anti-Dilution Math</h3><p>The vulnerability of standard “ve” tokenomics is inflation. If you lock your tokens for 4 years, and the protocol constantly mints new tokens to pay LPs, your percentage share of the total voting power is actively diluted every epoch.</p><p>The (3,3) mechanic introduces mathematical anti-dilution logic, heavily incentivizing locking over selling. The protocol protects the voting weight of lockers by issuing a “rebase” — an automatic allocation of the new emissions directly to veToken holders.</p><h3>The Rebase Implementation</h3><p>Implementations of this math vary wildly depending on the fork (Solidly vs. Velodrome vs. Aerodrome). Early Solidly iterations gave 100% anti-dilution protection, which over-subsidized lockers and starved LPs.</p><p>Modern architectures (like Velodrome v2 and Aerodrome) scale the rebase based on the ratio of locked tokens to total supply. The core concept ensures that lockers receive a proportional share of the epoch’s emissions (E).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SHFJUvgcufa7xm20p2ZWdg.png" /></figure><p>This specific polynomial curve ensures that when a small percentage of the total supply is locked, the rebase is minimal (directing the vast majority of inflation to attract LPs). As more of the supply is locked, the rebase grows, protecting the lockers’ governance power from being diluted by the protocol’s inflation.</p><p>Because veToken holders receive trading fees, bribes, <em>and</em> anti-dilution rebases, the optimal game-theoretic move is mutual cooperation: lock for the maximum duration (3,3).</p><h3>3. Bribery: The Liquidity Engine</h3><p>Because veToken holders dictate where the E (emissions) go, and LPs follow those emissions, voting power itself becomes the protocol&#39;s primary commodity. This creates a highly efficient &quot;bribery&quot; market.</p><p>If a new Layer 2 or a protocol needs deep liquidity, organically bootstrapping it requires hyper-inflating their own native token. Instead, they “rent” the DEX’s liquidity.</p><h3>The Execution Loop</h3><p>The smart contract topology handles this via the BribeVotingReward.sol contract.</p><ol><li><strong>The Bribe (Epoch N):</strong> The external protocol calls notifyRewardAmount(token, amount) on the Bribe contract attached to their pool&#39;s gauge. They deposit assets (e.g., $10,000 in USDC). The contract explicitly stamps this bribe to only be valid for the <em>next</em> epoch (Epoch N+1) to prevent flash-loan governance attacks.</li><li><strong>The Vote (Epoch N):</strong> veToken holders scan the dashboards. They calculate the APR of the bribes plus the organic trading fees. They call Voter.vote() to point their decaying voting weight at the highest-yielding pools.</li><li><strong>The Snapshot &amp; Emissions (Epoch N → N+1):</strong> The epoch transitions. Voting weights are snapshotted. The $10,000 bribe is unlocked for the voters to claim. Simultaneously, because the pool secured a massive percentage of the total votes, the Minter directs a massive percentage of the DEX&#39;s token emissions (e.g., $25,000 worth) to that specific pool&#39;s Gauge.</li><li><strong>The Liquidity:</strong> Yield-farming bots and LPs spot the massive emission APR on the pool. They instantly deposit their capital to farm the $25,000 in emissions, granting the external protocol the deep liquidity it required.</li></ol><h3>4. L2 Execution Dependencies: The OP Stack Context</h3><p>Aerodrome does not exist in a vacuum; it is natively deployed on Base, which is built on the OP Stack. For protocol engineers, porting Ethereum Mainnet logic directly to an L2 without accounting for the rollup’s specific architecture is a recipe for disaster.</p><p>The L2 execution environment alters how Aerodrome processes transactions, handles MEV (Maximal Extractable Value), and relies on block finality.</p><h3>Fixed Block Times and Oracle Updates</h3><p>Unlike Ethereum Mainnet, where block times fluctuate around 12 seconds based on network propagation and slot utilization, the OP Stack enforces a rigid, strictly deterministic 2-second block time.</p><p>For Aerodrome’s internal TWAP (Time-Weighted Average Price) oracles, this is a massive advantage. TWAP oracles rely on measuring the price at the start of a block. On Mainnet, the variance in block times introduces statistical noise into the time-weighted accumulator. On Base, the accumulator updates with high-fidelity, predictable mathematical precision.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OWpK8HSEW6EkLbeJNFcCqg.png" /></figure><p>Because Δt is almost always exactly 2 seconds, external lending protocols consuming Aerodrome’s oracle data benefit from a much smoother and more attack-resistant price feed.</p><h3>Sequencer Uptime and L2 Derivation</h3><p>The most critical infrastructure dependency for Aerodrome is the OP Stack Sequencer. The sequencer is responsible for ordering transactions and building L2 blocks before committing the state roots to the L1 derivation pipeline.</p><p>If the sequencer experiences downtime, the Aerodrome smart contracts are completely frozen. While this protects the protocol from being drained during an outage, the true danger lies in the <strong>recovery phase</strong>.</p><p>When the sequencer comes back online, a massive backlog of pending transactions floods the mempool. If global market prices shifted drastically during the L2 downtime, the pools on Aerodrome will open with extremely stale, inaccurate prices. Arbitrage bots will immediately extract millions of dollars of value from the AMM LPs in the first few blocks to realign the DEX’s prices with the broader market.</p><p>To mitigate systemic fallout from this, modern AMM integrations often require an “Uptime Oracle” (like those provided by Chainlink). External protocols reading Aerodrome’s state must verify that the L2 sequencer has been online and producing blocks for a continuous grace period (e.g., 30 minutes) before allowing liquidations or large swaps to execute against the pool’s liquidity.</p><h3>5. The Keeper Network: Automating the State Transition</h3><p>A frequently overlooked reality of decentralized finance is that smart contracts cannot execute themselves. There is no cron job in the EVM. If an epoch is scheduled to end on Thursday at 00:00 UTC, the state will not actually transition until an externally owned account (EOA) pays the gas to trigger the transaction.</p><p>In Aerodrome, the entire ve(3,3) flywheel hinges on the Voter.distribute() and Minter.update_period() functions.</p><h3>The Automation Bottleneck</h3><p>At the epoch boundary, the protocol must:</p><ol><li>Snapshot the final voting weights.</li><li>Mint millions of new AERO tokens.</li><li>Push those tokens to dozens of individual Gauge contracts.</li><li>Unlock the pending bribes in the BribeVotingReward contracts.</li></ol><p>This is a massive, gas-intensive state transition. If no one calls the function, LPs stop receiving emissions, voters cannot claim bribes, and the protocol gridlocks.</p><h3>Incentive Alignment via Bounties</h3><p>To solve this, Aerodrome does not rely on an altruistic core team to pay the gas. It decentralizes the execution by relying on a network of MEV searchers and Keepers, driven entirely by profit.</p><p>The protocol implements a bounty system within the epoch transition logic.</p><pre>// Conceptual architecture of the keeper bounty<br>function update_period() external returns (uint256) {<br>    uint256 _period = active_period;<br>    if (block.timestamp &gt;= _period + 1 weeks) {<br>        _period = (block.timestamp / 1 weeks) * 1 weeks;<br>        active_period = _period;<br>        <br>        uint256 emission = calculate_emission();<br>        IERC20(aero).mint(address(voter), emission);<br>        <br>        // The protocol explicitly rewards the caller<br>        uint256 keeper_bounty = calculate_bounty();<br>        IERC20(aero).transfer(msg.sender, keeper_bounty);<br>        <br>        return emission;<br>    }<br>    return 0;<br>}</pre><p>At exactly 00:00:01 UTC, a race begins. Highly optimized Keeper bots calculate the exact L2 gas cost required to execute the transition against the current value of the keeper_bounty. The first bot to successfully land the transaction in the sequencer&#39;s block takes the profit, ensuring the protocol&#39;s state advances autonomously without a centralized point of failure.</p><h3>Conclusion: The Architecture of Incentives</h3><p>Building a decentralized exchange is no longer a problem of simply writing an invariant equation and deploying a factory contract.</p><p>The evolution from Uniswap’s passive liquidity model to Aerodrome’s ve(3,3) MetaDEX illustrates a fundamental shift in protocol engineering. You must account for multi-block economic attacks, read-only state desynchronization, Layer 2 execution environments, and autonomous incentive layers.</p><p>Deploying an AMM fork is trivial. Code is a commodity. But meticulously designing the economic incentives, mitigating state desynchronization, and securing the epoch boundaries to create a self-sustaining liquidity engine — that judgment is a career.</p><p>Follow me on <a href="https://x.com/luishsoares">X</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3b3c1547ba52" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Disaggregating the Mempool: The Memory Architecture of DAG Consensus]]></title>
            <link>https://medium.com/coinmonks/disaggregating-the-mempool-the-memory-architecture-of-dag-consensus-ac41cb2e4a2d?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/ac41cb2e4a2d</guid>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[mempool]]></category>
            <category><![CDATA[defi]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Fri, 12 Jun 2026 12:20:27 GMT</pubDate>
            <atom:updated>2026-06-20T17:06:40.641Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*td0f0OhfpBpcM5Nv3E-yzw.png" /></figure><p>If you evaluate the architecture of next-generation, high-throughput networks — Sui, Aptos, or advanced modular sequencers — you will notice a fundamental shift in how they handle state. They have entirely abandoned the traditional monolithic block-building process.</p><p>Instead of a single leader proposing a block of transactions over a synchronous gossip network, these systems utilize protocols inspired by Narwhal, Bullshark, and more recently, Mysticeti. They separate data dissemination from consensus ordering. Nodes continuously stream transaction batches to each other, building a Directed Acyclic Graph (DAG) of causal history entirely in system memory. Consensus is then run purely on the structural metadata (the references within the graph) rather than the payload itself.</p><p>From a theoretical standpoint, this solves the leader bottleneck and allows throughput to scale linearly with bandwidth. But as an infrastructure advisor, I don’t look at the theory. I look at the implementation.</p><p>When you shift the entire transaction dissemination layer into a continuous, ever-expanding graph structure, you introduce a massive systems engineering hurdle: <strong>Memory Management and Concurrent State Traversal.</strong> If you misconfigure the garbage collection of a DAG mempool, your 256GB RAM validator will hit an Out-Of-Memory (OOM) panic and crash in minutes.</p><p>Here is the engineering teardown of how high-performance DAG mempools manage memory boundaries, the shift toward uncertified graphs, and exactly where the implementations bottleneck under load.</p><h3>1. The DAG Paradigm: Causal History in RAM</h3><p>In a traditional blockchain, consensus and data availability are bundled into one linear process. In a DAG-based system, validators do not wait for a block time. They constantly ingest user transactions, package them into “vertices” (or batches), and broadcast them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OpPexc4DI-yeKc-0PteIvA.png" /></figure><p>When a validator creates a new vertex for the current Round (let’s say, Round $R$), that vertex must include cryptographic pointers to a quorum of vertices from the previous round (Round $R-1$).</p><p>In older models like Narwhal, these were <strong>Certified DAGs</strong>, meaning every vertex required $2f+1$ signatures (a certificate) before it could be referenced, heavily taxing the CPU with signature verification. Bleeding-edge protocols like Mysticeti utilize <strong>Uncertified DAGs</strong>, completely dropping the reliable broadcast phase. Vertices are simply linked via “strong edges” (references to $f+1$ previous vertices).</p><p>This creates a dense, interlocking, and highly complex web of causal history resident entirely in the node’s system RAM. Because the network is asynchronous, different nodes will see different parts of the graph at different times. The node must hold this entire structure in memory so that when the consensus engine “commits” a specific leader vertex, the node can deterministically traverse the graph backward to sequence all the transactions that led to it.</p><h3>2. The Unbounded Graph and the OOM Threat</h3><p>Herein lies the architectural danger: <strong>a DAG naturally grows unboundedly.</strong></p><p>If a network is processing 100,000 transactions per second, it is generating thousands of vertices and tens of thousands of edge connections in memory every few seconds. In the actual node codebase (usually written in Rust), this is represented across three parallel memory structures:</p><ol><li><strong>The DAG Map:</strong> A massive concurrent hash map keyed by {validator_id, round} pointing to the vertex metadata.</li><li><strong>The Batch Store:</strong> The actual raw transaction payloads referenced by the DAG.</li><li><strong>The Pending Transactions Map:</strong> User transactions waiting to be packed into a vertex.</li></ol><p>If the node does not aggressively and safely prune these three structures, the memory heap balloons. A malicious actor can execute an <strong>Equivocation DoS Attack</strong>. By intentionally broadcasting conflicting uncertified vertices, the attacker forces honest nodes to store multiple branching histories in RAM. Because the protocol cannot immediately discard equivocations without violating liveness, the graph bloats exponentially, triggering the OS OOM killer and crashing the validator.</p><h3>3. Watermark Garbage Collection (The Implementation Subtlety)</h3><p>You cannot simply delete a vertex from memory just because the consensus thread successfully ordered it. Because the network is asynchronous, a slower, honest validator might still be constructing a path that references that exact vertex. If you drop it from RAM prematurely, you break the causal history for your peers, effectively acting as a Byzantine (faulty) node.</p><p>To safely prune the graph without breaking the network, elite implementations use <strong>Watermark Garbage Collection</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YCf3Ujz_CvcVSSCDCXf0vg.png" /></figure><p>The memory manager must track the state across the graph using two strict boundaries:</p><ul><li><strong>The High Watermark:</strong> The latest round that the consensus engine has successfully committed locally.</li><li><strong>The Low Watermark:</strong> The oldest round that is still needed by the <em>slowest honest validator</em> to maintain liveness.</li></ul><p>The node must continuously calculate the progress of $f+1$ honest validators. The mathematical invariant is strict: <strong>Never GC below the $f+1$ honest validators’ watermarks.</strong> Only when the protocol mathematically guarantees that a vertex falls definitively below the Low Watermark can the memory manager safely unlink the pointers, deallocate the structs from the DAG Map, and hand the memory back to the OS.</p><h3>4. The Concurrency Nightmare: Traversal vs. Pruning</h3><p>Even with a perfect watermark algorithm, the physical act of freeing memory introduces a severe multithreading bottleneck.</p><p>A DAG mempool is a highly concurrent environment. It is constantly being mutated by ingress threads (adding new vertices from peers), traversed by the consensus thread (running topological sorts to commit transactions), and purged by the GC thread.</p><p>If the implementation relies on standard Read-Write locks (RwLock)—for example, locking the entire DAG Map during a GC sweep to prevent data races—the node&#39;s throughput will periodically plummet to zero. When the GC thread takes the write-lock to delete Round $R-50$, the consensus thread is blocked from reading Round $R$, stalling the entire sequencing pipeline.</p><p>To survive mainnet traffic, protocols must abandon standard mutexes and implement <strong>Lock-Free Concurrent Data Structures</strong> combined with advanced memory reclamation strategies like <strong>Epoch-Based Reclamation (EBR)</strong> or <strong>Hazard Pointers</strong> (commonly found in high-performance Rust crates like crossbeam-epoch).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LiwWz-RAfA6b1684tk-0UQ.png" /></figure><p>These paradigms allow the consensus thread to traverse the graph using atomic pointers without ever blocking. When the GC thread identifies a vertex below the Low Watermark, it unlinks it atomically (Logical Unlink), but delays the actual physical memory deallocation until it can mathematically prove no active consensus thread is currently holding a reference to that specific memory address.</p><p>Follow me on <a href="https://x.com/luishsoares">X</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ac41cb2e4a2d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/coinmonks/disaggregating-the-mempool-the-memory-architecture-of-dag-consensus-ac41cb2e4a2d">Disaggregating the Mempool: The Memory Architecture of DAG Consensus</a> was originally published in <a href="https://medium.com/coinmonks">Coinmonks</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The L2 Blind Spot: Why Your Smart Contract Audit Won’t Save You From a Derivation Pipeline Exploit]]></title>
            <link>https://medium.com/@luishrsoares/the-l2-blind-spot-why-your-smart-contract-audit-wont-save-you-from-a-derivation-pipeline-exploit-363d595ff044?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/363d595ff044</guid>
            <category><![CDATA[blockchain-technology]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[ethereum]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Tue, 09 Jun 2026 12:56:59 GMT</pubDate>
            <atom:updated>2026-06-09T12:56:59.765Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WX4Djvorz-2XIca7KzzPOA.png" /></figure><h4>An engineering teardown of node parsing vulnerabilities, the Arbitrum zip-bomb, and why the boundary between L1 and L2 is your highest-risk asset.</h4><p>The industry loves to debate the cryptographic purity of validity proofs versus the game theory of optimistic fault proofs. But while the ecosystem argues over the math at the end of the line, they are collectively ignoring the most fragile component of the entire Layer 2 architecture: the derivation pipeline.</p><p>The math rarely breaks. What breaks is the software loop that reads raw, untrusted bytes from Layer 1, unpacks them, filters out the garbage, and attempts to reconstruct the L2 state.</p><p>Today, we are moving past standard smart contract auditing and looking directly into the belly of the rollup node. We are going to deconstruct how attackers have historically exploited L2 node implementations to spoof state, halt networks, and manipulate finality.</p><p>Code is a commodity. Judgment is a career. As a Senior Protocol Engineer, I can tell you that securing an L2 network requires the judgment to see where the node implementation physically fails under adversarial pressure.</p><h3>1. Anatomy of the Rollup Node Boundary</h3><p>To understand how to break an L2, you must first understand how it breathes. A rollup node (such as the op-node in the OP Stack or Arbitrum&#39;s nitro node) does not have its own independent consensus. It derives its entire reality from Layer 1.</p><p>The derivation pipeline is a deterministic, unidirectional state machine. It scans L1 blocks, extracts compressed transaction batches posted by the L2 sequencer, and simultaneously captures user deposits originating from L1 smart contracts. It then merges these two streams into payload attributes and hands them to the L2 execution engine (like op-geth) to build the block.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y7SPM2bSXx-KzmQ8A0DdQg.png" /></figure><p>Because L2s utilize modified execution clients to handle these L1-to-L2 bridges, the boundary between the standard EVM logic and the custom data-parsing logic is the ultimate attack surface.</p><h3>2. The Arbitrum Zip-Bomb: Exploiting Node Decompression</h3><p>Rollups rely heavily on data compression to minimize the exorbitant costs of posting calldata or blobs to Ethereum. But compression algorithms introduce severe risk when the node has to unpack that data on the other side.</p><p>Arbitrum relies on a dual-finality model. Transactions achieve “soft finality” when processed by the sequencer and “hard finality” when posted and derived from L1. Many cross-chain bridges act on soft finality in the interest of speed. If an attacker can force the L2 node to reject a batch <em>after</em> soft finality, they can trigger a massive state rollback and execute a cross-chain double-spend.</p><h3>The Historical Exploit Vector</h3><p>Researchers recently uncovered the <strong>Zip-Bomb Attack</strong>, a devastating vulnerability targeting Arbitrum’s transaction decompression mechanism.</p><p>To execute the attack, an adversary floods the sequencer with highly compressible transactions (e.g., payloads padded entirely with zeros). The sequencer soft-finalizes these transactions, compresses them into a tiny batch, and submits them to L1.</p><p>When the honest L2 nodes pull this batch from L1 and run it through the derivation pipeline’s decompression engine, the payload explodes in size. Because of a bug in the node’s implementation limits, the decompressed batch exceeds the protocol’s permitted memory footprint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KJc6tl6MSKsXXnfrDWTYvA.png" /></figure><p>The L2 node panics, deems the batch invalid, and rolls back all associated soft-finalized transactions. The attacker successfully withdraws funds on a target chain via the bridge, while their initial deposit on Arbitrum is completely erased from history. The attack succeeds purely by breaking the derivation parser.</p><h3>3. The Unbounded Deposit DoS: Halting Consensus via Gas</h3><p>Rollup derivation pipelines are designed with a strict protocol invariant to maintain censorship resistance: any deposit made on L1 <em>must</em> be included in the L2 chain within a specific timeframe (the sequencing window).</p><p>Furthermore, within any given L2 block, L1-originated deposits must be executed <em>before</em> any sequencer-submitted L2 transactions.</p><h3>The Historical Exploit Vector</h3><p>Early iterations of optimistic rollups suffered from a severe architectural flaw regarding the collision between this censorship-resistance mandate and L2 block gas limits.</p><p>An attacker could monitor the L1 chain and systematically spam the L1 Portal contract with massive, highly computationally expensive deposit transactions. When the L2 node’s derivation pipeline scanned the L1 block, it faithfully extracted these deposits. Following protocol rules, it attempted to bundle all these mandatory L1 deposits into a single L2 payload to hand to the execution engine.</p><ul><li><strong>The Consensus Client (Derivation) mandated:</strong> <em>“You must include these transactions immediately to satisfy L1 inclusion rules.”</em></li><li><strong>The Execution Client (EVM) responded:</strong> <em>“I cannot build a block this large; it violates the hard-coded block gas limit.”</em></li></ul><p>The node hit an irreconcilable conflict. The derivation pipeline stalled, the node crashed, and the entire L2 network halted. Mitigating this required deep architectural changes to the node implementation, enforcing strict resource metering limits on the L1 Portal to ensure the L1 ingress rate could never mathematically exceed the L2 block capacity.</p><h3>4. Address Aliasing: The L1-to-L2 Impersonation Threat</h3><p>When a user interacts with a standard smart contract on L2, msg.sender is cryptographically verified by the execution engine via the transaction signature. However, when a transaction is bridged from L1 to L2 (a deposit), there is no signature on L2. The execution engine blindly trusts the msg.sender provided by the derivation pipeline.</p><p>To prevent an L1 smart contract from bridging over and impersonating an L2 smart contract with the exact same address, rollups implement <strong>Address Aliasing</strong>. The derivation node intercepts the L1 contract address and applies a mathematical offset (e.g., adding 0x1111000000000000000000000000000000001111).</p><h3>The Exploit Vector</h3><p>This boundary between L1 message parsing and L2 execution has been the site of numerous critical vulnerabilities. If the derivation pipeline’s parsing logic has a flaw — such as misidentifying an Externally Owned Account (EOA) as a contract, or failing to apply the alias mask under specific edge cases (like a zero-value internal call) — an attacker can bypass the alias.</p><p>If the alias is bypassed, an attacker can use CREATE2 on L1 to deploy a malicious contract with the exact same address as a critical system contract on L2 (like a token bridge or governance module). They send a crafted deposit, the derivation pipeline fails to alias it, and the L2 execution engine processes the transaction as if it originated from the highly privileged L2 system contract. The attacker is instantly granted admin privileges and drains the protocol.</p><h3>The Perimeter is the Parser</h3><p>When assessing the security of an L2 infrastructure, do not stop at the smart contracts. A vulnerability in the derivation pipeline does not require an attacker to steal a private key, find a reentrancy bug, or manipulate a liquidity curve.</p><p>It only requires an adversary who understands how to format bytes on Layer 1 that the Layer 2 node cannot safely digest.</p><p>Building secure interoperability infrastructure requires systems-level paranoia. Because out here, judgment is the only thing protecting the state.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=363d595ff044" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Breaking the Bedrock: Adversarial Testing in P2P Node Architectures]]></title>
            <link>https://medium.com/@luishrsoares/breaking-the-bedrock-adversarial-testing-in-p2p-node-architectures-9c5febcd9c88?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/9c5febcd9c88</guid>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Mon, 08 Jun 2026 10:08:00 GMT</pubDate>
            <atom:updated>2026-06-08T10:08:00.910Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GbkUjl4lnfjmhjOv6jgOJA.png" /></figure><p>The Web3 security narrative is broken. It suffers from an extreme case of application-layer bias. Spend an hour reading industry research or scanning vulnerability disclosure feeds, and you will find an endless stream of post-mortems on smart contract reentrancy, flash-loan economic manipulation, and cross-chain bridge validation slips.</p><p>While application-layer hacks are painful, they are ultimately localized events. If a smart contract leaks funds, an enterprise or a community suffers. But if the underlying node client implementation fails, the entire network halts, splits, or falls under the consensus control of an adversary.</p><p>Writing a protocol specification is a mathematical and theoretical exercise. Implementing that specification into a high-performance, asynchronous, multi-threaded networking daemon operating over a hostile, open internet is where reality hits the code. Secure protocol engineering is the exercise of professional judgment under the worst possible network conditions. To defend these systems, we must move beyond checking bytecodes and look at how an adversary actively deconstructs a distributed system at the node level.</p><h3>The Node Architecture Matrix</h3><p>To successfully attack or harden a node client, you cannot treat it as a monolithic program. A modern Layer-1 or Layer-2 client — whether written in Rust, Go, C++, or Haskell — is a complex orchestration of asynchronous state machines passing messages over memory-bounded channels.</p><p>A standard node architecture segments responsibilities into distinct sub-engines:</p><ul><li><strong>The P2P Engine:</strong> Handles neighbor discovery, cryptographic handshakes, peer rating, and network topology enforcement.</li><li><strong>The Multiplexer &amp; Fabric:</strong> Manages low-level socket I/O, routing incoming network streams to specific protocol handlers.</li><li><strong>The Mempool Ingress:</strong> Filters, validates, and queues unconfirmed transactions from untrusted external sources.</li><li><strong>The Consensus Engine:</strong> Evaluates slot leadership, builds blocks, runs Verifiable Random Functions (VRFs), and manages fork choice rules.</li><li><strong>The State Transition/Execution Engine:</strong> Executes transactions against the global database (EVM, Wasm, eUTXO ledger) and updates the state trie or tree.</li></ul><p>An experienced adversary does not look for an elegant mathematical flaw in the whitepaper; they look for the resource allocation boundaries between these five engines and exploit the asymmetries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TC1iKvnQdm8DP1OI7k-x1g.png" /></figure><h3>1. Network Layer Ingress: Eclipse Attacks &amp; Topology Poisoning</h3><p>Before an adversary can exploit a node’s internal state transition engine, they must first control what information that node is allowed to see. If an attacker can isolate a node from the honest network, the game is won before a block is ever validated.</p><h3>The Attack Vector: DHT Table Manipulation &amp; Inbound Slot Exhaustion</h3><p>Most decentralized networks utilize structural variations of the <strong>Kademlia Distributed Hash Table (DHT)</strong> for peer discovery (such as Ethereum’s discv5 or Libp2p primitives). Nodes find peers by calculating the cryptographic distance—using an $XOR$ metric—between their own node ID hash and target IDs.</p><p>An advanced adversary launches a targeted <strong>Eclipse Attack</strong> through precise ID generation. By repeatedly hashing public keys (similar to a proof-of-work grind), the attacker generates dozens of distinct Node IDs whose $XOR$ distance places them directly into the target node’s routing table buckets.</p><p>Once the routing table is seeded with these malicious entries, the attacker floods the target with lookup responses populated exclusively by alternative attacker-controlled IPs.</p><p>Simultaneously, the attacker consumes all available inbound connection slots on the target node by initiating thousands of low-overhead cryptographic handshakes. When an honest peer drops off due to regular network churn, the attacker immediately fills the open slot. Once the target node’s inbound and outbound connection slots are saturated by the malicious collective, the node is completely blind to the true tip of the blockchain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PRcg_dsK8D95GOaqpdPLMg.png" /></figure><h3>The Adversarial Playbook: How to Verify</h3><ul><li><strong>Sybil-Resistance Boundary Simulation:</strong> You must validate the node’s peer governor using synthetic network isolation tests. Run the node client within an environment where 90% of discoverable network identifiers are malicious. Verify that the node reserves a hard set-aside ratio (e.g., 20%) of outbound connection slots exclusively for hardcoded, trusted bootnodes or historically stable peers that cannot be rotated out by DHT gossip.</li><li><strong>File Descriptor &amp; Socket Exhaustion Testing:</strong> Intentionally initiate connection handshakes, then freeze the TCP pipeline mid-way through negotiation (a classic Slowloris approach on Web3). Monitor whether the peer manager leaks socket descriptors or stalls the thread pool while waiting for timeouts.</li></ul><h3>2. Mini-Protocols &amp; Multiplexer Bandwidth Starvation</h3><p>Monolithic blockchains often blast data over an un-metered P2P socket. Advanced modern architectures (such as Substrate/Polkadot or Cardano’s ouroboros-network) use stateful <strong>mini-protocols</strong> multiplexed over a single TCP connection. This means block fetching, transaction gossip, and keep-alive heartbeats share a single pipe.</p><h3>The Attack Vector: Priority Inversion &amp; Channel Choking</h3><p>When multiple virtual communication streams share a single physical connection, the multiplexer must strictly enforce channel priority rules. Block headers and consensus votes must always take precedence over transaction gossip or peer telemetry.</p><p>An adversary exploits a flaw in this design by initiating a legitimate network handshake and then flooding the lowest-priority channel — such as mempool transaction gossip — with an immense volume of structurally flawless data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z6cK38LZFv84lFQKRZ6XTg.png" /></figure><p>If the multiplexer allocates buffer space or CPU scheduling based on incoming packet count rather than strict channel weight rules, a <strong>Priority Inversion</strong> occurs. The node’s CPU threads become fully occupied reading, unpacking, and queueing transaction data from the socket buffer.</p><p>As a result, critical block header packets arriving on the high-priority consensus channel are delayed or dropped at the OS socket level. The node remains online, but it begins falling behind the network tip, causing its own block production slots to miss deadlines.</p><h3>The Adversarial Playbook: How to Verify</h3><ul><li><strong>State Machine Fuzzing:</strong> Build property-based tests that intentionally feed out-of-order state transitions down the multiplexer. If a mini-protocol is in an Idle state, and an attacker sends a high-volume burst of payload data without a preceding request, the node must not parse or buffer the data. It must immediately terminate the entire TCP connection and slash the peer&#39;s reputation score.</li><li><strong>Bandwidth Asymmetry Auditing:</strong> Simulate highly saturated connections using network utility tools like tc (traffic control) to inject jitter and latency. Confirm that consensus messages can still break through even when transaction gossip channels are saturated to 150% of their designed capacity.</li></ul><h3>3. The Mempool Ingress: Resource Asymmetry Attacks</h3><p>The mempool is the absolute front line of a blockchain node. It is a memory buffer that ingests raw, unverified transactions directly from the public internet. Because it handles un-authenticated inputs before any state finality occurs, it represents a massive surface for resource exhaustion attacks.</p><h3>The Attack Vector: Asymmetric Execution Costs</h3><p>An attacker looks for execution paths where the computational cost to validate a transaction’s wire-format validity is monumentally higher than the fee required to submit it, or where validation fails <em>after</em> maximum work has been performed.</p><p>Consider an EVM or Wasm-based execution client. An attacker distributes millions of transactions via P2P gossip that contain structurally valid metadata but feature invalid cryptographic signatures, or signatures that purposefully trigger complex cryptographic precompiles (such as modular exponentiation or pairing checks on alternative curves).</p><p>When these transactions land in the node’s ingress pipeline, the node is forced to immediately allocate CPU cycles to check the signature and verify that the sender’s account holds enough native balance to cover the gas limit.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c1SB5aUwXmZylG5MoN83Gw.png" /></figure><p>Because the signature or balance check fails during the ingress phase, the transaction is dropped and never written to a block. The attacker pays exactly zero gas fees because the transaction never executes on-chain. Meanwhile, the target node’s CPU cores are pinned at 100% capacity processing these invalid signatures, rendering it completely incapable of sorting and propagating legitimate transactions.</p><h3>The Adversarial Playbook: How to Verify</h3><ul><li><strong>Algorithmic Complexity Fuzzing:</strong> Construct an automated test harness that generates mutated transaction payloads targeting the mempool ingress. Measure the processing time of rejected items down to the microsecond. If the node spends an asymmetric amount of time validating a transaction that it ultimately drops without collecting a fee, the ingress pipeline lacks proper rate-limiting heuristics.</li><li><strong>Dynamic Gas-Filtering Audits:</strong> Verify that the node implements a prioritized triage queue, where transactions from addresses with verifiable on-chain history and high gas-price commitments are processed ahead of un-vetted, high-frequency inputs.</li></ul><h3>4. State Transitions: Serialization &amp; Implementation Splintering</h3><p>The ultimate failure state of a layer-1 protocol is an un-planned chain split — a non-deterministic state transition where different nodes running the exact same protocol parameters interpret a specific transaction differently, fracturing the consensus state into two irreconcilable realities.</p><h3>The Attack Vector: Serialization Edge Cases &amp; Parsing Divergence</h3><p>Nodes rely heavily on high-speed binary serialization formats like Recursive Length Prefix (RLP), Concise Binary Object Representation (CBOR), or Protocol Buffers to compress data for P2P transport. When a block is received, it must be unpacked into local memory objects before execution.</p><p>An attacker constructs a block containing a highly specialized transaction layout featuring deeply nested arrays, duplicated keys, or negative length fields designed to trigger integer overflows inside the deserialization engine.</p><p>The security danger emerges from language runtime differences across multi-client networks (such as an ecosystem running clients written in Go, Rust, and C++):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aZaVflzf5E3C4_rfFijOZQ.png" /></figure><p>If half of the network’s validators run a client that catches the exception and rejects the block, while the other half runs a client that panics, crashes, or parses the malformed data as a default value, the network state fragments instantly.</p><h3>The Adversarial Playbook: How to Verify</h3><ul><li><strong>Differential Fuzzing (The Gold Standard):</strong> Take a single canonical block, apply structured mutation fuzzing to its serialized byte arrays, and feed these identical mutated streams simultaneously into distinct instances of different client software versions.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SKtLm_AYcHxOk_XmBlvAtQ.png" /></figure><p>Any divergence in the execution outcome — where one node flags the block as invalid, another crashes, and a third processes it — reveals an implementation splinter that would trigger an immediate consensus breakdown in production.</p><h3>The Hardening Blueprint: Deterministic Simulation</h3><p>Standard integration testing environments fail completely when applied to distributed P2P software. Unit tests cannot simulate a race condition inside a network socket under a 50% packet-loss environment. To achieve true protocol validation, engineering teams must deploy <strong>Deterministic Simulation Testing (DST)</strong>.</p><p>By leveraging advanced simulation frameworks (such as Cardano’s IOSim library or enterprise-grade platforms like Antithesis), developers can execute entire multi-node local networks inside a deterministic sandbox.</p><p>Within these environments:</p><ul><li>Time is completely synthetic and controlled by the testing engine.</li><li>The simulator can intentionally drop packets, reorder network frames, inject thread scheduling delays, and force disk I/O write failures.</li><li>If an adversarial sequence triggers a node crash or an invalid state transition, the simulator can replay the exact sequence of events down to the nanosecond and CPU instruction level.</li></ul><h3>Judgment Over Commodity</h3><p>Hardening a protocol node requires moving past standard software testing models. You cannot run a standard static analysis security scanner against a distributed state machine and expect it to identify a multiplexer priority inversion or a Kademlia isolation vector.</p><p>Validating these systems requires a deeply ingrained adversarial mindset: constructing custom mutation engines, orchestrating multi-client differential fuzzing campaigns, and aggressively seeking out resource asymmetries at every architecture boundary.</p><p>Code can always be refactored, and code generation can be optimized. But possessing the architectural judgment to anticipate how an adversary will exploit asynchronous networking constraints to break consensus — that is what separates a standard developer from a true senior protocol security engineer.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9c5febcd9c88" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The $292M Kelp DAO Hack: Why the Default Is the Architecture]]></title>
            <link>https://medium.com/@luishrsoares/the-292m-kelp-dao-hack-why-the-default-is-the-architecture-f0f04f77817f?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/f0f04f77817f</guid>
            <category><![CDATA[defi]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[dao]]></category>
            <category><![CDATA[architecture]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Thu, 28 May 2026 11:35:26 GMT</pubDate>
            <atom:updated>2026-05-28T11:35:26.108Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WGHg9w-puYpfpNCZn6nEYQ.png" /></figure><p><em>Code is a commodity. Judgment is a career.</em></p><p>On April 18, 2026, attackers drained roughly <strong>$292 million</strong> — 116,500 rsETH, nearly 18% of the token’s circulating supply — from Kelp DAO’s cross-chain bridge. It was the largest DeFi exploit of the year, and blockchain analytics firms attributed it to North Korea’s Lazarus Group.</p><p>Here is the part that should keep every protocol architect up at night: <strong>this was not a smart contract bug.</strong> No reentrancy. No integer overflow. No unchecked external call. The contracts did exactly what they were written to do. The bridge released funds because, by the rules of its own configuration, the funds were authorized to be released.</p><p>The exploit didn’t break the code. It walked through a door the architecture left open — a door that, it turns out, roughly <strong>half the ecosystem</strong> had also left open, because it shipped that way by default.</p><p>This is a teardown of how a single trust assumption took down a nine-figure protocol, and why the most dangerous line in your system isn’t a line of code at all. It’s a default.</p><h3>The Anatomy of the Drain</h3><p>Kelp’s rsETH bridge ran on LayerZero, whose security model rests on <strong>DVNs</strong> — Decentralized Verifier Networks. A DVN’s job is to independently attest that a cross-chain message (“release these funds”) genuinely originated on the source chain before the destination contract acts on it. The security of the whole bridge reduces to a simple question: <em>how many independent parties must agree before money moves?</em></p><p>Kelp’s answer was <strong>one.</strong></p><p>The rsETH pathway was configured as a <strong>1-of-1 DVN</strong> — a single verifier, LayerZero Labs’ own DVN, as the sole arbiter of truth for inbound messages. That is not redundancy. That is a single point of failure with a longer name.</p><p>The attackers understood this precisely, and engineered the rest of the attack around the off-chain infrastructure feeding that one verifier:</p><ol><li><strong>They compromised two RPC nodes</strong> that served as the verifier’s data source for reading source-chain state.</li><li><strong>They DDoS’d the legitimate RPC nodes offline</strong>, forcing the verifier to rely solely on the attacker-controlled nodes as the sole source of truth.</li><li><strong>They forged a cross-chain message.</strong> With no independent verifier to disagree, the lone DVN attested to it as valid, and the bridge released 116,500 rsETH to an attacker-controlled address.</li><li><strong>The malware on the RPC nodes deleted itself and its logs</strong>, slowing forensic reconstruction.</li></ol><p>Notice what’s absent from that chain: any flaw in the Solidity. Every component honored its contract. The verifier verified. The bridge released on a valid attestation. The system worked — it just had nothing to check the one voice it was listening to.</p><h3>The Real Vulnerability Was a Default</h3><p>It is tempting to file this under “Kelp misconfigured their bridge” and move on. That framing is comforting because it’s local: one team, one mistake, one lesson. It is also wrong, and the post-incident fight is what makes this exploit worth studying.</p><p>LayerZero’s initial post-mortem blamed the integrator — Kelp had ignored the multi-DVN redundancy that LayerZero said it recommended to partners.</p><p>Kelp’s rebuttal was devastating: LayerZero’s <strong>own quickstart guide and default GitHub configuration pointed to a 1/1 DVN setup.</strong> A LayerZero team member had even signed off on it in writing — <em>“No problem on using defaults either…”</em> And the kicker, from on-chain analysis: somewhere between <strong>40% and 47% of LayerZero OApps were running the same minimal-DVN configuration.</strong></p><p>So this was never one team’s careless mistake. It was the <strong>path of least resistance, blessed by the platform, adopted by nearly half the ecosystem.</strong> Kelp’s only error was the one that almost everyone made: trusting the default.</p><p>To its credit, LayerZero eventually owned it. The company issued a public apology, conceded <em>“we made a mistake by allowing our DVN to act as a 1/1 DVN for high-value transactions,”</em> stopped servicing 1/1 configurations entirely, and moved defaults toward <strong>at least five verifiers, with a floor of three</strong> where chains support it. Kelp migrated rsETH to Chainlink CCIP.</p><p>Those are the right fixes. But they arrived $292 million late, and only because an attacker found the door before a defender did.</p><h3>The Thesis: A Default Is a Recommendation With Authority Behind It</h3><p>The deep failure here was not technical. It was a <strong>diffusion of responsibility across a trust boundary.</strong></p><p>The platform shipped an insecure default and called it <em>developer choice.</em> The integrator accepted the default and called it <em>vetted by the platform.</em> Each party assumed the other owned the trust assumption. Neither did. And so the single most security-critical parameter in the entire system — <em>how many independent parties must agree before nine figures move</em> — was set by no one’s deliberate judgment. It was set by inertia.</p><p>This is the lesson that generalizes far beyond LayerZero:</p><blockquote><strong><em>Every default is an architectural decision made on the user’s behalf. When a platform sets a default, it is not offering a neutral starting point — it is making a recommendation with its full authority behind it, and most integrators will treat it as one.</em></strong></blockquote><p>“We believe developers should choose their own security configurations” is a fine principle and a terrible defense, because it pretends a default is neutral. It isn’t. A default that requires expertise to recognize as dangerous, and effort to override, will be adopted by everyone who lacks that expertise or that time — which, the on-chain data tells us, was nearly half the ecosystem.</p><p>You do not get to ship the gun loaded, point it at the user’s foot, and call the trigger “configurable.”</p><h3>Engineering the Guardrails</h3><p>If you are building or integrating cross-chain infrastructure, here is how the trust boundary should actually be architected.</p><p><strong>1. No single-entity verification for value-bearing paths — ever.</strong> A bridge’s security ceiling is the number of <em>independent</em> parties that must collude or be compromised to forge a message. One verifier is a ceiling of one. Require a multi-DVN quorum (an <em>m-of-n</em> with n ≥ 3, and meaningfully m &gt; 1) drawn from <strong>operationally independent</strong> verifiers — different operators, different infrastructure, different jurisdictions. Diversity is the entire point; five DVNs run by one team is still a single point of failure wearing a costume.</p><p><strong>2. Verification requirements must scale with value at risk.</strong> A static security configuration is wrong by construction, because a $1,000 message and a $290M message do not warrant the same paranoia. Architect <strong>value-tiered verification</strong>: low-value transfers can clear under a lighter quorum, while transfers above a threshold programmatically require a larger, more diverse verifier set — and, ideally, a timelock that gives off-chain monitoring a window to flag anomalies before settlement.</p><p><strong>3. Harden the data layer the verifiers trust.</strong> The attack didn’t subvert the verifier’s logic; it poisoned the verifier’s <em>inputs</em> by controlling RPC nodes and silencing the honest ones. A verifier reading from a single RPC source is a 1-of-1 system one layer down. Verifiers must read from <strong>redundant, independent RPC endpoints</strong>, cross-check their responses, and treat the sudden unavailability of honest sources (a DDoS) as a <strong>fail-closed signal</strong> — halt and escalate, never fall through to whatever node is still answering.</p><p><strong>4. Make the secure configuration the default — and make insecurity loud.</strong> This is the obligation that sits with the <em>platform</em>, not the integrator. The safe configuration should be the one you get by doing nothing. Anything that introduces a single point of failure for high-value paths should be impossible to enable silently — it should demand an explicit, friction-laden, logged override that no one stumbles into by copying a quickstart. The lesson LayerZero learned the expensive way: if your insecure mode requires expertise to <em>recognize</em>, you have already lost the users who needed protecting most.</p><h3>Conclusion</h3><p>The next nine-figure exploit won’t necessarily be a clever break in someone’s code. Increasingly, it will be a perfectly correct system faithfully executing a trust assumption that no human ever consciously chose — inherited from a default, propagated by a quickstart, ratified by inertia.</p><p>Securing these systems requires more than auditing syntax. It requires asking, of every trust boundary: <em>who actually owns this assumption, and did anyone decide it on purpose?</em> At Kelp, the answer was no one — and $292 million found the gap before a defender did.</p><p>Audit your defaults like an attacker reads them: as the configuration you’ll actually be running in production.</p><p>Code is a commodity. Judgment is a career.</p><p><em>Sources: Chainalysis (“Inside the KelpDAO Bridge Exploit”), Halborn (“Explained: The Kelp DAO Hack”), LayerZero (“KelpDAO Incident Statement”), The Block (LayerZero public apology), CoinDesk (Kelp/LayerZero dispute reporting), The Defiant (Dune analysis of LayerZero OApp DVN configurations).</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f0f04f77817f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ownership, Mutability, Signers: Solana Thinks in Rust]]></title>
            <link>https://medium.com/@luishrsoares/ownership-mutability-signers-solana-thinks-in-rust-158186928275?source=rss-5176d5a50f58------2</link>
            <guid isPermaLink="false">https://medium.com/p/158186928275</guid>
            <category><![CDATA[rust-programming-language]]></category>
            <category><![CDATA[solana-network]]></category>
            <category><![CDATA[rust]]></category>
            <category><![CDATA[solana-blockchain]]></category>
            <dc:creator><![CDATA[Luis Soares]]></dc:creator>
            <pubDate>Tue, 24 Mar 2026 07:56:03 GMT</pubDate>
            <atom:updated>2026-03-24T07:56:03.935Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/721/1*97Womb9CPHC2fh-Nofs9mQ.png" /></figure><p>If you already know Rust, you’re closer to understanding Solana’s runtime than you think. Not because both are written in Rust — that’s table stakes — but because the <em>conceptual model</em> Solana uses to manage on-chain state is a direct structural echo of how Rust thinks about memory: single ownership, explicit lifetimes, and a strict separation between data and the code that transforms it.</p><p>This article explores that mapping in depth. By the end, you won’t just know <em>how</em> to write a Solana program — you’ll understand <em>why</em> the mental model feels native to a Rust engineer the moment it clicks.</p><h3>Rust’s Ownership Model in 60 Seconds</h3><p>Before we get to Solana, let’s crystallize what ownership actually means in Rust, because the language gets a lot of credit for “memory safety” without people quite articulating the <em>mechanism</em>.</p><p>Rust enforces three invariants at compile time:</p><ol><li><strong>Every value has exactly one owner.</strong> When the owner goes out of scope, the value is dropped.</li><li><strong>You can have either one mutable reference or any number of immutable references — never both simultaneously.</strong></li><li><strong>References cannot outlive the value they point to.</strong> Lifetimes encode this constraint in the type system.</li></ol><p>The result is a model where <em>aliasing and mutation are mutually exclusive</em>. You either have exclusive write access, or shared read access. Never both.</p><p>This isn’t just about safety. It’s about <em>reasoning</em>. In a system where those invariants hold, you can look at any piece of code and know exactly who can change a value, when, and how.</p><h3>Solana’s Account Model: The Same Intuition, On-Chain</h3><p>Solana programs don’t store state inside themselves. There is no self.balance or contract.storage[key]. Instead, all state lives in <strong>accounts</strong> — discrete chunks of allocated memory on the Solana ledger.</p><p>A program is just a stateless function. It receives a list of accounts, reads from them, mutates some of them (if authorized), and returns. Sound familiar?</p><p>Here’s the formal structure of an account:</p><pre>pub struct AccountInfo&lt;&#39;a&gt; {<br>    pub key: &amp;&#39;a Pubkey,         // The account&#39;s unique address<br>    pub lamports: Rc&lt;RefCell&lt;&amp;&#39;a mut u64&gt;&gt;,  // SOL balance<br>    pub data: Rc&lt;RefCell&lt;&amp;&#39;a mut [u8]&gt;&gt;,     // Raw byte state<br>    pub owner: &amp;&#39;a Pubkey,      // Program that &quot;owns&quot; this account<br>    pub is_signer: bool,        // Did this key sign the transaction?<br>    pub is_writable: bool,      // Is mutation allowed?<br>    pub executable: bool,       // Is this account a deployed program?<br>    pub rent_epoch: Epoch,<br>}</pre><p>Three fields in AccountInfo should immediately jump out to you.</p><h3>owner: The Ownership Invariant</h3><p>Just like a Rust value has exactly one owner, every Solana account has exactly one <strong>program owner</strong> — the program whose address is stored in the owner field. Only the owning program is allowed to modify the account&#39;s data or debit its lamports.</p><p>This isn’t enforced by convention. The Solana runtime checks it. If your program tries to write to an account it doesn’t own, the transaction fails with a privilege violation. Same guarantee as Rust’s borrow checker — just enforced at runtime by a distributed validator network instead of at compile time by the compiler.</p><h3>is_writable: The Mutability Flag</h3><p>Rust says: you can have either &amp;mut T (exclusive mutation) or &amp;T (shared read). Solana says the same thing through is_writable. When a transaction is submitted, every account is declared as writable or read-only upfront. The runtime uses this declaration to determine which transactions can safely execute in parallel — accounts where no two concurrent transactions need a mutable reference can run simultaneously.</p><p>This is Solana’s answer to Rust’s aliasing XOR mutation rule, applied at the transaction scheduler level. It’s what makes Solana’s parallel execution model possible in the first place.</p><h3>is_signer: Authorization as a Type-Level Guarantee</h3><p>In Rust, if a function takes &amp;mut T, the compiler enforces that the caller actually has a mutable reference to provide. The type system encodes authorization. In Solana, is_signer plays an analogous role — it encodes that the private key corresponding to this account&#39;s pubkey actually signed the transaction. Programs that require a signer check it explicitly:</p><pre>if !ctx.accounts.user.is_signer {<br>    return Err(ProgramError::MissingRequiredSignature);<br>}</pre><p>Without this check, anyone could pass any public key in that account slot and your program would accept it. The pattern is manual where Rust’s borrow checker is automatic — but the <em>intent</em> is identical.</p><h3>Program Derived Addresses: Deterministic Ownership Without a Private Key</h3><p>Here’s where the ownership analogy gets really interesting.</p><p>In Rust, you often create values in a deterministic way: a Vec&lt;u8&gt; with a specific capacity, a struct initialized from a seed value. The <em>identity</em> of the value follows from how it was constructed.</p><p>Solana has an equivalent mechanism called <strong>Program Derived Addresses (PDAs)</strong>. A PDA is an account whose address is derived deterministically from a program ID and a set of seeds, with no corresponding private key. Because no private key exists, only the program that derived the address can sign for it (using a technique called a “bump seed” that pushes the address off the Ed25519 curve).</p><pre>let (pda, bump_seed) = Pubkey::find_program_address(<br>    &amp;[b&quot;vault&quot;, user_pubkey.as_ref()],<br>    &amp;program_id,<br>);</pre><p>This means a PDA is an account that is <em>owned by a program in a cryptographically enforced sense</em>. No external actor can forge a signature for it. The program that derived the PDA is the only entity that can authorize changes to it.</p><p>Think of it as a Box&lt;T&gt; that the program allocated on the ledger heap, with the provenance of that allocation encoded in the address itself.</p><h3>Anchor: Adding the Type System Layer</h3><p>Writing native Solana programs requires manual account validation — checking owners, verifying signers, ensuring accounts are writable before you touch them. It’s correct but verbose. It’s also where most Solana security vulnerabilities originate: a missing ownership check, a forgotten signer assertion.</p><p><a href="https://www.anchor-lang.com/"><strong>Anchor</strong></a> adds a type system layer over the raw account model, restoring the compile-time safety you’re used to from Rust.</p><pre>#[derive(Accounts)]<br>pub struct Deposit&lt;&#39;info&gt; {<br>    #[account(<br>        mut,<br>        has_one = owner,<br>    )]<br>    pub vault: Account&lt;&#39;info, Vault&gt;,<br>    #[account(mut)]<br>    pub owner: Signer&lt;&#39;info&gt;,<br>    pub system_program: Program&lt;&#39;info, System&gt;,<br>}</pre><p>Let’s decode what Anchor is doing here in Rust terms:</p><ul><li>Account&lt;&#39;info, Vault&gt; — This is a typed reference with a lifetime. Anchor deserializes the raw [u8] data into a Vault struct and verifies that the account&#39;s owner field matches the program ID. Like dereferencing a typed pointer with an ownership check baked in.</li><li>Signer&lt;&#39;info&gt; — This is a wrapper that asserts is_signer == true. It&#39;s the equivalent of a type that can only exist if the caller provided a valid mutable reference.</li><li>has_one = owner — A constraint that enforces referential integrity: the vault.owner field must match the owner account&#39;s public key. It&#39;s a foreign key check encoded as a type constraint.</li><li>mut — Maps directly to is_writable. Anchor generates the check.</li></ul><p>The #[account] attribute on handler functions then generates the deserialization, ownership checks, and signer assertions automatically — much like the borrow checker generates the memory safety guarantees you&#39;d otherwise have to enforce manually.</p><h3>Ready to Build on Solana?</h3><blockquote><em>If this architecture is clicking for you and you want to go from article to production-grade Solana developer, check out the </em><a href="https://metana.io/web3-rust-bootcamp-solana-blockchain/?utm_source=luis-medium-article&amp;utm_medium=referrals&amp;utm_content=blog-post-link&amp;utm_campaign=luis-medium-article"><strong><em>Metana Web3 Rust + Solana Bootcamp</em></strong></a><em>. It’s a structured, hands-on program that takes you from Rust fundamentals through native Solana programs, Anchor, DeFi protocols, and beyond — the fastest path I’ve seen to being able to ship real on-chain Rust code.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OYgwAiz1TJUrojTnws7yow.png" /><figcaption><a href="https://metana.io/web3-rust-bootcamp-solana-blockchain/?utm_source=luis-medium-article&amp;utm_medium=referrals&amp;utm_content=blog-post-link&amp;utm_campaign=luis-medium-article"><strong><em>Metana Web3 Rust + Solana Bootcamp</em></strong></a></figcaption></figure><h3>A Minimal Native Program: Seeing the Whole Picture</h3><p>Let’s write a minimal native Solana program to make this concrete. No Anchor — raw Solana SDK so you can see every invariant enforced explicitly.</p><pre># Cargo.toml<br>[package]<br>name = &quot;counter&quot;<br>version = &quot;0.1.0&quot;<br>edition = &quot;2021&quot;<br><br>[lib]<br>crate-type = [&quot;cdylib&quot;, &quot;lib&quot;]<br><br>[dependencies]<br>solana-program = &quot;1.18&quot;<br>borsh = &quot;1.4&quot;<br>borsh-derive = &quot;1.4&quot;</pre><pre>// src/lib.rs<br>use borsh::{BorshDeserialize, BorshSerialize};<br>use solana_program::{<br>    account_info::{next_account_info, AccountInfo},<br>    entrypoint,<br>    entrypoint::ProgramResult,<br>    msg,<br>    program_error::ProgramError,<br>    pubkey::Pubkey,<br>};<br><br>/// On-chain state: a simple u64 counter<br>#[derive(BorshSerialize, BorshDeserialize, Debug)]<br>pub struct CounterState {<br>    pub count: u64,<br>    pub authority: Pubkey,<br>}<br><br>entrypoint!(process_instruction);<br><br>pub fn process_instruction(<br>    program_id: &amp;Pubkey,<br>    accounts: &amp;[AccountInfo],<br>    instruction_data: &amp;[u8],<br>) -&gt; ProgramResult {<br>    let accounts_iter = &amp;mut accounts.iter();<br><br>    // Account 0: the counter state account (writable, owned by this program)<br>    let counter_account = next_account_info(accounts_iter)?;<br><br>    // Account 1: the authority who may increment the counter (must be signer)<br>    let authority = next_account_info(accounts_iter)?;<br><br>    // ── Ownership check ───────────────────────────────────────────────────────<br>    // Only this program can write to accounts it owns.<br>    // This is the Solana equivalent of Rust&#39;s ownership invariant.<br>    if counter_account.owner != program_id {<br>        msg!(&quot;Counter account does not belong to this program&quot;);<br>        return Err(ProgramError::IncorrectProgramId);<br>    }<br><br>    // ── Mutability check ──────────────────────────────────────────────────────<br>    // The caller must declare the account writable in the transaction.<br>    // Equivalent to requiring &amp;mut T.<br>    if !counter_account.is_writable {<br>        msg!(&quot;Counter account must be writable&quot;);<br>        return Err(ProgramError::InvalidAccountData);<br>    }<br><br>    // ── Signer check ──────────────────────────────────────────────────────────<br>    // The authority must have signed this transaction.<br>    // Equivalent to proving you hold the unique mutable reference.<br>    if !authority.is_signer {<br>        msg!(&quot;Authority must be a signer&quot;);<br>        return Err(ProgramError::MissingRequiredSignature);<br>    }<br><br>    // ── Deserialize state (typed access to raw bytes) ─────────────────────────<br>    let mut state = CounterState::try_from_slice(&amp;counter_account.data.borrow())?;<br>    <br>    // ── Authorization check ───────────────────────────────────────────────────<br>    // Verify the signer is actually the stored authority - not just *any* signer.<br>    if state.authority != *authority.key {<br>        msg!(&quot;Signer is not the counter&#39;s authority&quot;);<br>        return Err(ProgramError::InvalidAccountData);<br>    }<br><br>    // ── State transition ──────────────────────────────────────────────────────<br>    state.count = state<br>        .count<br>        .checked_add(1)<br>        .ok_or(ProgramError::ArithmeticOverflow)?;<br><br>    msg!(&quot;Counter incremented to {}&quot;, state.count);<br><br>    // ── Serialize back (write through the exclusive mutable borrow) ───────────<br>    state.serialize(&amp;mut &amp;mut counter_account.data.borrow_mut()[..])?;<br>    Ok(())<br>}</pre><p>Walk through the structure and the Rust parallel becomes vivid:</p><ul><li>Deserialization is a typed projection over raw [u8] — like casting a pointer to a struct, but with bounds-checked serialization.</li><li>borrow() and borrow_mut() are not coincidental naming — AccountInfo.data is Rc&lt;RefCell&lt;&amp;mut [u8]&gt;&gt;, and borrowing it follows exactly the same rules as Rust&#39;s runtime borrow checking. You get a Ref&lt;&gt; or RefMut&lt;&gt;, and attempting to get a mutable borrow while a shared borrow exists panics — at runtime, just as RefCell does.</li><li>The ownership check, mutability check, and signer check form a three-part guard that collectively establish the same invariants the borrow checker would enforce at compile time in a pure Rust program.</li></ul><h3>The Deeper Pattern: Stateless Programs, Typed Owned State</h3><p>The most counterintuitive thing about Solana for engineers coming from EVM (Ethereum/Solidity) is that the program and its state are completely separate. The program account is executable but holds no data. The state accounts hold data but are not executable.</p><p>This is not arbitrary. It’s the <em>separation of code and data</em> — the same principle that makes Rust programs easier to reason about than object-oriented designs where methods and state are fused together. A Solana program is a pure function. Its correctness is determined entirely by how it handles the accounts passed to it. It has no hidden mutable state of its own.</p><p>The consequence for program security is significant. There’s no reentrancy attack surface of the kind that plagued early Ethereum contracts, because a program has no persistent mutable state that a reentrant call could observe mid-execution. The state lives in accounts, and those accounts are either locked as writable by the current transaction or they&#39;re not. The scheduler enforces it before execution begins.</p><h3>What This Means for the Rust Developer</h3><p>If you’ve spent time with Rust’s ownership model, you’ve been training your intuition for Solana without knowing it. The mental discipline of asking “who owns this?”, “is this mutable right now?”, “does this reference outlive its referent?” — those questions map directly onto the questions you ask when designing a Solana program’s account structure.</p><p>The account model isn’t a new paradigm you have to learn from scratch. It’s the same paradigm, instantiated on a distributed ledger with a validator enforcing invariants instead of a compiler.</p><p>The practical implication: Rust engineers who take time to understand the account model at this level tend to write more secure Solana programs on their first attempt — not because they’ve memorized the API, but because the correctness reasoning is already native to how they think.</p><h3>Further Reading and Practice</h3><p>If you want to take this deeper, here are the next logical steps:</p><ul><li><strong>Cross-Program Invocations (CPIs)</strong>: How programs call other programs, and how ownership transfers work across those call boundaries. This is where invoke_signed and the bump seed pattern become essential.</li><li><strong>The Anchor constraint system</strong>: Understanding #[account(seeds = ..., bump)] and how Anchor generates PDA derivation and verification code, which eliminates an entire class of seed canonicalization bugs.</li><li><strong>Parallel execution and account locking</strong>: How the runtime’s transaction scheduler uses the is_writable declarations to build a conflict graph and identify parallelizable transaction sets.</li></ul><p>And if you want a structured path through all of it — from the Rust fundamentals all the way through production DeFi program architecture — the <a href="https://metana.io/web3-rust-bootcamp-solana-blockchain/?utm_source=luis-medium-article&amp;utm_medium=referrals&amp;utm_content=blog-post-link&amp;utm_campaign=luis-medium-article"><strong>Metana Web3 Rust + Solana Bootcamp</strong></a> is worth a look. It’s the most structured hands-on curriculum I’ve seen for getting Rust engineers production-ready on Solana.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OYgwAiz1TJUrojTnws7yow.png" /><figcaption><a href="https://metana.io/web3-rust-bootcamp-solana-blockchain/?utm_source=luis-medium-article&amp;utm_medium=referrals&amp;utm_content=blog-post-link&amp;utm_campaign=luis-medium-article"><strong><em>Metana Web3 Rust + Solana Bootcamp</em></strong></a></figcaption></figure><p>For more Rust articles, interactive exercises, and AI-powered code review, you can explore <a href="https://www.rustlab.dev/">RustLab.dev</a> — where this article lives alongside 180+ other deep-dives across every corner of the language.</p><p><em>Luis Soares is a Senior Rust and Blockchain Engineer with 23 years of systems experience spanning institutional finance and Web3. He is the author of five books on Rust and systems programming and writes the Rust Guru newsletter on LinkedIn.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=158186928275" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>