Security

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

KeyWho holds itWhat it allowsWhere it lives today
Spending key skThe owner of each confidential accountBoth viewing and spending for that accountFor 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 vkThe 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 usedDerived on demand from sk. Tally never shares a funder's viewing key with anyone
Lane keysThe funderSpending keys of the lane accounts a round pays from. tally prove needs them. They grant both view and spendThe 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 donorDecrypting the round total that the funder's proof is sealed toGenerated 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·HThe auditor named by auditor_id in the OpenZeppelin auditor registryDecrypting amounts of every account bound to that auditor_idGenerated by ct/scripts/deploy.ts and written outside the repository (see below)
Deployer identity tally-deployerThe person running the deploymentDeploying contracts and registering keys on testnetA 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:

  1. Generates one random auditor key for auditor_id 0 and registers its public point on chain.
  2. Writes the secret to $TALLY_SECRETS_DIR, which defaults to ~/.config/tally, as testnet-auditor-<token>.json with file mode 0600.
  3. Refuses to write a secret anywhere inside the repository. It throws if the target path is the repository or below it.
  4. Writes demo/deployment.testnet.json with public data only: network, contract ids, the auditor's public point and addr_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_e with 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.