DOC# PPSTPR SLUG ppst_private_programmable_state PRINTED 2026-05-06 03:47 UTC

PPST: extending SPST to arbitrary private computation

F_RP Construction II. Generalises SPST to private programmable state: arbitrary arithmetic circuits over committed pre/post-state, with R1CS-embedded program execution and atomic PPST-SPST composition.

FROM
Dax the Dev <[email protected]>
SOURCE
https://blog.skill-issue.dev/blog/ppst_private_programmable_state/
FILED
2026-05-02 15:00 UTC
REVISED
2026-05-02 15:00 UTC
TIME
6 min read
SERIES
relayerless-privacy
TAGS
#zk #cryptography #circuits #r1cs #aleo #aztec #phd

SPST gave us private value transfer with self-paying fees on a smart-contract chain. That’s the Solana analogue of Zcash’s Sapling — and exactly what every existing relayer-dependent privacy mixer (Tornado, RAILGUN, Light v1) does, just without the relayer.

But Tornado-style protocols are not the goal. The goal is Turing-complete private computation: a Solana program that runs on encrypted state and produces a proof of correct execution without leaking what the state was, what the inputs were, or what the program output. That’s PPST.

This is post 4 of 11 in the relayerless-privacy series. Reading post 3 first will help, but the construction here stands alone.

What “private program” means here

Definition (Private Program). A private program is an arithmetic circuit

C:FpninFpnoutC : \mathbb{F}_p^{n_{\mathsf{in}}} \to \mathbb{F}_p^{n_{\mathsf{out}}}

over the BN254 scalar field, specified by an R1CS constraint system (A,B,C)(A, B, C) of size NCN_C. Each program is identified by

program_id  =  Poseidon(vkC),\mathsf{program\_id} \;=\; \mathsf{Poseidon}(\mathsf{vk}_C),

where vkC\mathsf{vk}_C is the Groth16 verification key. The program identifier is a public, deterministic commitment to the program’s logic.

Definition (Private State). A vector stateFpk\mathsf{state} \in \mathbb{F}_p^k committed as

cmstate  =  Poseidon(state[0],,state[k1],rstate).\mathsf{cm}_{\mathsf{state}} \;=\; \mathsf{Poseidon}(\mathsf{state}[0], \ldots, \mathsf{state}[k-1], r_{\mathsf{state}}).

State commitments are leaves in a state Merkle tree TS\mathcal{T}_S of depth 32, root rtS\mathsf{rt}_S. This is a separate tree from the SPST note-commitment tree.

Definition (State Transition). A triple (statepre,aux,statepost)(\mathsf{state}_{\mathsf{pre}}, \mathsf{aux}, \mathsf{state}_{\mathsf{post}}) where aux\mathsf{aux} is private auxiliary input and

C(statepre,aux)  =  statepost.C(\mathsf{state}_{\mathsf{pre}}, \mathsf{aux}) \;=\; \mathsf{state}_{\mathsf{post}}.

The transition consumes cmpre\mathsf{cm}_{\mathsf{pre}} via nullification and produces cmpost\mathsf{cm}_{\mathsf{post}} as a new tree leaf. The program logic CC is never revealed to the verifier — only program_id\mathsf{program\_id} is.

The PPST relation

The relation RPPST\mathcal{R}_{\mathsf{PPST}} is the set of (x,w)(x, w) pairs:

Public instance x=(rtpre,rtpost,nfstate,cmpost,program_id,f)x = \bigl(\mathsf{rt}_{\mathsf{pre}}, \mathsf{rt}_{\mathsf{post}}, \mathsf{nf}_{\mathsf{state}}, \mathsf{cm}_{\mathsf{post}}, \mathsf{program\_id}, f\bigr).

Private witness w=(statepre,rpre,pathpre,skstate,aux,statepost,rpost,vkC)w = \bigl(\mathsf{state}_{\mathsf{pre}}, r_{\mathsf{pre}}, \mathsf{path}_{\mathsf{pre}}, sk_{\mathsf{state}}, \mathsf{aux}, \mathsf{state}_{\mathsf{post}}, r_{\mathsf{post}}, \mathsf{vk}_C\bigr).

Nine constraints, all enforced by the outer PPST circuit:

#NameConstraint
P1Program identificationprogram_id=Poseidon(vkC)\mathsf{program\_id} = \mathsf{Poseidon}(\mathsf{vk}_C)
P2Pre-state commitmentcmpre=Poseidon(statepre,rpre)\mathsf{cm}_{\mathsf{pre}} = \mathsf{Poseidon}(\mathsf{state}_{\mathsf{pre}}, r_{\mathsf{pre}})
P3Pre-state membershipMerkleVerify(rtpre,cmpre,pathpre)=1\mathsf{MerkleVerify}(\mathsf{rt}_{\mathsf{pre}}, \mathsf{cm}_{\mathsf{pre}}, \mathsf{path}_{\mathsf{pre}}) = 1
P4State nullificationnfstate=PRFskstate(cmpre)\mathsf{nf}_{\mathsf{state}} = \mathsf{PRF}_{sk_{\mathsf{state}}}(\mathsf{cm}_{\mathsf{pre}})
P5Program executionC(statepre,aux)=statepostC(\mathsf{state}_{\mathsf{pre}}, \mathsf{aux}) = \mathsf{state}_{\mathsf{post}}
P6Post-state commitmentcmpost=Poseidon(statepost,rpost)\mathsf{cm}_{\mathsf{post}} = \mathsf{Poseidon}(\mathsf{state}_{\mathsf{post}}, r_{\mathsf{post}})
P7Post-state tree updatertpost=MerkleInsert(rtpre,cmpost)\mathsf{rt}_{\mathsf{post}} = \mathsf{MerkleInsert}(\mathsf{rt}_{\mathsf{pre}}, \mathsf{cm}_{\mathsf{post}})
P8Fee extractionvalue-bearing state OR companion SPST
P9State authorizationpkstate=PRFskstate(0)\mathsf{pk}_{\mathsf{state}} = \mathsf{PRF}_{sk_{\mathsf{state}}}(0) embedded in pre-state

P5 is the heart of the construction. The user-defined program CC — written in Circom, Noir, Leo, or any high-level circuit DSL — is embedded as a sub-circuit inside the outer PPST relation. The R1CS for CC becomes constraints inside the R1CS for RPPST\mathcal{R}_{\mathsf{PPST}}.

How the program embedding works

The outer circuit sizes look like:

NPPST    Noverhead  +  NCN_{\mathsf{PPST}} \;\approx\; N_{\mathsf{overhead}} \;+\; N_C

where Noverhead25,000N_{\mathsf{overhead}} \approx 25{,}000 R1CS constraints (Merkle paths, commitment hashes, PRF evaluations — see SPST §3.1.6) and NCN_C is the program circuit size.

Program complexityNCN_CTotal PPSTGroth16 prove (M2)
Simple (token transfer, vote, ACL)10³ — 10⁴35,000 — 50,0001 — 3 s
Moderate (private AMM swap, auction bid, credential)10⁵ — 10⁶125,000 — 10⁶5 — 60 s
Complex (private ML inference, DB queries)> 10⁷impractical for direct Groth16minutes — hours

Complex programs need IVC. PPST extends naturally: decompose the computation into TT uniform steps each running CstepC_{\mathsf{step}}, fold them with Nova or SuperNova, then wrap the final accumulator in a Groth16 decider proof. The on-chain verifier always sees a constant-size 128-byte proof regardless of TT. Off-chain proving is O(TCstep)O(T \cdot |C_{\mathsf{step}}|) but the chain doesn’t care.

Theorem 3.6 — PPST soundness

Statement. If Groth16 is knowledge-sound and Poseidon is collision-resistant, no PPT adversary can cause the PPST verifier to accept a transaction corresponding to an invalid state transition (one where C(statepre,aux)statepostC(\mathsf{state}_{\mathsf{pre}}, \mathsf{aux}) \neq \mathsf{state}_{\mathsf{post}}) except with negligible probability.

Proof sketch. Suppose A\mathcal{A} produces a valid PPST transaction whose underlying transition is invalid. By Groth16 knowledge soundness, the extractor E\mathcal{E} recovers a witness ww^* satisfying constraints P1–P9 — including P5: C(statepre,aux)=statepostC(\mathsf{state}^*_{\mathsf{pre}}, \mathsf{aux}^*) = \mathsf{state}^*_{\mathsf{post}}. Direct contradiction. ∎

Corollary (State Integrity). The state tree TS\mathcal{T}_S maintains the invariant that every leaf is a commitment to a state that resulted from a valid execution of an authorized program starting from a previously valid state. By induction on accepted transactions, this invariant holds at all times.

Theorem 3.7 — PPST zero-knowledge

Statement. PPST reveals nothing about statepre\mathsf{state}_{\mathsf{pre}}, statepost\mathsf{state}_{\mathsf{post}}, aux\mathsf{aux}, or the internal logic of CC beyond the public outputs.

Proof sketch. Direct from perfect ZK of Groth16. The simulator S\mathcal{S} depends only on the public instance xx, not on the witness. For any two valid witnesses w0,w1w_0, w_1 consistent with the same xx, the proof distributions are identical.

What does leak:

What does not leak:

Theorem 3.8 — PPST-SPST composability

This is the magic. PPST and SPST compose into a single atomic transaction: execute a private program AND transfer shielded value, in one ZK proof.

Construct the composite relation RPPST+SPST=RPPSTRSPST\mathcal{R}_{\mathsf{PPST+SPST}} = \mathcal{R}_{\mathsf{PPST}} \wedge \mathcal{R}_{\mathsf{SPST}} with a linking constraint:

link  =  Poseidon(nfstate,nf1,,nfn)\mathsf{link} \;=\; \mathsf{Poseidon}(\mathsf{nf}_{\mathsf{state}}, \mathsf{nf}_1, \ldots, \mathsf{nf}_n)

binding the PPST state nullifier to the SPST input nullifiers. Both sub-proofs reference the same link value as a public input.

Cross-constraint (Value Mediation): if the program outputs a transfer amount Δv\Delta v, the SPST component enforces

ivin,i(SPST)  =  jvout,j(SPST)  +  f  +  Δvto_program.\sum_i v^{(\mathsf{SPST})}_{\mathsf{in},i} \;=\; \sum_j v^{(\mathsf{SPST})}_{\mathsf{out},j} \;+\; f \;+\; \Delta v_{\mathsf{to\_program}}.

That is — value flowing from the SPST shielded pool into the program’s state (or out of it) is reconciled inside the proof. An observer cannot tell whether the program consumed value, produced value, or merely transferred it.

Practical realisation. For a moderate program (NC50,000N_C \sim 50{,}000):

Ncomp=NPPST+NSPST+Nlink    (50,000+25,000)+24,000+400    100,000 constraints.N_{\mathsf{comp}} = N_{\mathsf{PPST}} + N_{\mathsf{SPST}} + N_{\mathsf{link}} \;\approx\; (50{,}000 + 25{,}000) + 24{,}000 + 400 \;\approx\; 100{,}000 \text{ constraints}.

Groth16 prover time on commodity hardware: 5–10 seconds. Single 128-byte proof on-chain. ~200,000 CU verification cost on Solana.

Comparison with Aleo and Aztec

AspectProsCons
PPST (this work) Composes with SPST atomically; deployable as Solana smart-contract layer; relayer-free; 128-byte Groth16 proof program_id leaks (no function privacy); per-program Groth16 ceremony; complex programs need IVC
Aleo records model (ZEXE) Universal Marlin/Varuna SRS; native L1 chain with prover marketplace; relayer-free Requires its own L1; program ID also visible; delegated proving leaks witness
Aztec PXE + AVM Universal PLONK/Honk SRS; Noir DSL; client-side proving; relayer-free via FPCs L2 rollup architecture (separate sequencer); function call boundary L→public visible

The thing PPST gets that Aleo and Aztec don’t is deployment as a protocol layer on a high-performance Layer-1. Aleo and Aztec each require running their own consensus or sequencer. PPST runs as a Solana program on the same validators as Jupiter and Helium — inheriting Solana’s TPS, finality, and infrastructure.

What’s left

PPST plus SPST gives us private value + private computation. That’s two of the three privacy properties. The remaining gap is submitter anonymity: even with a perfect ZK proof, the wrapping Solana transaction is signed by an Ed25519 key whose public key is on-chain. Address graph analysis trivially links the “private” transaction to the submitter’s identity.

The next post is about closing that gap — without a relayer, without a mixing service, and without a separate L1.

Bibliography

Previous: SPST: self-paying shielded transactions ← · Next: TAB: threshold-anonymous broadcast →

← Back to article