Key handling
Which keys exist in a Tally round, who holds each one, where each lives today, the retired demo auditor key and the history rewrite, and the planned threshold custody of the auditor key.
Tally runs on Stellar testnet only. The arrangements on this page are for a testnet demonstration. Do not use them with real value.
The keys
| Key | Who holds it | What it allows | Where it lives today |
|---|---|---|---|
Spending key sk | The owner of each confidential account | Both viewing and spending for that account | For a registering contributor: derived on their own device from a wallet signature, and never sent anywhere. For the demo's lane accounts: see Lane keys |
Viewing key vk | The account owner (derived from sk and the token contract) | Decrypting the account's amounts. For a funder, it would also recompute every ephemeral scalar the funder ever used | Derived on demand from sk. Tally never shares a funder's viewing key with anyone |
| Lane keys | The funder | Spending keys of the lane accounts a round pays from. tally prove needs them. They grant both view and spend | The demo writes them in plaintext to demo/last-round.json. That file is gitignored and holds testnet keys only |
Donor disclosure key (r_R, P_R) | The donor | Decrypting the round total that the funder's proof is sealed to | Generated by the donor with tally challenge. In the published evidence rounds, secret_r_R is in challenge.json on purpose (see below) |
Auditor key k, with public point K_aud = k·H | The auditor named by auditor_id in the OpenZeppelin auditor registry | Decrypting amounts of every account bound to that auditor_id | Generated by ct/scripts/deploy.ts and written outside the repository (see below) |
Deployer identity tally-deployer | The person running the deployment | Deploying contracts and registering keys on testnet | A Stellar CLI identity, created and funded through friendbot by the deploy script if missing |
Spending and viewing keys
Tally follows OpenZeppelin v0.9.0's normative key derivation (docs/sdk/key-derivation.md). The spending key is derived from a deterministic ed25519 signature by the account's own signer, through HKDF-SHA-512, and is bound to both the token contract and the account address. The viewing key vk is derived from sk and the contract.
The specification is explicit about one consequence, and a client must show it to the user: the confidential account is only as private as the key that signs for the address. Anyone who obtains that key can both view and spend. Registration is single-use, so the key cannot be rotated in place. Recovering from a compromise means registering a new address and moving the funds.
Tally's registration code (registration/) runs headless and is tested against live testnet. There is no browser page. See Registering.
Why the funder's viewing key is never shared
A donor who wants to check a total might ask for the funder's viewing key. Tally does not support that. The viewing key would let its holder recompute every ephemeral scalar the funder ever used, which opens every individual amount retroactively, including commitments inside recipients' balances. Challenge and response is the only supported model: the donor brings their own key and nonce, and receives a proof that reveals only the aggregate (README).
Why a donor key is published in the evidence
evidence/round-*/challenge.json contains secret_r_R. This is deliberate. It is the donor's key, not the funder's.
Publishing it makes everyone the donor for that one demonstration round, so anyone can decrypt the total and check it. It reveals nothing about any individual amount and nothing about the funder. A real donor keeps r_R private, and then only that donor learns the total (evidence README).
The secret guard allowlists these files with that reason recorded. They are still scanned for Stellar secret seeds and for the retired key.
The auditor key on the current deployment
The current testnet deployment was made on 2026-10-05 with a fresh auditor key. ct/scripts/deploy.ts does the following:
- Generates one random auditor key for
auditor_id0 and registers its public point on chain. - Writes the secret to
$TALLY_SECRETS_DIR, which defaults to~/.config/tally, astestnet-auditor-<token>.jsonwith file mode0600. - Refuses to write a secret anywhere inside the repository. It throws if the target path is the repository or below it.
- Writes
demo/deployment.testnet.jsonwith public data only: network, contract ids, the auditor's public point andaddr_f.
The demo never needs the auditor secret. It reads the auditor's public key from chain.
OpenZeppelin's specification describes what a compromised auditor key exposes: every amount and checkpoint, and the standing openings, for every account bound to that auditor_id. It does not expose viewing keys or spending authority. The specification asks for the auditor key to be custodied at the same level as a viewing key.
The retired demo auditor key
The commit "feat(demo): reproducible round" (2026-08-21; originally 83f3f8c) put a demo auditor secret key in demo/deployment.testnet.json.
- The key was generated for the testnet demo and guarded nothing of value.
- It is retired. The deployment it belonged to (token
CCDZ52D7…JYHD) is no longer used. - The current deployment's auditor key has never been in the repository.
- The secret guard detects the retired key by its SHA-256 hash, so a copy reappearing in any tracked file fails the build.
The history rewrite
On 2026-10-05 the value was replaced in every historical blob with REDACTED-retired-demo-auditor-key, using git filter-repo --replace-text. No other content changed. Every commit from "feat(demo): reproducible round" onward has a new id. docs/HISTORY-REWRITE.md maps each old id to its new id.
The rewrite has been made in the project's local repository. Copies of the old history may persist elsewhere: in forks, caches and earlier clones. Removing the value from history reduces exposure. It does not un-publish the key. Rotating to a fresh key is what made the exposure harmless.
Planned: threshold custody of the auditor key
Threshold custody is designed, not built. Today the testnet auditor key is a single secret in one file. The design below is from docs/OPERATOR-DESIGN.md §2.
The planned operator service would hold auditor keys so that no single party can decrypt alone.
- 2-of-3 threshold custody. The custodians are the tenant's compliance function, Tally, and an independent custodian the tenant chooses. At least one of the three must be neither the tenant nor Tally. The tenant's freeze and clawback signers must not hold a quorum of shares.
- Key generation. A distributed key generation over the curve the system uses (Grumpkin for OpenZeppelin Confidential Tokens, Baby JubJub for Nethermind's Stellar Private Payments). The group public key is registered with a published proof of possession, because the on-chain registry checks none.
- Decryption. Each custodian returns its share of
k·R_ewith a DLEQ proof. The requester combines them, so only the party entitled to the result ever holds it. Custodians encrypt their contributions to the requester's disclosure key. - Share maintenance. Shares are refreshed on a fixed schedule and whenever a custodian changes. The group key stays the same, which avoids the blind windows and reverted proofs that on-chain key rotation causes.
- History. Shares for every key version are kept for the life of the archive.
- Standing openings (the per-account balances an auditor accumulates) would be additively secret-shared among the same custodians.
The weakest point. OpenZeppelin's clawback proof and auditor-side disclosure proofs need k itself as a private witness. For those, t custodians would release their shares to an attested enclave (TEE) that checks for an approved request in the transparency log, reconstructs k, proves, wipes it, and publishes its attestation. For the duration of one proof, k exists in one place. The design names two alternatives that would avoid this, and both are open questions for SDF and OpenZeppelin.
See Operator design for the full design and Threat model for its threats.
Threat model
What an attacker could try against a confidential payout round as built today, what stops each attempt, and the threat table for the planned operator service.
Secret guard
What scripts/check-secrets.ts detects, how the allowlist works, how the retired demo key is matched by hash, the negative tests, and how CI runs the guard before anything else.