Sealed-voting flow
Guide · end-to-end lifecycle
This guide walks the complete ShadowKit lifecycle with the real SDK calls. It mirrors the live testnet demos — GovVault at CDYN…WTX5.
The seven steps
Section titled “The seven steps”-
Snapshot eligible holders — build a Poseidon Merkle tree; its root is registered on
GovVaultat init. -
Create a proposal —
create_proposal(action_spec, cap, deadline)returns a proposal id; the deadline fixes the tlock round. -
Prove + seal a vote —
generateVoteProof: Groth16 proof + tlock-sealed(direction, weight). The witness never leaves the browser. -
Cast on-chain —
cast_voteverifies the proof, checks the nullifier + binding + root, and stores the sealed ciphertext. No tally is exposed. -
Sealed until close —
votes_castis public;weighted_yes/nostay None. The tally is cryptographically unknowable until the drand round. -
Close & reveal — after the deadline,
buildRevealArgstlock-decrypts every vote;close_and_revealre-aggregates on-chain →Approved | Rejected. -
Agent executes — on approval, the bounded agent pays for data, plans, and submits a policy-gated swap;
mark_executedmakes it single-shot.
1–2 · Snapshot & propose
Section titled “1–2 · Snapshot & propose”import { buildSnapshot } from "@shadowkit/snapshot-tool";import { bindings } from "@shadowkit/shared";
const snapshot = await buildSnapshot(holders); // depth 20 (== circuit)// GovVault.init(admin, verifierId, snapshot.rootBe32Hex, usdcId, quorumCfg)const proposalId = await govVault.create_proposal({ action_spec, cap, deadline, // deadline → tlock round});3–4 · Prove, seal, and cast
Section titled “3–4 · Prove, seal, and cast”The proof and the sealed ciphertext are produced together so the proof attests the ciphertext is well-formed (public signal #4).
import { generateVoteProof } from "@shadowkit/zk-prover";
const { merklePath, pathIndices } = snapshot.getPath(myLeafIndex);const { proof, publicSignals, sealedCiphertext } = await generateVoteProof( { secret, merklePath, pathIndices, weight, proposalId: String(proposalId), direction: 1, merkleRoot: snapshot.root }, { wasmPath: "/zk/vote.wasm", zkeyPath: "/zk/vote_final.zkey" }, deadlineUnixSeconds,);
await govVault.cast_vote(proposalId, proof, publicSignals, sealedCiphertext);5 · Sealed until close
Section titled “5 · Sealed until close”The running tally is unknowable: weighted_yes / weighted_no are None in ProposalView, and the votes themselves are tlock-encrypted to a drand round that hasn’t happened yet. Only votes_cast (participation) is public.
6 · Close & reveal
Section titled “6 · Close & reveal”import { buildRevealArgs } from "@shadowkit/tally-reveal";
// After the deadline (drand round reached):const args = await buildRevealArgs(proposalId, sealedVotes); // real tlock decryptawait govVault.close_and_reveal( args.proposalId, args.revealedYesW, args.revealedNoW, args.decryptions,);// The chain re-aggregates the decryptions against the committed ciphertexts:// RevealMismatch if any decryption doesn't bind to its stored ciphertext or the sums disagree.7 · Agent executes the approved action
Section titled “7 · Agent executes the approved action”import { AgentRunner } from "@shadowkit/agent";
const runner = new AgentRunner(agentConfig); // session key + Gemini key = server secretsconst { txHash } = await runner.run(proposalId, onLog);// watch(approved) → x402 pay data → LLM plan (≤ cap) → AgentPolicy.enforce → swap → mark_executed