Quickstart

Verify the published testnet round and run a fresh round yourself, from a clean clone, with the exact commands and the output to expect.

This page takes you from a clean clone to two results:

  1. You verify a round that Tally has already published on testnet (pnpm verify:evidence). This takes about two minutes.
  2. You run a complete new round on testnet yourself (pnpm demo). This takes several minutes.

Everything runs against Stellar testnet. Testnet tokens have no value.

To check the published round without cloning anything, use Verify this round in your browser. It runs the same verification module as tally verify. See Verifying in the browser.

Prerequisites

You needWhy
gitTo clone the repository and its submodule
Node.jsTo run the TypeScript tools through tsx. CI uses Node 24
pnpmTo install dependencies and run the scripts. CI uses pnpm 9
Network access to Stellar testnetverify reads the round and its transfers from the public testnet RPC; demo also funds new accounts through friendbot

You do not need the Noir toolchain to verify a round. The compiled circuits (circuit.json) and the pinned verification keys (vk.zk.bin) are committed to the repository. You need nargo 1.0.0-beta.11 only if you want to rebuild the circuits, and Rust only if you want to run the contract tests.

The repository pins OpenZeppelin stellar-contracts v0.9.0 (df602b6) as a git submodule at vendor/stellar-contracts. Clone with --recurse-submodules, or run git submodule update --init after cloning.

1. Verify the published round

git clone --recurse-submodules https://github.com/Tally-Network/Tally
cd Tally
pnpm install
pnpm verify:evidence

pnpm verify:evidence reads evidence/latest.json to find the currently published round (now round-005). It then runs tally verify with that round's funder address, round id, challenge and bundle.

Expected output

This is the output recorded in evidence/README.md:

✓ round found: 5 lanes, window [5033961, 5033976]
✓ 16 transfers from the declared lanes inside the window
✓ public inputs reconstructed from chain state only
✓ verification key matches the pinned artifact (1760 B)
✓ proof verified (16224 B, zero-knowledge)

TOTAL DISBURSED: 3160  (stroops of the wrapped asset)
over 16 transfers from 5 declared lanes, ledgers 5033961–5033976
no individual amount was revealed.

The tool also prints informational lines marked · between these, such as the registry and token addresses, how many days remain in the retention window, which circuit size it used, and how many public inputs it built.

What each line means

LineWhat verify did
round foundRead the lane set and the ledger window from the round registry contract, using the funder address and round id
16 transfers from the declared lanes inside the windowListed every transfer event sent from a declared lane between the opening and closing ledgers
public inputs reconstructed from chain state onlyBuilt every input to the proof check from chain data and from the donor's challenge. From the funder's bundle it reads only the proof, r_disc and the sealed total
verification key matches the pinned artifactChecked that the key derived from the committed circuit equals the committed vk.zk.bin. It refuses to verify on a mismatch
proof verifiedChecked the zero-knowledge proof
TOTAL DISBURSEDDecrypted the total with the donor's own key and nonce

The donor's secret key for this round is published on purpose in the round's challenge.json. That makes everyone the donor for this one demonstration round. It reveals the total and nothing about any individual amount or about the funder. A real donor keeps that key private, so only they learn the total.

Exit codes

ExitMeaning
0Verified. The total is printed
1Could not verify. For example: a missing argument, no round with that id under that funder, a round with fewer than 5 transfers, or a verification key that does not match the pinned one
2The proof was rejected (PROOF FAILED)
3The round has aged out of the RPC's event retention window. This is not a proof failure

The exit codes reference has the full list.

The ~7-day retention caveat

round-005 was published on 2026-10-05 and is verifiable until ledger 5154939, about seven days later. A scheduled job publishes the next round before then. After that, pnpm verify:evidence exits with code 3.

Soroban RPC serves only a rolling window of events, about 120,960 ledgers or roughly seven days on testnet. verify lists a round's transfers from chain events on purpose, so that the funder cannot choose which transfers the total covers. Once the round's ledgers leave the window, the RPC no longer serves those events.

verify checks the RPC's oldest ledger before it lists anything. If the round is too old, it says so and exits with 3:

✗ this round has aged out of the RPC's event retention window.

    round opened at ledger  5033961
    RPC serves from ledger  5044310   (10,349 ledgers ≈ 0.6 days too old)

  This is not a proof failure. The proof is untouched and would still verify.

If that happens, this command runs a fresh round and publishes it as the next evidence/round-NNN:

pnpm evidence:refresh

This keeps the current evidence verifiable. It does not make an old round verifiable again. That needs a persistent event archive, which Tally has not built. See known limits.

Try to break it

The verifier should reject both of these with PROOF FAILED and exit code 2. These commands come from evidence/README.md:

# inflate the sealed total by one
python3 -c "import json;b=json.load(open('evidence/round-005/bundle.json'));b['v_tilde_disc']=hex(int(b['v_tilde_disc'],16)+1);json.dump(b,open('/tmp/t.json','w'))"
npx tsx cli/tally.ts verify --funder GBNYNIHC6ZFH5D2Z5KPVWRFUG76L2NWHUKAX2KTOEJRAM2WPSUSB3PQY \
  --round 030a11181f262d343b424950575e656c737a81888f969da4abb2b9c0c7ced5dc \
  --challenge evidence/round-005/challenge.json --bundle /tmp/t.json        # expect PROOF FAILED, exit 2

# replay the bundle against a challenge it was not built for
npx tsx cli/tally.ts challenge --out /tmp/c2.json
npx tsx cli/tally.ts verify --funder GBNYNIHC6ZFH5D2Z5KPVWRFUG76L2NWHUKAX2KTOEJRAM2WPSUSB3PQY \
  --round 030a11181f262d343b424950575e656c737a81888f969da4abb2b9c0c7ced5dc \
  --challenge /tmp/c2.json --bundle evidence/round-005/bundle.json          # expect PROOF FAILED, exit 2

If either one passes, something is wrong. Please open an issue.

2. Run a round yourself

git clone --recurse-submodules https://github.com/Tally-Network/Tally
cd Tally
git submodule update --init      # OpenZeppelin stellar-contracts v0.9.0 @ df602b6
pnpm install
pnpm demo

pnpm demo runs demo/run-round.ts against the testnet deployment named in demo/deployment.testnet.json. That file holds public data only. It creates fresh accounts on every run, so you can run it repeatedly.

You can point it elsewhere with three environment variables: TALLY_REGISTRY, TALLY_DEPLOYMENT and TALLY_RPC.

What it does

[pre-round]  one transfer BEFORE open_round        ← must be excluded
[1] open_round(funder, round_id, lanes[])          ← opened_at stamped by the contract
[2] 16 transfers across 5 lanes                    ← amounts encrypted, addresses public
[3] close_round(funder, round_id)                  ← closed_at stamped
[4] DONOR: given only the funder address + round id
       · resolves lane set and window FROM CHAIN
       · enumerates transfers from declared lanes
       · discards anything outside the window
[5] ONE aggregate proof over 16 events / 5 lanes → donor verifies the total

Before step 1 the script creates and funds a funder account, 5 lane accounts and 17 recipient accounts, registers the lanes and recipients as confidential accounts, and deposits into each lane.

The pre-round transfer is the negative case, and it runs every time. Lane 0 sends 999 to a recipient before the round is opened. The donor's total must exclude it and come out as 3160, not 4159. If that ever flips, the window has stopped working.

What to expect

The amounts in the demo are fixed, so the counts and the total are the same on every run. Ledger numbers and timings change. This is the donor part of the run that published round-005 (from demo/README.md):

[4] DONOR — given only the funder address and round id
  resolved from chain: 5 lanes, window [5033961, 5033976]
  transfers from declared lanes (all time) : 17
  inside the declared window               : 16
  excluded by the window                   : 1  <- the pre-round transfer

[5] aggregate proof + donor verification
  proof     16224B in 4723ms, spans 5 sender accounts
  verified  yes
  donor total = 3160   expected 3160   MATCH
  pre-round 999 correctly NOT counted: yes

=== ROUND VERIFIED ===

If any step fails, the script prints FAILED: with the reason and exits with code 1. It also exits with 1 if it does not find exactly 16 transfers inside the window, if the proof does not verify, or if the donor's total does not match.

The file it leaves behind

The demo writes demo/last-round.json. It holds the funder address, the round id, the registry and the lane spending keys, so you can run tally prove against the round afterwards. Spending keys grant both viewing and spending. The file is for testnet only and is ignored by git. A real funder would keep these keys in a signer service.

3. Run the tests

pnpm test                 # secret guard + its negative tests, typecheck, OZ v0.9.0 conformance
                          # vectors, local register/transfer proving, retention logic, contract tests
pnpm test:registration    # live testnet: non-custodial key derivation and registration

pnpm test includes the contract tests, which run cargo test and need a Rust toolchain.

Next steps

  • Walkthrough: the same round as a story, step by step, with the challenge, prove and verify commands.
  • Verifying a round: verify any round, not only the published one.
  • Measurements: costs and limits on testnet.