Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions agent/flow-trace/00_INDEX.md

Large diffs are not rendered by default.

95 changes: 67 additions & 28 deletions agent/flow-trace/04_DKG_AND_COMPUTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,12 @@ For `secure-8192`, the threshold key uses plaintext modulus 17,000,000 and three
(`0x0800000000db4001`, `0x0800000000d54001`, `0x0800000000cbc001`). The paired share-encryption key
uses plaintext modulus 576460752317792257 (the largest threshold prime) and two 62-bit primes
(`0x2000000000104001`, `0x200000000013c001`). Both use ring degree 8192 and statistical security
parameter 45. The threshold encryption error variance is
17723039943798878305460955570711717478400. These values bind the C1-C7 witness dimensions and the
on-chain BFV parameter hash. C7 uses `Q_INVERSE_MOD_T = 3898177`, the inverse of the product of the
three threshold primes modulo 17,000,000. New secure E3s use on-chain parameter-set index 2. Index 1
retains the previous secure tuple for historical requests. C3 share encryption and user-data
encryption use non-centered `k1` residues in `[0, t - 1]`; the Rust witness, Noir equation, and
quotient bounds must agree.
parameter 45. The threshold encryption error variance is 17723039943798878305460955570711717478400.
These values bind the C1-C7 witness dimensions and the on-chain BFV parameter hash. C7 uses
`Q_INVERSE_MOD_T = 3898177`, the inverse of the product of the three threshold primes modulo
17,000,000. New secure E3s use on-chain parameter-set index 2. Index 1 retains the previous secure
tuple for historical requests. C3 share encryption and user-data encryption use non-centered `k1`
residues in `[0, t - 1]`; the Rust witness, Noir equation, and quotient bounds must agree.

```
ThresholdKeyshare receives AllEncryptionKeysCollected
Expand Down Expand Up @@ -1928,9 +1927,13 @@ lookup uses the request record, so a round remains visible while its key is pend
the round only when both records exist. Either handler can complete the activation after their
records converge, and deferred checks cover slow live-handler ordering. Duplicate request and
committee events do not reset the round, replace indexed output, or resubmit an already-matching
Merkle root. The shared Interfold contract also emits requests for other E3 programs. The CRISP
indexer ignores those requests before it creates a round or makes a program-specific RPC call. An
old program's historical round therefore cannot stop a fresh CRISP backfill.
Merkle root. A duplicate request whose holder discovery fails keeps the stored census and owes a
retry. The shared Interfold contract also emits requests and plaintext outputs for other E3
programs. The CRISP indexer ignores those requests before it creates a round or makes a
program-specific RPC call. Its `PlaintextOutputPublished` handler leaves a round without a CRISP
record unchanged. An old program's historical round therefore cannot stop a fresh CRISP backfill.
The handler decodes the tally only for a stored round whose recorded program is the configured one.
Any other stored round finishes with an empty tally.

Startup rebuilds deadline callbacks for active and expired rounds and releases an interrupted
compute submission for retry. The compute transition is atomic, and a synchronous program-server
Expand Down Expand Up @@ -2134,17 +2137,48 @@ server that lost its database, or one of several instances, can report `not_inde
Closing the gap entirely would need the guest to tell a replace from an add, which means publishing
that distinction — the thing the whole design exists to hide.

Capacity: `TREE_DEPTH = 20` gives 2^20 entries, against a physical ceiling of roughly three writes
per block at the secure preset — append-only is not capacity-bound.
Capacity: `TREE_DEPTH = 20` gives 2^20 entries. Every input, whether a vote, an update, or a mask,
appends one entry, and `_verifyInputProof` refuses an input once the round holds `inputLimit`
entries (`InputLimitReached`), on both `publishInput` and `validateInputProof`. The limit is at most
`MAX_INPUTS_PER_ROUND` (100,000). Every input adds at most one fresh ciphertext to the sum that the
committee decrypts. Decryption stays correct for `SEARCH_Z` additions, which is 100,000 for
secure-8192. The insecure-512 test preset is sized for 1,024 additions. A CUSTOM round on it, or a
CONSTANT round with zero credits, can accept more inputs than that.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

A mask needs no voter signature, so any account can fill `inputLimit` with masks (`00_INDEX.md`,
"Masks can fill the input limit").

**Plaintext modulus bound.** The committee decrypts each tally coefficient modulo the plaintext
modulus `t` of the round's BFV parameters: 100 for insecure-512 and 17,000,000 for secure-8192. Every
ballot coefficient is 0 or 1 and the tally adds one ballot per selected slot, so a coefficient
counts the ballots that set that bit, and the decoded count is exact only while fewer than `t`
ballots set it. `CRISPProgram` does not enforce the bound, and the input tree (`2^20` entries) does
not prevent a round past it. At secure-8192 a wrong count needs 17 million ballots in one round; at
insecure-512 it needs 100, so a round on that preset with 100 or more voters for one option can
decode a wrong result with every proof valid.
modulus `t` of the round's BFV parameters: 100 for insecure-512 and 17,000,000 for secure-8192. A
ballot holds one weight in `[0, t)` for each option, so the decrypted tally holds per-option totals:
coefficient `o` is the total weight on option `o`, exact only while it is below `t`.
`CRISPProgram.validate` decodes `t` from `e3ProgramParams` (the `BfvParameters` tuple that
`encode_bfv_params` and `encodeBfvParams` write) and sizes the round so that no total reaches it:

- CONSTANT credits: `credits >= t` reverts with `CreditsExceedPlaintextModulus`. A ballot's weights
sum to at most `credits`, so the input limit is `(t - 1) / credits`, capped at
`MAX_INPUTS_PER_ROUND`. Zero credits carry no weight and take `MAX_INPUTS_PER_ROUND`. No divisor
is recorded.
- CUSTOM credits: the round records the token snapshot and a divisor of at least
`getPastTotalSupply(snapshot) / t + 1`. A requested 0 takes that minimum, and a smaller request
reverts with `VotingPowerDivisorBelowMinimum`. A slot's weight bound is
`getPastVotes(slot, snapshot) / divisor`, and the votes of all accounts sum to at most the total
supply, so the bounds of all slots sum to less than `t`. A token that does not answer
`getPastTotalSupply` reverts with `CustomCreditsRequireVotesToken`.

An ONCHAIN round takes each slot's bound from the contract, so the bound holds without trusting the
coordinator. A census-tree round takes it from the census leaf, so it also relies on the coordinator
writing `credits` (CONSTANT) or the scaled voting power (CUSTOM) into each leaf, the same trust the
census already carries. The server reads CUSTOM leaf balances at `snapshotOf(e3Id)`, in the clock
units of the token, which is the snapshot that sized the divisor. It retries a failed `getPastVotes`
read. If the read keeps failing, the server posts no root: `setMerkleRoot` accepts one root, so a
census without that voter would bar the voter for the whole round. The round stays registered with
`discovery_pending` set, and a retry pass posts the root after it reads every voter. A CUSTOM round
also relies on the token: at the snapshot, `getPastVotes` over all accounts must not sum above
`getPastTotalSupply`. ERC20Votes meets this. `BondedVotes` with an escrow votes source does not
guarantee it (see the `BondedVotes` gap in `invariants/01_PROTOCOL_ONCHAIN.md`). The bound still
holds while the FOLD that two accounts both count is at most the FOLD that carries no vote at the
snapshot.

A round where _every_ entry is unusable fails at the output commitment, because the processor's
empty ciphertext does not deserialize. That is only reachable when no honest input exists, and is
Expand Down Expand Up @@ -2186,19 +2220,24 @@ derived from the commitments accepted a mask that published its ballot alone: th
second opening of the parent commitment that satisfied the single equation. A per-coefficient linear
relation, aligned across three ciphertexts that pack with the same `BIT_CT`, proves the same
statement for the committed coefficients under any opening that keeps the carriers, so the circuit
needs no `pack_checked` digit asserts. At secure-8192 the `crisp` circuit is 1,844,049 gates and
`crisp_onchain` 1,824,326, under the `2^21` browser ceiling. With the checked helper on the three
commitments, the secure `crisp` circuit measured 2,520,034 gates.
needs no `pack_checked` digit asserts. At secure-8192 the `crisp` circuit is 1,759,587 gates and
`crisp_onchain` 1,739,864, under the `2^21` browser ceiling.

The circuit returns `sum_ct_commitment` on every path, so the public inputs, the stored commitment,
the ballot digest, and the published ciphertext have the same shape whichever operation ran. Telling
them apart would mean distinguishing a fresh BFV ciphertext from a sum, which the encryption scheme
hides. The SDK has one code path for all three, and `CrispSDK.prepareBallot` makes the same
`state/previous-ciphertext` request either way, so the request pattern says nothing either.

The plaintext is fully constrained on both branches. `check_coefficient_values_with_balance` binds
every coefficient of `k1`: those inside an option segment must be binary, and every coefficient
outside the ballot region must be zero. `check_coefficient_zero` requires the whole polynomial to be
zero for a mask. Both read the payload at `k1[D - MAX_MSG_NON_ZERO_COEFFS ..]`, because the witness
generator reverses the message over the full BFV degree — the ballot occupies the **last** 50
coefficients, and the options appear back to front.
The plaintext is fully constrained on both branches. The witness generator reverses the message over
the full BFV degree, so option `o` is `k1[D - 1 - o]`. `check_coefficient_values_with_balance`
requires every coefficient outside the first `num_options` message positions to be zero and decodes
each option weight. The prover hints the weight `v` and a quotient `r`. `check_weight` range-checks
the coefficient, `v`, and `r` to `[0, t)` and checks `k1 + t * r == Q_MOD_T * v`. With every value
below `t`, the equation holds over the integers, so `v` is the value that the coefficient decrypts
to. The fold binds this `k1` to the plaintext of `user_data_encryption_ct0` through `k1_commitment`.
A packed commitment has one opening only while every coefficient fits its slot.
`user_data_encryption_ct0` range-checks its `k1` to `[0, t)`, and the ballot circuit bounds every
coefficient of its own `k1`. The weights must sum to at most the slot's bound, and with two options
at most one weight is nonzero. `check_coefficient_zero` requires the whole polynomial to be zero for
a mask.
5 changes: 4 additions & 1 deletion agent/invariants/01_PROTOCOL_ONCHAIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ every section.
original behaviour) or an escrow adapter (only locked FOLD votes, so holders must lock to
participate while operators keep weight by bonding). Reading the denominator off the escrow
instead would omit the bonded half and let participation exceed 100%. Summed voting power must
never exceed total supply. — `BondedVotes.sol`; `flow-trace/02`
never exceed total supply. **Gap:** under an escrow source, the vesting-locked term reads the
current locks and the present wallet balance. After a slash leaves a lock larger than its bond, a
past answer can count FOLD that another account also counts at that timepoint. The CUSTOM-credit
divisor of a CRISP round relies on this sum. — `BondedVotes.sol`; `flow-trace/02`; `flow-trace/04`
- **Bonded delegation moves weight; it never copies it.** An owner's bonded weight is its bonded
FOLD, plus its vesting-locked FOLD under an escrow source. At every timepoint it counts at the
owner or at exactly one delegate. Two functions write the links, each at the token's clock in one
Expand Down
62 changes: 37 additions & 25 deletions agent/invariants/02_CRYPTO_CIRCUITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ every section.
rebuild the matching verifier artifacts before deploying a protocol-version-7 node. —
`flow-trace/04`; `scripts/build-circuits.ts`
- fhe.rs v0.4.1 passes plaintext-scaled ballot coefficients as non-centered residues. Both CRISP
vote circuits must check `Q_MOD_T`, rather than `Q_MOD_T_CENTERED`, against those coefficients. —
`examples/CRISP/circuits/bin/{crisp,crisp_onchain}/src/main.nr`
vote circuits decode each ballot weight against `Q_MOD_T`, rather than `Q_MOD_T_CENTERED`. —
`examples/CRISP/circuits/lib/src/utils.nr`
- The C3 and user-data-encryption `k1` witnesses use non-centered residues in `[0, t - 1]`. Their
Noir equations and asymmetric quotient bounds must match the Rust witnesses. —
`circuits/lib/src/core/dkg/share_encryption.nr`; `crates/zk-helpers/src/circuits/`
Expand Down Expand Up @@ -368,15 +368,14 @@ every section.
not create (the parent's and the ballot's ciphertexts) and keep plain `pack`: `verify_slot_update`
checks a relation that is linear and aligned coefficient by coefficient across ciphertexts packed
with the same `BIT_CT`, so an opening that keeps the carriers proves the same relation for the
committed coefficients. With the checked helper on the three commitments, the secure `crisp`
circuit measured 2,520,034 gates, above the browser ceiling. The exemption holds only for that
shape of relation, and only while every commitment in it has a bounded opening that something else
fixes: the ballot through the range checks of `user_data_encryption_ct0/ct1`, and the parent and
the published result because `chain_head_per_slot` takes an entry only when its bytes reproduce
its commitment and it extends the selected head. A check at one point over those commitments, or a
Secure Process that follows a parent by its stored commitment without its bytes, needs injective
openings: chained masks could then carry a coefficient past the radix and shift a plaintext
coefficient by a carry. — `flow-trace/04`
committed coefficients. The exemption holds only for that shape of relation, and only while every
commitment in it has a bounded opening that something else fixes: the ballot through the range
checks of `user_data_encryption_ct0/ct1`, and the parent and the published result because
`chain_head_per_slot` takes an entry only when its bytes reproduce its commitment and it extends
the selected head. A check at one point over those commitments, or a Secure Process that follows a
parent by its stored commitment without its bytes, needs injective openings: chained masks could
then carry a coefficient past the radix and shift a plaintext coefficient by a carry. —
`flow-trace/04`
- A bound that is not tight enough for the slot is no bound for this purpose. C2b's `as u64` cast
limited coefficients to `2^64` while the slot was `radix = 2^64` with `base = 2^60`, so a digit
could still overflow. — `flow-trace/04`
Expand Down Expand Up @@ -570,21 +569,34 @@ every section.
a change to either ballot circuit must regenerate those constants (`pnpm compute:vk-hash`, both
presets) and the generated verifiers; a deployed `CRISPProgram` keeps the circuit its verifier was
built from. — `flow-trace/04`
- **A CRISP tally coefficient is exact only below the plaintext modulus.** Every ballot coefficient
is 0 or 1, and the tally adds one ballot per selected slot, so each decrypted coefficient counts
the ballots that set that bit of that option. The committee decrypts it modulo the plaintext
modulus `t` of the round's BFV parameters: 100 at insecure-512 and 17,000,000 at secure-8192.
Voting power does not change the bound, because a ballot adds at most 1 to each coefficient. A
tally format that is not one bit per coefficient per ballot needs a new bound. **Gap:**
`CRISPProgram` does not limit the slots of a round. When `t` or more ballots in one round set the
same bit, `decodeTally` reads the residue and the count is wrong with every proof valid. That
takes 100 ballots at insecure-512 and 17 million at secure-8192. — `flow-trace/04`
- **CRISPProgram keeps every option total below the plaintext modulus.** A ballot holds one weight
for each option, so coefficient `o` of the decrypted tally is the total weight on option `o`, and
the committee decrypts it modulo the plaintext modulus `t` of the round's BFV parameters.
`CRISPProgram.validate` must read `t` from `e3ProgramParams`, not from a constant, and size the
round so that no total reaches `t`. A CONSTANT-credit round must refuse `credits >= t` and accept
at most `(t - 1) / credits` inputs, capped at `MAX_INPUTS_PER_ROUND`. Each input adds at most one
fresh ciphertext to the decrypted sum. A mask adds its ciphertext to the one already in the slot,
so the count follows the inputs, not the slots. Every secure preset must therefore be searched
with `SEARCH_Z` of at least `MAX_INPUTS_PER_ROUND`. A CUSTOM-credit round must record a divisor of
at least `getPastTotalSupply(snapshot) / t + 1`, which bounds the totals only while the past votes
of all accounts sum to at most the past supply. ERC20Votes meets this. `BondedVotes` with an
escrow votes source does not guarantee it (`01_PROTOCOL_ONCHAIN.md`). Every input counts toward
the limit, including updates and masks, on both `publishInput` and `validateInputProof`. A
census-tree round also relies on the coordinator writing `credits` or the scaled voting power at
the snapshot into each leaf. For a CUSTOM-credit round, the server reads that power at
`snapshotOf(e3Id)`, in the clock units of the token. If the server cannot read the power of a
voter, it posts no root until a retry pass reads every voter. A change to the tally format, the
credit modes, or the parameter sets needs a new bound. — `flow-trace/04`
- **CRISP constrains every coefficient of the ballot plaintext, at the real BFV degree.** The
witness generator reverses the message over the full degree, so the payload starts at
`D - MAX_MSG_NON_ZERO_COEFFS + (MAX_MSG_NON_ZERO_COEFFS mod num_options)` with the options back to
front; `crisp_lib::utils::ballot_layout` derives that offset, both checkers use it, and no code
may hand-code it. Each coefficient inside an option segment encodes one bit as 0 or `q_mod_t`,
everything outside the ballot region must be zero, and a mask's plaintext must be zero everywhere.
witness generator reverses the message over the full degree, so option `o` is `k1[D - 1 - o]`.
`check_coefficient_values_with_balance` must decode each option weight from that coefficient,
require every other coefficient to be zero, and bound the sum of the weights by the slot's bound.
A mask's plaintext must be zero everywhere. `check_weight` must range-check the option
coefficient, the hinted weight, and the quotient to `[0, t)`. Then `k1 + t * r == Q_MOD_T * v`
holds over the integers, and `v` is the value that the coefficient decrypts to. The bound on the
coefficient also gives the packed `k1_commitment` one opening. Thus the fold's equality check
binds this `k1` to the plaintext that `user_data_encryption_ct0` encrypts. Without that bound, a
prover can open the commitment to shifted option coefficients that decode to other weights.
Indexing as if the polynomial were the message width makes both checks read only padding: every
vote passes any balance bound, and a mask — which needs no signature and may be written to any
eligible slot — can carry an arbitrary payload into someone else's ballot. Tests must build `k1`
Expand Down
2 changes: 1 addition & 1 deletion circuits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ From the repository root:
```bash
pnpm tsx scripts/build-circuits.ts # compile circuits, verification keys, artifacts
./scripts/lint-circuits.sh # nargo fmt --check; nargo check (skipped if nargo absent)
./scripts/test-circuits.sh # nargo test: lib, decryption_aggregator, dkg_aggregator (3 preset/committee pairs)
./scripts/test-circuits.sh # nargo test: lib, decryption_aggregator, dkg_aggregator and node_fold (3 preset/committee pairs); nargo execute: config
```

Pin **nargo** and **bb** to the versions in `crates/zk-prover` and `versions.json`. For local work,
Expand Down
Loading
Loading