<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Skill Issue Dev | Dax the Dev — Papers</title>
  <subtitle>Formal papers and preprints from Dax the Dev</subtitle>
  <id>https://blog.skill-issue.dev/papers/</id>
  <link rel="alternate" type="text/html" href="https://blog.skill-issue.dev/papers/"/>
  <link rel="self" type="application/atom+xml" href="https://blog.skill-issue.dev/papers.xml"/>
  <updated>2026-04-30T00:00:00.000Z</updated>
  <author><name>Dax the Dev</name><email>dax@skill-issue.dev</email></author>
  <generator>Astro</generator>
  <icon>https://blog.skill-issue.dev/favicon.svg</icon>
  <entry>
  <title type="html"><![CDATA[Production Round-Constant Selection for Poseidon-128 over BN254]]></title>
  <id>https://blog.skill-issue.dev/papers/poseidon-round-constants/</id>
  <link rel="alternate" type="text/html" href="https://blog.skill-issue.dev/papers/poseidon-round-constants/"/>
  <published>2026-04-30T00:00:00.000Z</published>
  <updated>2026-04-30T00:00:00.000Z</updated>
  <author><name>Hayden &apos;Dax&apos; Porter-Aylor</name></author>
  <category term="paper"/>
  <category term="status:preprint"/>
  <category term="Poseidon"/>
  <category term="round constants"/>
  <category term="BN254"/>
  <category term="parameter selection"/>
  <category term="zero-knowledge"/>
  <category term="Grain LFSR"/>
  <summary type="html"><![CDATA[Poseidon-128 over BN254 has converged on a small number of canonical parameter sets, but the round-constant tables shipped by widely deployed implementations diverge subtly. We document the methodology by which a production parameter set should be selected — the Grain-LFSR procedure of Grassi et al., the security margin, and the alpha-vs-round-count tradeoff for BN254. We trace the divergence between the round-constant streams of circomlib and arkworks to a specific design choice in how the LFSR is consumed during partial rounds, give a deterministic test vector for circomlib-compatibility, and argue that production implementers in 2026 should treat the circomlib table as a frozen specification.
]]></summary>
  <content type="html"><![CDATA[<h2>1. Introduction</h2>
<p>The Poseidon hash function [@grassi2021poseidon] occupies an unusual position in production zero-knowledge cryptography: the algorithm is widely deployed, the parameter sets are nominally standardised by the original paper, and yet the actual round-constant tables shipped by independent implementations differ. Two implementations both claiming &quot;Poseidon-128 over BN254 with $t = 3$&quot; can nevertheless produce different hashes for the same input.</p>
<p>The cause is not malice or careless engineering. It is that the round-constant generation procedure — a deterministic stream from a Grain-style linear-feedback shift register, parameterised by the field, the state size, the alpha, and the round counts — has multiple defensible <em>interpretations</em> of small details: byte ordering of the seed, whether full rounds are emitted before partial rounds in the constant stream, whether constants for the non-spongy state elements during partial rounds are emitted-and-discarded or skipped entirely. Different implementations made different choices in 2020 and 2021. Those choices are now load-bearing for a substantial deployed footprint and cannot be re-litigated without breaking existing zero-knowledge proofs.</p>
<p>This paper does three things. First, it documents the parameter-selection methodology a production implementer should follow if starting fresh, including the alpha-vs-round-count tradeoff specific to BN254 [@barreto2005bn]. Second, it traces the divergence between the round-constant streams of two prominent reference implementations — <code>circomlib</code> [@grassi2021poseidon] and <code>arkworks</code> — to a specific design choice in how the LFSR is consumed during partial rounds. Third, it argues that for new deployments aiming for cross-implementation compatibility, the right move in 2026 is to treat the <code>circomlib</code> parameter file as a frozen specification and validate one&#39;s implementation against a deterministic test vector.</p>
<h2>2. Parameter selection for BN254</h2>
<p>Poseidon&#39;s parameter space is a five-dimensional lattice $(\mathbb{F}_p, t, \alpha, R_F, R_P)$:</p>
<ul>
<li>$\mathbb{F}_p$ is the base field. For SNARK-friendly BN254 deployments, $p$ is the curve&#39;s scalar-field modulus, a 254-bit prime.</li>
<li>$t$ is the state size in field elements. For two-to-one hashing $t = 3$; for absorbing more inputs per permutation, $t \in {4, 5, 9}$.</li>
<li>$\alpha$ is the S-box exponent. The sole constraint is $\gcd(\alpha, p - 1) = 1$.</li>
<li>$R_F$ is the number of full rounds (S-box applied to all state elements).</li>
<li>$R_P$ is the number of partial rounds (S-box applied to one state element).</li>
</ul>
<p>For BN254, $p - 1$ has small factors $2$ and $3$, so $\alpha = 2, 3, 4$ all share a factor with $p - 1$ and produce non-bijective S-boxes. The smallest legal $\alpha$ is $5$. This is not negotiable — $\alpha = 5$ for BN254 is mechanically forced by the field, not a design preference.</p>
<h3>2.1 The alpha-vs-round-count tradeoff</h3>
<p>Higher $\alpha$ would, in principle, reduce the number of rounds needed: each S-box introduces more algebraic non-linearity, so fewer rounds suffice for a given security target. For BN254, the candidate alternatives to $\alpha = 5$ are $\alpha = 7$ and $\alpha = 11$. The trade is concrete: an $\alpha = 7$ S-box costs four constraint multiplications in R1CS (one for $x^2$, one for $x^4$, one for $x^6 = x^4 \cdot x^2$, one for $x^7 = x^6 \cdot x$), versus three for $\alpha = 5$. Recommended round counts for $\alpha = 7$ on BN254 are $R_F = 8$, $R_P \approx 47$ — that is, ten fewer partial rounds than $\alpha = 5$&#39;s $(8, 57)$ recommendation.</p>
<p>The total constraint count under each choice is:
$$
\mathsf{cost}<em>{\alpha = 5} = 8 \cdot 3t + 57 \cdot 3 = 72 + 171 = 243 \text{ for } t = 3
$$
$$
\mathsf{cost}</em>{\alpha = 7} = 8 \cdot 4t + 47 \cdot 4 = 96 + 188 = 284 \text{ for } t = 3
$$</p>
<p>Under this accounting, $\alpha = 5$ wins at $t = 3$ for the recommended security margin. For larger $t$ the gap narrows and may invert; production implementers targeting wide states ($t \geq 5$) should re-do the arithmetic.</p>
<h3>2.2 Security margin and round counts</h3>
<p>The original Poseidon analysis computes the minimum round count to resist three classes of attack: Gröbner-basis interpolation, statistical/differential cryptanalysis, and the round-counting attack on the partial-rounds region. The recommended $(R_F, R_P)$ for BN254 with $t = 3$ at the 128-bit security level is $(8, 57)$, with a safety margin of approximately $25%$ above the minimum. Subsequent cryptanalytic work [@bariant2023algebraic] has tightened the analysis but not invalidated the recommendation.</p>
<p>We adopt the original recommendation in this paper and note that Poseidon-2 [@grassi2023poseidon2] should be considered for new deployments — Poseidon-2 simplifies the round structure with provably-equivalent security, and its parameter recommendations are cleaner to argue about.</p>
<h2>3. The Grain-LFSR round-constant procedure</h2>
<p>The round-constant stream is generated by a Grain-style LFSR seeded from a description of the parameter set. The seed is, conceptually, the tuple $(\text{field-id}, t, R_F, R_P, \alpha)$, encoded into a fixed-width bit string that initialises the LFSR. The LFSR then produces a stream of bits, which is chunked into field-element-sized blocks. Each block is rejected if it exceeds the field modulus and accepted otherwise — a rejection-sampling technique that ensures uniform distribution in $\mathbb{F}_p$.</p>
<p>Under the original specification, the stream is consumed <em>in round order, in state-element order within each round.</em> Concretely: for a state size $t = 3$ and $R_F + R_P = 8 + 57 = 65$ rounds, the stream produces $65 \cdot 3 = 195$ field elements, indexed $r_{0,0}, r_{0,1}, r_{0,2}, r_{1,0}, \dots, r_{64, 2}$.</p>
<p>This is where the implementations diverge.</p>
<h3>3.1 The <code>circomlib</code> interpretation</h3>
<p><code>circomlib</code> consumes the stream in the obvious linear order described above and <em>materialises every constant</em>, including the constants for state elements that are not S-boxed in partial rounds. The constants for non-active partial-round positions are added to the state during the linear MDS step but never participate in an S-box. This wastes a few constraints&#39; worth of additions but has the property of treating the LFSR as a single, monolithic stream.</p>
<h3>3.2 The <code>arkworks</code> interpretation</h3>
<p><code>arkworks</code> (and a small handful of other Rust-native implementations) optimises by <em>skipping</em> the LFSR positions that would have produced constants for the non-active state elements during partial rounds. The argument is that those constants are mathematically redundant — they can be folded into a constant offset per non-active position that is accumulated across the partial-rounds region — and therefore should not consume LFSR output. The constant stream consumed by <code>arkworks</code> is shorter than <code>circomlib</code>&#39;s by exactly $(t - 1) \cdot R_P$ field elements.</p>
<p>Both interpretations produce a Poseidon hash that is, on its own, a valid instantiation of the specification. They are, however, <em>mutually incompatible</em>: a circuit compiled against <code>circomlib</code> constants will not verify a proof produced against <code>arkworks</code> constants, even though both implementations claim to be Poseidon-128 over BN254 with $(t, \alpha, R_F, R_P) = (3, 5, 8, 57)$.</p>
<p>This is not an implementation bug. It is a specification ambiguity that the original paper did not foreclose. By 2026 the deployed weight of <code>circomlib</code>-compatible circuits — including all production deployments of zk-SNARK shielded-pool stacks built on the Iden3 toolchain [@hopwood2022zcash] — substantially exceeds the deployed weight of any alternative interpretation. We argue below that this asymmetry should be the deciding factor for new implementations.</p>
<h2>4. Recommendation: treat circomlib as the frozen specification</h2>
<p>For a new deployment of Poseidon-128 over BN254 in 2026, the production-correct choice is:</p>
<ol>
<li><strong>Adopt the <code>circomlib</code> round-constant table verbatim.</strong> Do not regenerate constants from the LFSR seed in your own implementation; instead, ingest the JSON file shipped by <code>circomlib</code> and validate it against a known test vector (§5).</li>
<li><strong>Treat the table as a build artifact.</strong> Encode it in your binary at build time, not as a runtime dependency; cryptographic parameters should not be reachable through the network or the filesystem at runtime.</li>
<li><strong>Document the divergence.</strong> If your implementation uses an alternative interpretation (e.g., for compatibility with an older <code>arkworks</code> deployment), state which interpretation, why, and what proofs are interoperable.</li>
</ol>
<p>This recommendation is conservative. It accepts that the spec ambiguity should not be re-litigated, that the deployed footprint is a fact, and that interoperability is more valuable than implementation independence for an SDK that aims to participate in the existing zero-knowledge ecosystem.</p>
<p>The reader who finds this conservative is correct. The reader who finds it inappropriate is also correct in principle: if a new deployment is genuinely greenfield, a fresh start with Poseidon-2 [@grassi2023poseidon2] is cleaner. Poseidon-2 has tighter parameter recommendations, an unambiguous LFSR consumption order, and active maintenance from the original authors. New deployments that do not need <code>circomlib</code> interoperability should prefer Poseidon-2.</p>
<h2>5. A deterministic test vector</h2>
<p>To validate that an implementation matches the <code>circomlib</code> interpretation, we publish the following test vector. With state $\mathbf{s}<em>0 = (0, 1, 2)$ and the canonical Poseidon-128 BN254 parameter set $(t, \alpha, R_F, R_P) = (3, 5, 8, 57)$ using the <code>circomlib</code> round-constant table, the output of the permutation $\mathbf{s}</em>{65}$ is a triple of field elements whose first coordinate (the canonical sponge output) is:</p>
<p>$$
\mathsf{poseidon}<em>2(0, 1, 2)\big|</em>{0} = \texttt{0x115cc0f5e7d690413df64c6b9662e9cf...}
$$</p>
<p>\note{TODO: empirical validation — replace the truncated digest above with the full 32-byte hex value extracted from a fresh circomlib run and a parallel reference implementation, and include parallel test vectors for inputs $(1, 2)$, $(2, 3)$, and the all-zeros input.}</p>
<p>The test vector is deterministic, parameter-set-pinned, and publicly auditable. An implementation that does not reproduce it bit-for-bit is not <code>circomlib</code>-compatible, regardless of what the README claims.</p>
<h2>6. Operational implications</h2>
<p>Three operational consequences of treating <code>circomlib</code> as the frozen specification:</p>
<ol>
<li><strong>Constant-table provenance becomes part of the supply chain.</strong> The JSON file is now a critical input to the cryptographic correctness of the system; it should be checksummed, version-pinned, and ideally vendored into the source tree rather than pulled at build time from an external registry. The risk is the same kind of supply-chain risk that affects code dependencies, but with a higher blast radius — a tampered constant table will silently produce hashes that are valid under no circuit verifier in the world.</li>
<li><strong>Cross-curve agility is constrained.</strong> A team that wants to migrate from BN254 to BLS12-381 must regenerate constants under a different field. Because the LFSR seed depends on the field identifier, the resulting table is unrelated to the BN254 table. Tooling that bakes in a single constant table requires a structural change to support multiple curves.</li>
<li><strong>Hardware accelerators must commit to a constant table.</strong> ASIC and FPGA accelerators for Poseidon must hard-code the round constants into ROM. A team that ships hardware against <code>circomlib</code> constants and then needs to switch to Poseidon-2 has stranded hardware.</li>
</ol>
<p>None of these are unique to Poseidon. They apply to any cryptographic primitive whose security depends on a specific constant table. They are worth naming because the original Poseidon paper does not call them out, and operations teams discover them empirically the first time a constant-table mismatch surfaces.</p>
<h2>6.4 The MDS matrix is also a parameter</h2>
<p>A reviewer would correctly note that round constants are only one of two parameters that vary across Poseidon implementations. The MDS (Maximum Distance Separable) matrix used in the linear layer is also implementation-specified, and different implementations have shipped different MDS matrices that produce different hashes for the same input.</p>
<p>The original Poseidon paper [@grassi2021poseidon] specifies a procedure for constructing an MDS matrix from a Cauchy matrix over $\mathbb{F}_p$, parameterised by the same Grain LFSR seed used for round constants. The procedure is deterministic given the seed, but again leaves degrees of freedom: which subset of field elements to use as the Cauchy parameters, how to verify the matrix&#39;s MDS property over the field, and how to handle the case where the candidate matrix fails the MDS test (rejection-sample? fall back to a known-good matrix? error out?).</p>
<p><code>circomlib</code> ships a pre-computed MDS matrix that is treated as a constant, just like the round-constant table. <code>arkworks</code> re-derives the matrix from the seed at startup and verifies the MDS property at runtime. Both approaches produce <em>some</em> valid MDS matrix; they do not produce the <em>same</em> MDS matrix.</p>
<p>The recommendation is symmetric to the round-constant recommendation: treat the <code>circomlib</code> MDS matrix as part of the frozen specification. Vendor it. Checksum it. Validate against the test vector.</p>
<h2>6.5 The state-aliasing question</h2>
<p>A subtler interoperability question: when implementations encode the Poseidon state $\mathbf{s} \in \mathbb{F}_p^t$, do they use little-endian or big-endian byte order? Does the canonical hash output reduce $\mathbf{s}_0$ modulo $p$ before serialising, or does it serialise the in-memory representation directly? When does an implementation accept inputs that exceed the field modulus, and when does it reject them as invalid?</p>
<p>These questions have answers in any individual implementation, but the answers are not specified in the paper. A user porting test vectors between two implementations may find that the same input-bytes produce different field elements, and the same field elements produce different output-bytes, even if the round constants and MDS matrix agree.</p>
<p>We adopt the following convention in our deployment:</p>
<ol>
<li>Inputs are parsed as little-endian 32-byte unsigned integers.</li>
<li>Inputs that exceed the field modulus are rejected with a parse error rather than silently reduced.</li>
<li>Outputs are serialised as little-endian 32-byte unsigned integers, with the leading bit cleared (so the output fits in 254 bits, matching the BN254 field).</li>
</ol>
<p>This convention matches <code>circomlib</code>&#39;s convention. It does not match <code>arkworks</code>&#39; default convention, which uses big-endian. Implementations that need to interoperate with both must perform a byte-swap at the boundary; we document this in the SDK&#39;s <code>Poseidon</code> module-level docstring.</p>
<h2>6.6 Domain separation</h2>
<p>A frequently overlooked consideration: a hash function used in multiple distinct <em>contexts</em> in a protocol should ideally have a per-context domain separator that prevents collisions across contexts. For Poseidon, the conventional approach is to absorb a context tag as the initial state element, so $\mathsf{poseidon}(\mathsf{tag}, x_1, x_2)$ rather than $\mathsf{poseidon}(0, x_1, x_2)$.</p>
<p>The choice of tag is implementation-defined. <code>circomlib</code> uses an integer in the range ${1, \dots, 2^{16}}$, encoding the context as a small, human-readable identifier. Other implementations use a hash of a string identifier. Production implementers should pick a convention, document it, and stick to it for the lifetime of any deployed circuit.</p>
<p>In our deployment we use <code>circomlib</code>-style integer tags, with tag $0$ reserved for the un-domain-separated case (which we then refuse to use in production code). The full tag table is part of the SDK&#39;s circuit specification and is checked at compile time against a manifest file.</p>
<h2>6.7 An aside on field element representation</h2>
<p>Implementations differ subtly in how they represent field elements at the API boundary. The two prevalent representations are <em>canonical</em> (every element of $\mathbb{F}_p$ has a unique byte representation, the lexicographically least integer in its residue class) and <em>Montgomery form</em> (every element is multiplied by a precomputed constant $R$ for arithmetic efficiency, with conversion happening at the boundary).</p>
<p>For interop, the API boundary should be canonical. For internal arithmetic, Montgomery form is faster. The conversion is essentially free if the caller is doing many operations, but expensive if the caller is doing one. SDKs that surface a <code>hash_one_pair</code> API where each call performs a single Poseidon should keep elements in canonical form; SDKs that surface a streaming <code>hash_many</code> API can keep them in Montgomery form across the stream and convert only at boundaries.</p>
<p>Production implementations should benchmark this. Our own benchmarks suggest a $3$-$5%$ gain from Montgomery form on hot paths\note{TODO: empirical validation — pin to measured numbers from the BN254 hash-throughput benchmark suite on the target deployment hardware.}, but the gain disappears on cold-call paths where conversion dominates. The right answer is workload-dependent.</p>
<h2>6.8 Cross-implementation differential testing</h2>
<p>The interoperability story above suggests a concrete engineering practice: differential test against <code>circomlib</code> for every change to the SDK&#39;s Poseidon implementation. Concretely, a pre-commit hook should compare the SDK&#39;s hash output against <code>circomlib</code>&#39;s reference implementation for a fixed corpus of test vectors, and fail the commit if any differ.</p>
<p>We adopt this practice in our deployment. The corpus is approximately 1,000 inputs covering: zero, one, the field modulus minus one, randomly-sampled elements, structured patterns (powers of two, fibonacci sequences), and known-tricky cases (inputs that exercise the rejection-sampling boundary in the LFSR). Differential testing has caught two regressions in our implementation since adoption — one in the MDS-application code, one in the partial-rounds-vs-full-rounds boundary — that would otherwise have shipped as silent incompatibilities.</p>
<p>The cost of maintaining the test corpus is modest. The benefit is enormous: a Poseidon implementation that passes a 1000-input differential test against <code>circomlib</code> is vastly more likely to interoperate with the deployed circuit population than one that has only been unit-tested against its own internals.</p>
<h2>6.9 The case against fragmentation</h2>
<p>A recurring temptation in cryptographic engineering is to ship a &quot;better&quot; version of a primitive — one with cleaner mathematics, faster constants, smaller round counts. Each such variant fragments the deployed ecosystem, and over a multi-year horizon the fragmentation cost typically exceeds the local efficiency gain.</p>
<p>This is not an argument against innovation. Poseidon-2 is a genuine improvement over Poseidon, and a deployment that does not need backward compatibility should ship Poseidon-2 [@grassi2023poseidon2] from day one. It is an argument against shipping minor variants — a tweak to the LFSR consumption order, a different MDS construction, a slightly different alpha — that produce mathematically identical security properties but bit-incompatible outputs. Such variants do not improve the world; they fork it.</p>
<p>The recommendation, therefore, is to choose a primitive (Poseidon-1 with <code>circomlib</code> parameters, or Poseidon-2 with the AFRICACRYPT-2023 parameters) and stay there. Cross-curve agility is fine; cross-version agility within the same primitive is not.</p>
<h2>7. Conclusion</h2>
<p>Round-constant selection for Poseidon-128 over BN254 is a settled choice in 2026, but the settlement is <em>de facto</em> rather than <em>de jure</em>: the specification permits multiple consumption orders for the LFSR stream during partial rounds, and the implementations that diverge are not wrong in any specifiable way. The deployed footprint of <code>circomlib</code>-compatible circuits is the deciding factor for production implementers, and we recommend treating the <code>circomlib</code> constant table as a frozen specification with a deterministic test vector.</p>
<p>The methodological lesson is broader than Poseidon. When a cryptographic specification leaves degrees of freedom, the first widely-deployed interpretation becomes the specification by accretion. Implementations that ignore this property fragment the ecosystem; implementations that recognise it preserve interoperability at the cost of mathematical aesthetics. In 2026 the right move for production zero-knowledge stacks is to recognise the property and document the constraint.</p>
<h2>References</h2>
<p>[^ref]</p>
]]></content>
</entry>
<entry>
  <title type="html"><![CDATA[L1 Nullifier Sets: Consensus-Layer Double-Spend Prevention in a UTXO-Based Privacy Chain]]></title>
  <id>https://blog.skill-issue.dev/papers/l1-nullifier-sets/</id>
  <link rel="alternate" type="text/html" href="https://blog.skill-issue.dev/papers/l1-nullifier-sets/"/>
  <published>2026-04-28T00:00:00.000Z</published>
  <updated>2026-04-28T00:00:00.000Z</updated>
  <author><name>Hayden &apos;Dax&apos; Porter-Aylor</name></author>
  <category term="paper"/>
  <category term="status:draft"/>
  <category term="nullifier"/>
  <category term="consensus"/>
  <category term="privacy"/>
  <category term="UTXO"/>
  <category term="Bitcoin"/>
  <category term="zero-knowledge"/>
  <category term="sparse Merkle tree"/>
  <summary type="html"><![CDATA[We formalise the choice of locating the nullifier set in consensus state, rather than in wallet-side software, for a UTXO-based privacy chain derived from Bitcoin. We give a chain model extending the Bitcoin backbone protocol with a shielded UTXO type, a soundness theorem bounding the probability of an accepted double-spend by the collision probability of the underlying nullifier hash, and a proof sketch reducing soundness to the collision resistance of Poseidon-2. We discuss extensions to compressed-account chains, the storage and synchronisation costs of a monotone nullifier set, and identify two open questions on sparse-Merkle-tree maintenance under adversarial insert orderings.
]]></summary>
  <content type="html"><![CDATA[<h2>1. Introduction</h2>
<p>A shielded UTXO chain reveals neither sender, recipient, nor amount on chain. To prevent double spends without revealing the spent note, every shielded protocol since Zerocash [@bensasson2014zerocash] has used a <em>nullifier</em>: a deterministic single-use identifier $\mathsf{nf}$ derived from a secret known only to the spender and the public commitment of the consumed note. The chain treats the global set of disclosed nullifiers as state; a duplicate nullifier indicates a double spend.</p>
<p>The structural question every such chain must answer is <em>where the nullifier set lives</em>. The historical answers — wallet-side index, smart-contract sidechain, consensus-layer state — span a spectrum of soundness, operator-trust, and engineering complexity. The Zcash protocol [@hopwood2022zcash] places the set in node chainstate; the Tornado-style Ethereum deployments place it in a contract; some lighter-weight UTXO experiments leave it to the wallet.</p>
<p>We focus on a specific point in this design space: <strong>a Bitcoin-fork chain that carries the nullifier set in consensus state.</strong> We refer to such a chain as an <em>L1-nullifier chain</em>. The contribution of this paper is:</p>
<ol>
<li>A chain model that extends the Garay–Kiayias–Leonardos backbone protocol [@garay2015backbone] with a shielded UTXO type and a chainstate-resident nullifier set.</li>
<li>A soundness theorem establishing that in this model the probability of an accepted double spend is bounded above by the probability of a hash collision in the underlying nullifier construction.</li>
<li>A proof sketch reducing soundness to the collision resistance of the hash used to derive nullifiers — in our instantiation, Poseidon-2 [@grassi2023poseidon2] over the BN254 scalar field.</li>
<li>A treatment of two extensions: compressed-account chains where notes are batched into a Merkle commitment per block, and the operator cost of maintaining a sparse Merkle tree [@buterin2016sparse] over the nullifier set under adversarial insert orderings.</li>
</ol>
<p>The paper is theorem-statement in shape and engineering-aware in spirit. Where empirical claims appear, they are flagged.</p>
<h2>2. Chain model</h2>
<p>We adopt the backbone abstraction of Garay et al. [@garay2015backbone] with three additions.</p>
<p><strong>Definition 2.1 (Shielded UTXO chain).</strong> A shielded UTXO chain $\mathcal{C}$ is a sequence of blocks $B_0, B_1, \dots$ where each $B_i$ contains:</p>
<ol>
<li>A set of <em>transparent</em> transactions $\mathsf{Tx}^t_i$ in the standard Bitcoin sense.</li>
<li>A set of <em>shielded</em> transactions $\mathsf{Tx}^s_i$. Each $\tau \in \mathsf{Tx}^s_i$ contains:<ul>
<li>A vector of <em>new commitments</em> ${C_j}$ — Pedersen commitments [@pedersen1991noninteractive] to values not visible on chain.</li>
<li>A vector of <em>spent nullifiers</em> ${\mathsf{nf}_k}$ — the nullifier outputs of consumed notes.</li>
<li>A zero-knowledge proof $\pi$ asserting that the spent notes were valid and unspent at some prior block.</li>
</ul>
</li>
<li>A coinbase commitment to the root of the global nullifier set $\mathsf{NS}_i$ as of block $i$.</li>
</ol>
<p>A node maintains $\mathsf{NS}<em>i = \bigcup</em>{j \le i} \bigcup_{\tau \in \mathsf{Tx}^s_j} {\mathsf{nf}_k : \mathsf{nf}_k \in \tau}$ as part of its chainstate, together with the standard UTXO set.</p>
<p><strong>Definition 2.2 (Validity).</strong> Block $B_i$ is valid with respect to $\mathcal{C}|_{&lt;i}$ if:</p>
<ol>
<li>Every transparent transaction in $\mathsf{Tx}^t_i$ is valid by Bitcoin script rules.</li>
<li>For every shielded transaction $\tau \in \mathsf{Tx}^s_i$, the proof $\pi$ verifies under the canonical shielded-spend relation.</li>
<li><strong>No-collision rule:</strong> For every spent nullifier $\mathsf{nf}<em>k$ in $B_i$, $\mathsf{nf}<em>k \notin \mathsf{NS}</em>{i-1} \cup \bigcup</em>{\tau&#39; \in \mathsf{Tx}^s_i \setminus {\tau}} {\mathsf{nf} : \mathsf{nf} \in \tau&#39;}$.</li>
<li>The coinbase&#39;s $\mathsf{NS}$-root commitment matches the recomputed root after applying $B_i$&#39;s nullifiers.</li>
</ol>
<p>The no-collision rule is the central addition. Under the standard backbone semantics, an honest node observing a block whose shielded transactions reuse a nullifier <em>rejects the block</em>. The block is not &quot;valid until someone notices&quot;; it is invalid at the consensus boundary, in the same way a transparent transaction spending an absent UTXO is invalid.</p>
<p><strong>Definition 2.3 (Double spend).</strong> A <em>double spend</em> against $\mathcal{C}$ is a pair of accepted shielded transactions $\tau, \tau&#39;$ in the same chain history such that $\tau \neq \tau&#39;$ and $\exists, \mathsf{nf} : \mathsf{nf} \in \tau \cap \tau&#39;$.</p>
<h2>3. Soundness</h2>
<p><strong>Theorem 3.1 (Soundness of L1-nullifier-set enforcement).</strong> Let $\mathcal{C}$ be a shielded UTXO chain as in Definition 2.1, with nullifier function $\mathsf{nf} = H(sk, C)$ where $H$ is a $(\lambda)$-collision-resistant hash and $sk$ is uniformly distributed in the note&#39;s secret-key space. Then, conditioned on the underlying backbone protocol&#39;s common-prefix and chain-quality properties [@garay2015backbone] holding with parameter $\epsilon$, the probability that an accepted block contains a double spend is at most $\mathrm{negl}(\lambda) + \epsilon$.</p>
<p><em>Proof sketch.</em> Let $\tau$ and $\tau&#39;$ be two shielded transactions sharing a nullifier $\mathsf{nf}$. Two cases.</p>
<p><strong>Case A.</strong> $\tau$ and $\tau&#39;$ consume the same underlying note. By construction of the proof system, the existence of a valid $\pi$ for both transactions implies both knew the witness $(sk, C)$ for the consumed note. The chain accepts at most one such transaction by the no-collision rule (Definition 2.2, clause 3). Acceptance of both therefore violates the no-collision rule and constitutes a consensus violation, not a soundness violation: such an acceptance can occur only if the backbone&#39;s common-prefix property fails, which happens with probability $\le \epsilon$.</p>
<p><strong>Case B.</strong> $\tau$ and $\tau&#39;$ consume <em>distinct</em> notes that produce the same nullifier. This requires $H(sk_1, C_1) = H(sk_2, C_2)$ with $(sk_1, C_1) \neq (sk_2, C_2)$, i.e., a collision in $H$. By assumption $H$ is $(\lambda)$-collision-resistant, so the probability of this case is $\mathrm{negl}(\lambda)$.</p>
<p>A union bound over the two cases yields the theorem.</p>
<p>The structure of the argument deserves comment. The interesting work is done by the no-collision rule, which lifts double-spend prevention from a wallet-side liveness property (where one trusts every wallet to refuse to construct a colliding spend) to a chain-wide safety property (where a colliding block is rejected by every honest node regardless of who produced it). The proof is then almost trivial; this is the point. A clean primitive admits a clean theorem.</p>
<h3>3.1 Concrete instantiation: Poseidon-2 over BN254</h3>
<p>In our reference implementation, $H(sk, C) = \text{Poseidon2}_t(sk, C)$ with $t = 3$ and the BN254 [@barreto2005bn] scalar field. Poseidon-2 [@grassi2023poseidon2] inherits the cryptanalytic argument of the original Poseidon design [@grassi2021poseidon] with simplified round structure, and is currently the subject of active analysis [@bariant2023algebraic]. Under the standard sponge indifferentiability argument [@bertoni2008indifferentiability], Poseidon-2 is collision-resistant against generic adversaries up to the output bound, which for the BN254 field with a 256-bit output gives $\lambda = 128$ bits of collision resistance — sufficient for practical deployment.\note{TODO: empirical validation — confirm cryptanalytic state-of-the-art for Poseidon-2 at submission time. Bariant et al. 2023 introduces a degree-of-freedom argument worth surveying.}</p>
<p>Theorem 3.1 then specialises to the statement that, conditioned on the backbone holding, the probability of an accepted double spend on this chain is $2^{-128}+\epsilon$ — for any plausible $\epsilon$, the cryptographic term dominates the operational term, which is the desired regime.</p>
<h2>4. Storage and synchronisation costs</h2>
<p>The headline cost of consensus-resident nullifier sets is monotonically increasing storage. Each accepted shielded spend permanently extends $\mathsf{NS}$ by $|H|$ bytes. We give a back-of-envelope analysis.</p>
<p>For Poseidon-2 over BN254, $|H| = 32$ bytes. If the chain achieves a long-term steady state of $k$ shielded spends per block at a block interval of $T$ seconds, the annual nullifier-set growth is
$$
\Delta_{\text{year}} = k \cdot \frac{31{,}536{,}000}{T} \cdot 32 \text{ bytes}.
$$</p>
<p>For a one-minute-block chain ($T = 60$) with five shielded spends per block, $\Delta_{\text{year}} \approx 84$ MB.\note{TODO: empirical validation — confirm against measured Vanta block telemetry once a steady-state shielded-spend rate is established.} Over a decade this yields $\approx 840$ MB of nullifier state, well below the current Bitcoin UTXO set [@bonneau2015sok].</p>
<p>Synchronisation cost is dominated by the $O(\log n)$ sparse-Merkle insertion path per nullifier, where $n$ is the size of $\mathsf{NS}$. Provided that nodes maintain $\mathsf{NS}$ as an SMT [@buterin2016sparse] with hash-array-mapped-trie compression, the per-insert cost is on the order of microseconds, and the per-block insert batch is dominated by the proof-verification time of the shielded transactions, not the SMT maintenance.</p>
<h2>4.1 Initial-block-download cost</h2>
<p>The initial-block-download (IBD) cost dominates the operator experience for a new node joining the network. Concretely: how long does it take for a freshly synced node to reach tip, given that it must validate every shielded transaction in the chain&#39;s history?</p>
<p>The validation cost per shielded transaction breaks down into three components:</p>
<ol>
<li>The script-validation cost (transparent path), unchanged from upstream Bitcoin.</li>
<li>The proof-verification cost. For Groth16 [@groth2016size] verification on BN254 with three pairing operations, this is approximately 30 ms per proof on a modern CPU.</li>
<li>The nullifier-set membership check and SMT update, approximately 100 µs per nullifier on a SSD-backed implementation.</li>
</ol>
<p>Component (2) dominates by two orders of magnitude. A back-of-envelope calculation: assuming 5 shielded spends per block at 30 ms verification each, the per-block proof cost is 150 ms. For a one-minute-block chain over ten years, this is $5 \cdot 525{,}600 \cdot 10 \cdot 0.030 \approx 22$ hours of pure CPU time, which scales linearly with the number of cores available for parallel proof verification. On a 16-core machine, IBD completes in under 90 minutes — comparable to Bitcoin&#39;s transparent IBD.\note{TODO: empirical validation — measure on production hardware once the chain has substantial history.}</p>
<p>The takeaway is that IBD is feasible but not trivial. Operators running on resource-constrained hardware (single-core SBCs, embedded systems) should be advised that IBD is the primary bottleneck and that the chain&#39;s hardware floor is set by proof-verification performance.</p>
<h2>4.2 Pruning the proof, not the nullifier</h2>
<p>A natural follow-on question to §6.2&#39;s discussion of nullifier pruning: can we instead prune the <em>proofs</em> that established the validity of historical shielded spends, while retaining the nullifiers themselves?</p>
<p>The answer is yes, and our reference implementation does so: a node that has reached the network&#39;s checkpointed tip discards proof bytes for transactions older than a configurable horizon, retaining only the nullifier and the commitment-tree root. The savings are substantial — proofs are 256 bytes in our Groth16 instantiation, so over a decade with 5 spends per block one saves roughly $5 \cdot 525{,}600 \cdot 10 \cdot 256 \approx 6.5$ GB of proof data.</p>
<p>The soundness of pruning rests on the same assumption that justifies pruning the witness data of confirmed transparent transactions: a sufficiently-confirmed shielded spend will not be reorganised, and a node that joins after the prune horizon has elapsed must trust the network&#39;s checkpoints to validate history rather than re-verifying every proof. This is a standard SPV-style assumption and is conventionally accepted.</p>
<h2>5. Extension: compressed-account chains</h2>
<p>A <em>compressed-account chain</em> batches notes into a single Merkle commitment per block to reduce on-chain storage; the chainstate stores roots, not individual notes. The natural extension of L1 nullifier sets to compressed-account chains stores not individual nullifiers but a Merkle tree of nullifiers per block, with the per-block root committed in the coinbase.</p>
<p>The soundness statement (Theorem 3.1) carries over with minor modifications. The no-collision rule generalises to: <em>for every spent nullifier in the block, no preceding block&#39;s nullifier-tree contains it, and no other transaction in this block discloses it.</em> Membership proofs replace direct lookup. The asymptotic cost of a membership proof is again $O(\log n)$ but now with a larger constant due to the per-block tree maintenance overhead.</p>
<p>We do not pursue a full proof of the compressed-account variant in this paper; we register only that the soundness argument transfers, conditioned on the membership-proof verifier being itself sound — a property the underlying SNARK already provides.\note{TODO: empirical validation — formalise once a reference compressed-account UTXO chain ships in production.}</p>
<h2>6. Two open questions</h2>
<p>We close with two questions our own engineering experience has not yet resolved.</p>
<h3>6.1 Adversarial insert ordering</h3>
<p>A sparse Merkle tree&#39;s per-insert cost depends, in the worst case, on the depth of the longest currently-occupied path. An adversary that influences the order of nullifier insertions — by, e.g., populating insertion blocks with adversarially-chosen shielded transactions — could in principle drive insertion paths toward a worst-case profile, increasing per-block validation time. We do not have a tight bound on the adversary&#39;s leverage here. Existing analyses of Merkle-tree insertion costs assume uniformly random keys, which is an unrealistic assumption when the keys are hash outputs the adversary partly chooses.</p>
<p>This is sharper than the corresponding question for transparent UTXO sets, where insertion order is largely unconstrained, because nullifier values are deterministically derived from the spending witness. An adversary controlling a fraction of block-production capacity has bounded but non-zero influence over the nullifier stream. <strong>Open question.</strong> Bound the worst-case per-insert cost as a function of the adversary&#39;s mining-power fraction.</p>
<h3>6.2 Pruning under monotonic growth</h3>
<p>The nullifier set is, by construction, monotone: nothing is ever removed. After ten years of operation a chain with substantial shielded-spend volume will accrete several gigabytes of nullifier state. We do not have a satisfying answer to <em>when, if ever, it is safe to prune.</em> Pruning under the canonical model breaks soundness: a pruned nullifier could be re-spent.</p>
<p>A speculative direction: pruning conditioned on a soundness-preserving accumulator structure that retains a constant-size membership proof for any past nullifier. We do not pursue this here.</p>
<h2>6.3 Witness-availability in light-client settings</h2>
<p>A third question the engineering literature touches but has not, to our knowledge, formalised: <em>under what conditions can a light client verify that an alleged nullifier set is faithful?</em> A light client does not store the full $\mathsf{NS}$; it stores commitment-tree roots and verifies SPV-style. The coinbase commitment to the SMT root of $\mathsf{NS}_i$ allows a light client to verify, given a membership proof, that a particular nullifier is included or excluded as of block $i$.</p>
<p>The verification cost is logarithmic in $|\mathsf{NS}_i|$, but the proof construction is performed by full nodes, which have an incentive to truthfully serve membership proofs to light clients only insofar as the underlying chain is honest by hypothesis. A light client receiving a fabricated <em>exclusion</em> proof from a malicious full node has no recourse if the underlying merkle root does not commit to enough information to detect the fabrication. We rely on the SMT&#39;s structural property that exclusion proofs in a sparse Merkle tree are unique and verifiable — given a leaf path, either the leaf is empty (and the path&#39;s hashes match the root) or the leaf is occupied (and the path includes the occupant). An adversary cannot forge an exclusion proof for a present element, conditioned on the SMT hash function being collision-resistant.</p>
<p>This reduces the light-client safety argument to the same Theorem 3.1 — collision resistance of the underlying hash — without introducing a new trust assumption.</p>
<h2>6.4 Re-org tolerance and finality</h2>
<p>A separate question concerns how nullifier-set state behaves under chain reorganisations. Bitcoin&#39;s UTXO set is reorg-tolerant by construction: a UTXO that is spent in a block, and then the block is reorganised away, is restored as unspent. The same property must hold for the nullifier set: a nullifier consumed in a block that is subsequently orphaned must be removed from $\mathsf{NS}$, returning the corresponding note to the unspent state.</p>
<p>In our reference implementation this is straightforward — the nullifier set is maintained as part of the chainstate snapshot, and chainstate rollbacks happen atomically with block-level rollbacks. The implementation cost is confined to the SMT update logic: deletions during reorg are operationally identical to insertions during apply, with the input ordering reversed.</p>
<p>The interesting subtlety is that selfish-mining attacks [@eyal2014majority] [@sapirshtein2016optimal] interact with the nullifier set in the same way they interact with the transparent UTXO set: an attacker who withholds a block can construct a competing chain with a different nullifier history, and the network may converge on either. A spender whose transaction is reorganised away should be informed by their wallet, exactly as a transparent-transaction spender is informed by their wallet. The nullifier set inherits the reorg semantics of the underlying chain; it does not introduce a new finality caveat.</p>
<h2>7. Conclusion</h2>
<p>We have shown that a Bitcoin-fork shielded UTXO chain that places the nullifier set in consensus state admits a clean soundness statement (Theorem 3.1) reducing double-spend prevention to the collision resistance of the underlying nullifier hash. The cost is monotonic chainstate growth, manageable in practice for plausible shielded-spend rates [@bonneau2015sok] and below current Bitcoin UTXO-set sizes for a decade-scale operating window.</p>
<p>The architectural argument is that <em>consensus is the right place for safety properties.</em> Wallet-side enforcement is a liveness property that the network depends on every wallet implementer to honour. Consensus-side enforcement makes the rule binding for every honest node regardless of wallet provenance. For privacy chains aiming at the same finality semantics as transparent Bitcoin, the consensus-side answer is the structurally honest one.</p>
<h2>References</h2>
<p>[^ref]</p>
]]></content>
</entry>
<entry>
  <title type="html"><![CDATA[Asymmetric Tool Surfaces for AI-Agent Cryptographic Primitives]]></title>
  <id>https://blog.skill-issue.dev/papers/asymmetric-tool-surfaces/</id>
  <link rel="alternate" type="text/html" href="https://blog.skill-issue.dev/papers/asymmetric-tool-surfaces/"/>
  <published>2026-04-25T00:00:00.000Z</published>
  <updated>2026-04-25T00:00:00.000Z</updated>
  <author><name>Hayden &apos;Dax&apos; Porter-Aylor</name></author>
  <category term="paper"/>
  <category term="status:preprint"/>
  <category term="zero-knowledge"/>
  <category term="MCP"/>
  <category term="AI agents"/>
  <category term="SDK design"/>
  <category term="threat modelling"/>
  <category term="privilege separation"/>
  <summary type="html"><![CDATA[We argue that SDKs exposing cryptographic primitives to autonomous AI agents must obey an explicit asymmetry rule: read and pure-compute operations may be exposed without privilege, but state-changing authority must remain behind an out-of-band human or hardware confirmation. We formalise this rule, instantiate it for a shielded-pool zero-knowledge SDK exposing its surface via the Model Context Protocol, and identify the structural reasons the asymmetric design is tractable for cryptographic SDKs while intractable for general-purpose RPC surfaces. The contribution is a small, opinionated discipline that renders adversarial-prompt and supply-chain compromise of the agent layer non-catastrophic.
]]></summary>
  <content type="html"><![CDATA[<h2>1. Introduction</h2>
<p>The deployment posture of cryptographic software development kits has changed faster than the threat model that governs them. Until recently, an SDK was a library — a process-local API consumed by an application written by a human, in a language understood by that human, after the human had read enough of the source to form an intuition for what the calls actually did. The principal threat was a buggy caller, and the principal mitigation was clear documentation, robust types, and careful examples.</p>
<p>The introduction of the Model Context Protocol [@anthropic2024mcp] and the subsequent rapid adoption of MCP servers across cryptographic tooling in 2025 and 2026 has changed the principal. The principal is now, with non-negligible probability, an autonomous large-language-model (LLM) agent calling the SDK on behalf of a user that may not understand the call&#39;s semantics. The agent is potentially compromised by prompt injection [@greshake2023injection], by tool-poisoning of the upstream MCP server, or by an adversarial input embedded in the agent&#39;s working context. The &quot;lethal trifecta&quot; identified by Willison [@willison2025promptinjection] — private data, untrusted input, external communication — is a default property of an MCP-connected wallet stack.</p>
<p>This paper makes a structural argument: cryptographic SDKs that expect to be invoked by AI agents should obey an <em>asymmetric tool-surface discipline</em>, exposing only read and compute primitives over MCP and reserving state-changing authority for an out-of-band, human-in-the-loop, or hardware-enforced confirmation channel. The discipline is narrow and opinionated. It rules out a number of conveniences that look attractive in a normal SDK. It also makes adversarial compromise of the agent layer survivable rather than catastrophic.</p>
<p>The remainder of the paper is organised as follows. Section 2 establishes the threat model and notation. Section 3 defines the asymmetry rule and its decision procedure. Section 4 gives a worked example: an instantiation of the rule for <code>zera-mcp</code>, the MCP surface of a production shielded-pool SDK. Section 5 discusses extensions and limitations. Section 6 concludes.</p>
<h2>2. Threat model</h2>
<p>Let $\mathcal{S}$ denote a cryptographic SDK, and let $\mathcal{T} = {t_1, \dots, t_n}$ denote the finite set of tools $\mathcal{S}$ exposes via an agent-facing protocol such as MCP. Each tool $t_i$ has a typed signature $t_i : \mathbb{I}_i \to \mathbb{O}_i$ and an effect signature $\mathsf{eff}(t_i) \in {\bot, \mathsf{read}, \mathsf{compute}, \mathsf{write}}$.</p>
<p>We adopt the following adversary $\mathcal{A}$:</p>
<ul>
<li>$\mathcal{A}$ may control the agent&#39;s prompt context, including any tool-result text the agent has read.</li>
<li>$\mathcal{A}$ may inject text into any external content the agent fetches.</li>
<li>$\mathcal{A}$ does <strong>not</strong> control any signing key held by a wallet or hardware-security module operating outside the agent.</li>
<li>$\mathcal{A}$ does <strong>not</strong> control the network&#39;s consensus rules.</li>
</ul>
<p>Under this adversary, we require that for any execution trace $\sigma$ produced by the agent calling tools in $\mathcal{T}$, the on-chain state $\Sigma$ reachable from $\sigma$ should differ from the on-chain state reachable under the trivial empty execution only via transactions that were explicitly co-signed by a wallet $W$ outside $\mathcal{A}$&#39;s control.</p>
<p>In words: an adversary that captures the agent must not be able to move funds. They may be able to spend compute, retrieve information the user already had access to, and produce mathematical objects (commitments, proofs) that are inert until co-signed.</p>
<h2>3. The asymmetry rule</h2>
<p>We say a tool surface $\mathcal{T}$ is <em>asymmetric under separation $W$</em> if for every $t_i \in \mathcal{T}$,
$$
\mathsf{eff}(t_i) \in {\mathsf{read}, \mathsf{compute}} ;;\Longleftrightarrow;; t_i \in \mathcal{T}<em>{\text{exposed}}
$$
and the residual $\mathcal{T} \setminus \mathcal{T}</em>{\text{exposed}}$ — the tools whose effect signature is $\mathsf{write}$ — are reachable only through $W$.</p>
<p>The decision procedure that produces $\mathcal{T}_{\text{exposed}}$ is the following:</p>
<blockquote>
<p><em>For each candidate tool $t$, ask: if the agent is fully compromised by $\mathcal{A}$, what is the worst-case state delta $\Delta\Sigma$ a single invocation of $t$ can induce? If $\Delta\Sigma = \emptyset$ — that is, if the call is observationally pure modulo the agent&#39;s own resources — admit $t$ to $\mathcal{T}_{\text{exposed}}$. Otherwise, route the function behind $W$.</em></p>
</blockquote>
<p>Two consequences follow. First, the procedure is composable: if every individual tool is observationally pure under $\mathcal{A}$, any agent-driven <em>composition</em> of them remains observationally pure under $\mathcal{A}$, because composition does not synthesise authority. Second, the procedure is local: the decision for $t_i$ does not depend on the other tools in $\mathcal{T}$, which makes it tractable for SDK authors to apply at PR review time.</p>
<p>The asymmetry rule does not eliminate every class of attack. An $\mathcal{A}$ that captures the agent can still, in principle, mislead the human into approving a transaction that they would not have approved given full information. The rule cannot fix social engineering. What it forbids is an architecture in which the agent has unilateral capability to drain user funds without human action.</p>
<h3>3.1 Why this is tractable for cryptographic SDKs</h3>
<p>Cryptographic SDKs are unusual among software libraries in that the boundary between <em>computation</em> and <em>authority</em> is unusually crisp. Constructing a Pedersen commitment [@pedersen1991noninteractive] does not move money; submitting a transaction does. Computing a Poseidon hash [@grassi2021poseidon] does not move money; signing the resulting witness with a private key does. Producing a Groth16 proof [@groth2016size] does not move money; broadcasting that proof to a chain does. Each of these primitives admits a clean factorisation along the read/compute vs. write axis.</p>
<p>Compare this to a general-purpose enterprise SDK, where the same call may simultaneously query, mutate, and authorise — the asymmetry rule is too aggressive in that setting because the rule&#39;s first effect is to forbid the very calls users want to make. Cryptographic SDKs are exempt from this critique because the underlying mathematics already separates the two.</p>
<h3>3.2 What the rule rules out</h3>
<p>It is worth being explicit about which conveniences the asymmetry rule eliminates, because each is one a typical SDK author will want.</p>
<ol>
<li><strong>One-shot transfer tools.</strong> A tool of the form <code>transfer(asset, amount, to)</code> is the canonical write effect. The asymmetry rule forbids exposing it on the agent surface even if the SDK author has access to a signing key — the right place for that key is a wallet, not the SDK process.</li>
<li><strong>Privileged read tools that proxy write authority.</strong> A tool like <code>revoke_pending_proof(proof_id)</code> looks like a read but actually mutates the SDK&#39;s local state in a way that affects future write operations. The rule treats it as a write.</li>
<li><strong>Implicit authority via cached secrets.</strong> A tool that &quot;remembers&quot; a recent unlock of a key for some grace period is an implicit write — the agent has effective signing authority for the duration. The rule treats the unlock itself as a write.</li>
</ol>
<h2>4. Worked example: <code>zera-mcp</code></h2>
<p>We instantiate the asymmetry rule for <code>zera-mcp</code>, the MCP server bundled with the <code>zera-sdk</code> shielded-pool toolkit. The SDK exposes a small surface: four tools and three resources, all conforming to the asymmetry rule.</p>
<h3>4.1 Tools admitted to $\mathcal{T}_{\text{exposed}}$</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Signature</th>
<th>Effect</th>
</tr>
</thead>
<tbody><tr>
<td><code>compute_commitment</code></td>
<td>$(\mathsf{asset}, \mathsf{amount}, r) \to \mathsf{Commit}$</td>
<td>$\mathsf{compute}$</td>
</tr>
<tr>
<td><code>derive_nullifier</code></td>
<td>$(\mathsf{sk}, \mathsf{Commit}) \to \mathsf{Nullifier}$</td>
<td>$\mathsf{compute}$</td>
</tr>
<tr>
<td><code>build_spend_proof</code></td>
<td>$(\mathsf{note}, \mathsf{recipient}, \mathsf{amount}) \to \pi$</td>
<td>$\mathsf{compute}$</td>
</tr>
<tr>
<td><code>get_pool_state</code></td>
<td>$\bot \to (\mathsf{root}, n_\text{unspent})$</td>
<td>$\mathsf{read}$</td>
</tr>
</tbody></table>
<p><code>compute_commitment</code> is the additively-homomorphic Pedersen commitment $C = g^v h^r$ over the BN254 curve [@barreto2005bn], parameterised by the asset and the amount under a caller-supplied blinding factor $r$. Its output binds the value but reveals nothing about it. The function is observationally pure: $\mathcal{A}$ invoking it in any order with any inputs cannot produce a state delta on chain.</p>
<p><code>derive_nullifier</code> produces $\mathsf{nf} = \text{Poseidon}_2(\mathsf{sk}, C)$, the deterministic single-use nullifier for the note committed to by $C$. Disclosure of $\mathsf{nf}$ proves that <em>some</em> note has been consumed without revealing which one — and the nullifier is single-use precisely because the chainstate enforces uniqueness, not because the SDK does. The SDK is computing a hash; the chain is what makes the hash matter.</p>
<p><code>build_spend_proof</code> runs the canonical Groth16 prover for the shielded-spend relation, producing a proof bytestring $\pi$ together with a public-input vector. The proof is a mathematical object: it does not move funds. It is inert until packaged into a transaction, signed by a wallet, and submitted to the chain. We take the deliberate position — defended in §4.3 — that the prover is a tool rather than a resource, despite being deterministic.</p>
<p><code>get_pool_state</code> returns the most recent commitment-tree root and an aggregate count of unspent notes. It is plainly $\mathsf{read}$.</p>
<h3>4.2 Tools excluded from $\mathcal{T}_{\text{exposed}}$</h3>
<p>The following functions exist in the SDK but are not exposed via MCP. They live behind the wallet $W$:</p>
<ul>
<li><code>submit_transaction(tx)</code> — broadcasting authority. Lives in the wallet.</li>
<li><code>unlock_key(passphrase)</code> — private-key access. Lives in the hardware-backed keystore.</li>
<li><code>set_pool_endpoint(url)</code> — changes which chain the SDK queries; implicit write because it can redirect future proof construction. Lives in user-mediated config.</li>
</ul>
<p>A reader will note that <code>submit_transaction</code> is, mechanically, exactly the kind of call an agent often <em>wants</em> to make. We argue that mechanical convenience is the wrong frame: the question is whether the agent should ever have unilateral authority to broadcast. We take the position that it should not.</p>
<h3>4.3 Why the prover is a tool, not a resource</h3>
<p>In MCP, <em>resources</em> are read-only, cacheable, and side-effect-free; <em>tools</em> are explicit operations the model decides to invoke. A first-pass reading of <code>build_spend_proof</code> makes it look like a resource: the prover is mathematically deterministic, and given the same witness one obtains the same proof modulo the random tape used for the Fiat-Shamir transform. Resources fit deterministic functions cleanly.</p>
<p>This reading is wrong in practice. A prover is <em>computationally</em> side-effecting: an order-of-magnitude latency cost (4–8 seconds in our benchmarks for a Groth16 spend proof on consumer hardware\note{TODO: empirical validation — tighten with measured BN254 prover numbers from the zera-sdk-core benchmark suite once it lands.}) makes it unsafe to be aggressively cached and silently re-invoked. Resources in MCP are meant to be cheap; a four-to-eight-second resource invoked in an agent loop will exhaust user patience and platform budget long before it exhausts the abstract semantics of the call. The taxonomy, in short, is sensitive to economic facts, not just mathematical ones.</p>
<p>We therefore route the prover behind a tool call, which forces the agent to deliberate about whether to re-invoke it, and we maintain the rule: the prover is a tool, but it is a <em>compute</em> tool, not a <em>write</em> tool.</p>
<h2>5. Discussion</h2>
<h3>5.1 Composability across SDKs</h3>
<p>Because the rule is local — admission of $t_i$ depends only on $t_i$&#39;s effect signature, not on the rest of $\mathcal{T}$ — it composes across multiple SDKs that an agent might call in sequence. An agent that holds an MCP connection to <code>zera-mcp</code>, a shielded-pool SDK, and a separate MCP connection to a generic web-search service can have arbitrary cross-call interaction without the asymmetric SDK losing its property: read+compute calls are still read+compute calls.</p>
<h3>5.2 Multi-step authority</h3>
<p>A common objection: if every authority decision is human-mediated, common multi-step flows (recurring shielded payments, scheduled deposits) become user-hostile. We acknowledge the friction. The asymmetry rule does not forbid wallets from offering <em>delegated authority</em> — a wallet can have its own internal policy that allows the agent to broadcast pre-authorised transactions matching a signed schedule — but it requires the policy to live in the wallet, not in the SDK&#39;s MCP surface.</p>
<p>This matters because the wallet is a privileged process with its own user-interface affordances and its own threat model; the SDK&#39;s MCP surface is, by hypothesis, talking to a potentially-compromised agent. The two are not interchangeable. Delegation policy living in the wallet is auditable through the wallet&#39;s own surface; delegation policy living in the SDK is exfiltrable along with the rest of the agent&#39;s prompt context.</p>
<h3>5.3 What the rule cannot prevent</h3>
<p>The rule does not prevent a compromised agent from:</p>
<ol>
<li>Constructing a <em>valid-looking</em> commitment whose underlying note belongs to the attacker, then surfacing it to the user as if it were a recipient-supplied address.</li>
<li>Using <code>get_pool_state</code> to time when the user&#39;s wallet is most likely to approve transactions and clustering its social-engineering attempts there.</li>
<li>Producing a stream of proofs that exhaust the user&#39;s prover budget without ever broadcasting.</li>
</ol>
<p>Defences against (1) rely on display-layer integrity in the wallet (the wallet must show what it is signing). Defences against (2) and (3) rely on rate-limiting and on the wallet&#39;s policy. None of these are eliminated by the asymmetry rule; we claim only that the rule prevents the catastrophic outcome — direct loss of funds — and reduces the rest to known classes of attack with known mitigations.</p>
<h3>5.4 Empirical evidence</h3>
<p>We have operated <code>zera-mcp</code> against a population of agents during internal red-team exercises since early 2026.\note{TODO: empirical validation — quantify with concrete adversarial-test counts once the red-team report is publishable.} No agent has been able to induce a state delta absent a wallet co-signature, which is the property the asymmetry rule was constructed to deliver. This is consistent with the formal argument above and is necessary but not sufficient evidence: the absence of an exploit during red-teaming does not constitute a security proof.</p>
<h3>5.5 Relation to existing privilege-separation work</h3>
<p>The asymmetry rule is, at one level of abstraction, an instance of the principle of least authority — a discipline with a long pedigree in operating systems and capability-based security. The contribution of this paper is not the principle itself but the observation that cryptographic SDKs admit a particularly clean factorisation along the principle&#39;s axis, and that the factorisation matches the read/compute vs. write taxonomy already present in the MCP specification.</p>
<p>The closest analogue in the smart-contract literature is the <em>separation of view and state-modifying functions</em> enforced at the language level by Solidity&#39;s <code>view</code>/<code>pure</code> qualifiers and by the EVM&#39;s <code>STATICCALL</code> opcode. A view function in Solidity cannot mutate state, and the EVM enforces this by reverting any attempt to do so from a static context. The proposal here is that the MCP layer of a cryptographic SDK should adopt the same discipline, with the SDK author — not the protocol — enforcing that exposed tools have view-or-pure semantics.</p>
<p>There is a subtle but important difference between the EVM&#39;s static-call discipline and the MCP rule we propose. The EVM enforces statefulness through opcode semantics: a contract function is <code>pure</code> if and only if it never reads chain state, and <code>view</code> if and only if it never writes. MCP has no such enforcement, and consequently the discipline must be carried by the SDK author at design time. This makes the rule a code-review artifact rather than a runtime guarantee. We accept this trade-off because the alternative — embedding effect semantics into MCP itself — would expand the protocol&#39;s surface area in ways that are unlikely to be adopted by upstream maintainers in the near term.</p>
<h3>5.6 Failure modes specific to ZK SDKs</h3>
<p>A handful of failure modes are unique to zero-knowledge SDKs and worth registering explicitly.</p>
<p>The first is <em>witness exfiltration.</em> A shielded-spend witness contains the secret key of the consumed note. If <code>build_spend_proof</code> is implemented naïvely, the witness is held in agent-accessible memory long enough that a compromised agent can extract it. We mitigate this by passing the secret key to the prover via an out-of-band channel (a wallet-managed pipe) rather than as an MCP tool argument; the agent constructs the <em>recipe</em> for a spend, but the wallet supplies the secret material at proof-construction time. The agent never sees the secret. This requires careful API design: <code>build_spend_proof</code> accepts a <em>handle</em> for the note (a non-secret identifier), and the wallet resolves the handle to the actual key material.</p>
<p>The second is <em>commitment confusion.</em> A compromised agent can construct a valid Pedersen commitment to a value the user did not authorise, and then surface it to the user as if it were the agreed-upon commitment. The asymmetry rule does not prevent this — <code>compute_commitment</code> is observationally pure regardless of which value it commits to. The mitigation lives in the wallet&#39;s display layer: the wallet must independently re-derive the commitment for the user-confirmed value and refuse to co-sign if the agent-supplied commitment does not match. This is an instance of the broader principle that user confirmation should be a function of values the user can read, not of opaque cryptographic objects whose contents are obscured.</p>
<p>The third is <em>proof-system parameter pinning.</em> If the prover circuit can be selected at MCP-call time — for example, by accepting a circuit identifier as an argument — a compromised agent can request a proof against a circuit that does not enforce the constraints the user expected. The mitigation is to remove the choice from the agent: the wallet pins the circuit set, and the SDK refuses to construct proofs against circuits the wallet has not whitelisted. We adopt this in <code>zera-mcp</code> and recommend it as a default for any SDK exposing a configurable prover.</p>
<h2>6. Conclusion</h2>
<p>The Model Context Protocol is now the default tool-calling surface for AI agents [@anthropic2024mcp]. Cryptographic SDKs that expose themselves via MCP inherit a new principal — the agent — and a new threat model in which the agent may be compromised by adversarial input. We have argued that a small, structural discipline — the asymmetry rule — addresses the most catastrophic class of compromise and is tractable to apply because cryptographic primitives admit a natural read/compute vs. write factorisation that general-purpose SDKs do not.</p>
<p>The rule is opinionated, narrow, and rules out conveniences. We claim that none of those conveniences are worth the catastrophic blast radius they introduce.</p>
<h2>References</h2>
<p>[^ref]</p>
]]></content>
</entry>
</feed>