Integrate ShadowKit
Guide · adoption
ShadowKit is a drop-in SDK: four ESM packages plus a set of Soroban contracts. You can use the
live testnet contracts as-is, or deploy your own. This page is the shortest path from npm install
to a working private vote in your application — browser, Node, or a Cloudflare Worker.
1 · Install
Section titled “1 · Install”npm install @shadowkit/zk-prover @shadowkit/snapshot-tool \ @shadowkit/tally-reveal @shadowkit/sharedYou also need the circuit artifacts (vote.wasm, vote_final.zkey, poseidon3.wasm) and the
verification key. Serve them as static assets (the demo serves them from /zk/*) so the prover can
fetch them in the browser, or read them from disk in Node.
2 · Point at the contracts
Section titled “2 · Point at the contracts”You have two choices:
-
Reuse the deployed testnet contracts — copy the IDs below into your config and skip deployment. Good for prototyping and demos.
-
Deploy your own —
git clonethe repo, thenjust deploy-testnet(wrapsscripts/deploy-demo.sh). It builds the wasm, deploysGroth16Verifier→GovVault→AgentPolicy→FallbackAMM, registers your snapshot root, and writes every ID to.env.demo.testnet. Required for production / mainnet.
export const CONFIG = { rpcUrl: "https://soroban-testnet.stellar.org", networkPassphrase: "Test SDF Network ; September 2015", govVaultId: "<GovVault C... id>", verifierId: "<Groth16Verifier C... id>", agentPolicyId: "<AgentPolicy C... id>", merkleRoot: "<snapshot root, 32-byte hex>", // must equal the root registered on GovVault.init};3 · Build your eligibility snapshot
Section titled “3 · Build your eligibility snapshot”Your voter set is a Poseidon Merkle tree over BLS12-381 (depth 20, matching the circuit). The root is
registered once on GovVault.init; each member keeps a secret + their Merkle path.
import { buildSnapshot } from "@shadowkit/snapshot-tool";
const snapshot = await buildSnapshot(holders); // [{ secret, weight, direction }]// snapshot.rootBe32Hex → register on GovVault.init(...)// snapshot.members[i] → { secret, weight, direction, merklePath, pathIndices }4 · Prove + seal a vote (client side)
Section titled “4 · Prove + seal a vote (client side)”One call produces a real Groth16 proof and the tlock-sealed ballot. The witness — identity, weight, direction — never leaves the client.
import { generateVoteProof } from "@shadowkit/zk-prover";
const { proof, publicSignals, sealedCiphertext } = await generateVoteProof( { ...member, merkleRoot, proposalId, sealKey }, { wasmPath: "/zk/vote.wasm", zkeyPath: "/zk/vote_final.zkey" }, deadline, // unix seconds → tlock drand round);publicSignals are the four field elements in the on-chain binding order
[merkleRoot, nullifier, proposalId, sealedCommitmentHash].
5 · Cast on-chain
Section titled “5 · Cast on-chain”Marshal the proof to the verifier’s byte layout and submit cast_vote. The voter’s wallet is the
transaction source; it pays the fee but reveals nothing about the ballot.
const op = govVault.call( "cast_vote", nativeToScVal(proposalId, { type: "u32" }), proofScVal(proof), // Proof { a: BytesN<96>, b: BytesN<192>, c: BytesN<96> } pubSignalsScVal(publicSignals), // Vec<Fr> ← see the gotcha below sealedVoteScVal(sealedCiphertext), // SealedVote { ciphertext, round, sealed_commitment_hash });While the proposal is open, votes_cast increments but weighted_yes/no stay null — the tally is
cryptographically unknowable until the drand round releases.
6 · Close & reveal
Section titled “6 · Close & reveal”After the deadline, decrypt every sealed ballot and re-aggregate on-chain. The contract recomputes the tally from the decrypted votes, so no one ever had to trust an off-chain count.
import { buildRevealArgs } from "@shadowkit/tally-reveal";
const args = await buildRevealArgs(sealedVotes); // tlock-decrypt each ballotawait govVault.call("close_and_reveal", /* id, weighted_yes, weighted_no, decryptions */);// → status becomes Approved | Rejected7 · Bound the execution agent (optional)
Section titled “7 · Bound the execution agent (optional)”On approval, a policy-gated agent can carry out the result. AgentPolicy is a smart-account policy the
agent cannot break: it caps the amount, pins the asset pair, and enforces single-shot execution via
mark_executed. Wire your own LLM/planner behind it — the on-chain policy is the trust boundary, not the model.
See Agent for the full policy gate and the AgentBoard demo.
Production checklist
Section titled “Production checklist”- Deploy your own contracts (don’t ship against the shared testnet IDs).
- Register your real snapshot root on
GovVault.init; keep secrets client-side only. - Serve the circuit artifacts with long-cache headers; verify their hashes match your trusted setup.
- Encode
pub_signalsasScVal::U256(see the gotcha in step 5). - Test the full propose → cast → close → reveal loop on testnet before mainnet.
- Review Architecture and Contracts for the policy + error model.
Next: the complete lifecycle with every real call is in the Sealed-voting flow guide.