Enclave is now The Interfold.
Protocol Internals
PVDKG and ZK Proofs

PVDKG and ZK Proof Pipeline

This page describes the publicly verifiable distributed key generation (PVDKG) and the zero-knowledge proof pipeline of the Interfold. It is for auditors and core contributors. It covers the circuits, the DKG state machine, the phases P1 to P4, proof aggregation, on-chain anchoring, and the behavior when committee members crash. At the end, you can follow every proof from the ciphernode that makes it to the contract that checks it.

DKG

Key generation. Up to 19 members publish proven contributions. The key uses an accepted roster of 14.

The scheme is threshold BFV. After sortition finalizes a committee of N ciphernodes, the committee selects an accepted roster of H dealers. The threshold public key is the sum of the H roster contributions. Each roster member holds a Shamir share of the matching secret key, and any T + 1 roster members can decrypt together. The circuits are written in Noir and proved with Barretenberg (bb) as UltraHonk proofs. For the cryptographic specification, read the PVDKG article (opens in a new tab).

Terminology

TermDefinition
Individual key pairA BFV key pair that one ciphernode makes for one DKG. It encrypts the Shamir shares sent to that node.
Secret key contributionOne ciphernode's part of the threshold secret key.
Public key shareThe threshold BFV public key that matches one secret key contribution.
Smudging noise contributionOne ciphernode's part of the smudging noise that protects partial decryptions.
Secret key shareThe Shamir share of the summed secret key that one roster member holds after C4.
Smudging noise shareThe Shamir share of the summed smudging noise that one roster member holds after C4.
Threshold public keyThe sum of the roster public key shares. Data providers encrypt to this key.
Partial decryptionOne roster member's decryption share of a ciphertext, before reconstruction.
Committee size NThe number of ciphernodes that finalizeCommittee selects.
Threshold TThe polynomial degree of the Shamir sharing. Decryption needs T + 1 valid shares.
Roster size HThe exact number of dealers whose contributions form the threshold key.
Accepted roster SThe ascending list of H party IDs that the active aggregator proposes and every node accepts.
Party IDThe index of a member in the address-sorted committee. The lowest address is party 0.
Active aggregatorThe member with the lowest eligible party ID. It coordinates the roster and the aggregate proofs.

Committee shapes and fault budget

crates/committee/src/lib.rs defines three committee shapes. The on-chain registry stores threshold as [H, N]. The runtime field threshold_m carries T and threshold_n carries N.

CommitteeNTHMembers that can drop before the roster (N−HN - H)Roster members that can drop before decryption (H−(T+1)H - (T + 1))
minimum31210
micro94540
small1991454

These numbers describe the circuit shape and the liveness budget. An omitted member is not proven dishonest.

Circuit map

The ProofType enum in crates/events/src/interfold_event/signed_proof.rs names each node-made proof. Its numeric value is the proofType that slashing evidence uses. The CircuitName enum in crates/zk/config/src/circuit.rs maps each circuit to a package under circuits/bin/.

IDProofType (value)CircuitNamePackagePhaseProves
C0C0PkBfv (0)PkBfvdkg/pkP1Commits to the individual public key used for share encryption.
C1C1PkGeneration (1)PkGenerationthreshold/pk_generationP1The public key share comes from the committed secret key contribution.
C2aC2aSkShareComputation (2)SkShareComputationdkg/sk_share_computationP1The secret key Shamir shares match the C1 commitment and are a valid sharing.
C2bC2bESmShareComputation (3)ESmShareComputationdkg/e_sm_share_computationP1The same for the smudging noise shares.
C3aC3aSkShareEncryption (4)ShareEncryptiondkg/share_encryptionP1A secret key share is BFV-encrypted under the recipient's C0 key.
C3bC3bESmShareEncryption (5)ShareEncryptiondkg/share_encryptionP1The same for a smudging noise share.
C4aC4aSkShareDecryption (6)DkgShareDecryptiondkg/share_decryptionP1The decrypted roster shares match their C2a commitments. Commits to their sum.
C4bC4bESmShareDecryption (7)DkgShareDecryptiondkg/share_decryptionP1The same for the smudging noise shares.
C5C5PkAggregation (8)PkAggregationthreshold/pk_aggregationP2The threshold public key is the sum of the H roster shares committed in C1.
C6C6ThresholdShareDecryption (9)ThresholdShareDecryptionthreshold/share_decryptionP4A partial decryption uses the C4 secret key and smudging noise shares.
C7C7DecryptedSharesAggregation (10)DecryptedSharesAggregationthreshold/decrypted_shares_aggregationP4Lagrange interpolation, CRT reconstruction, and decoding give the plaintext.

P3 uses the user-data encryption packages threshold/user_data_encryption_ct0, threshold/user_data_encryption_ct1, and the wrapper threshold/user_data_encryption. They have no ProofType because data providers, not ciphernodes, make these proofs.

Recursive aggregation circuits

The packages under circuits/bin/recursive_aggregation/ fold the inner proofs into two on-chain proofs.

PackageCircuitNameRole
c2ab_foldC2abFoldFolds C2a and C2b.
c3_fold, c3_fold_kernelC3Fold, C3FoldKernelFolds the C3 proofs of one branch in sequence. The kernel makes the first accumulator.
c3ab_foldC3abFoldFolds the final secret key and smudging noise C3 folds.
c4ab_foldC4abFoldFolds C4a and C4b.
node_foldNodeFoldFolds C0, C1, c2ab_fold, c3ab_fold, and c4ab_fold for one node.
nodes_fold, nodes_fold_kernelNodesFold, NodesFoldKernelFolds the H node_fold proofs in sequence.
dkg_aggregatorDkgAggregatorVerifies nodes_fold and C5. Its EVM proof goes to publishCommittee.
c6_fold, c6_fold_kernelC6Fold, C6FoldKernelFolds the T + 1 C6 proofs of one ciphertext.
decryption_aggregatorDecryptionAggregatorVerifies c6_fold and C7. Its EVM proof goes to publishPlaintextOutput.

Proof count per E3

L is the number of threshold CRT moduli: 3 for insecure-64 and for secure-8192. The current TrBFV code makes one smudging noise share set, so the smudging multiplicity Z is 1.

CircuitCount for one nodeCount for the committee
C01Up to N
C11Up to N
C2a1Up to N
C2b1Up to N
C3a(N−1)⋅L(N - 1) \cdot LUp to N(N−1)LN (N - 1) L
C3b(N−1)⋅L⋅Z(N - 1) \cdot L \cdot ZUp to N(N−1)LZN (N - 1) L Z
C4a1H used in the key
C4bZZH used in the key
C51 (active aggregator)1
C61 for each ciphertext outputT + 1 used for each ciphertext output
C71 for each ciphertext output (active aggregator)1 for each ciphertext output

C3 is the most expensive circuit. Its witness is N wide, also when some C0 keys are missing. A dealer proves every slot except its own. For a recipient without a C0 key, the dealer encrypts the share under its own C0 key to fill the slot. The dealer does not deliver that placeholder.

Two key hierarchies

The protocol uses two different BFV key pairs. They use different parameter sets.

KeyPreset (BfvPreset)PurposeLifetime
Individual keyInsecureDkg64 or SecureDkg8192Encrypts the Shamir shares sent to one node. C0 commits to it.One DKG
Threshold keyInsecureThreshold64 or SecureThreshold8192Data providers encrypt to it. C1 proves each contribution.One E3

The threshold key uses a common random polynomial (CRP) aa. Every node derives the same CRP from the default seed with create_deterministic_crp_from_default_seed. The node encrypts its individual secret key at rest with its local Cipher (AES-256-GCM with an Argon2id key).

DKG state machine

Each selected ciphernode runs one ThresholdKeyshare actor for the E3. The actor moves through the KeyshareState enum in crates/keyshare/src/threshold_keyshare/state.rs.

StateWork in this state
CollectingEncryptionKeysMake the individual key pair, request C0, and collect peer C0 keys.
GeneratingThresholdShareMake the TrBFV contribution and the Shamir shares, then request C1, C2, and C3.
AggregatingDecryptionKeyCollect and verify peer shares, publish signed readiness, accept one roster, and compute local key shares.
ReadyForDecryptionProve C4, exchange DecryptionKeyShared, verify peer C4 proofs, and publish KeyshareCreated.
DecryptingCompute the partial decryption of each ciphertext output.
GeneratingDecryptionProofWait for the signed C6 proofs.
CompletedThe node published its decryption share.

Every state except Completed can move to Failed { failed_at_stage, reason }. A persisted failure can only be written again with the same payload, so a restart cannot resume an earlier phase.

P1: Distributed key generation

Frozen DKG timing

When CiphernodeSelected arrives, the node reads getDeadlines(e3Id) and getE3TimeoutConfig(e3Id) from the Interfold contract. It persists the frozen DKG deadline and window before it makes keys. The DKG deadline is the ticket deadline plus dkgWindow. Each collector cutoff is a fraction of that window, measured from the ticket deadline:

tcut=min⁡ ⁣(tdkg−Wdkg+⌊Wdkg⋅bps10 000⌋, tdkg)t_{\text{cut}} = \min\!\left(t_{\text{dkg}} - W_{\text{dkg}} + \left\lfloor \frac{W_{\text{dkg}} \cdot \text{bps}}{10\,000} \right\rfloor,\ t_{\text{dkg}}\right)
CollectorCutoff (bps)KindOverride variable
EncryptionKeyCollector1000 (10%)Hard cutoff for C0 keysE3_ENCRYPTION_KEY_COLLECTION_TIMEOUT_SECS
ThresholdShareCollector7500 (75%)Soft checkpoint. The hard stop is the DKG deadline.E3_THRESHOLD_SHARE_COLLECTION_TIMEOUT_SECS
DecryptionKeySharedCollector10000 (100%)Hard cutoff at the DKG deadlineE3_DECRYPTION_KEY_SHARED_COLLECTION_TIMEOUT_SECS

An override variable can only make a cutoff earlier. After a restart, a collector uses the time that remains, not a new window. The mainnet configuration sets dkgWindow to 21600 seconds. The C0 cutoff is then 36 minutes after the ticket deadline, and the soft checkpoint is at 4.5 hours.

C0: individual key commitment

The node makes its individual BFV key pair and publishes EncryptionKeyPending. ProofRequestActor requests ZkRequest::PkBfv. C0 takes the individual public key polynomials pk0pk_0 and pk1pk_1 over the DKG moduli and outputs pk_commitment. C0 does not prove correct key generation. It binds the key so that C3 can prove which key encrypted each share.

The node publishes EncryptionKeyCreated with the signed C0 proof. Receivers verify it in ProofVerificationActor before they store the key. Before reserving a verification slot, the receiver checks the committee signer, E3, C0 proof type, and commitment to the advertised key. The EncryptionKeyCollector finishes when all N keys arrive. At the C0 cutoff, it continues if it has at least H keys that include the node's own key. With fewer keys, the node records DKGTimeout and stops.

A local verifier error leaves C0 verification pending. The receiver retains the authenticated input and retries after five seconds. Each failed attempt doubles the delay, up to 60 seconds, and logs a warning with the attempt count. Verification completion, a canonical stage past DKG, or E3RequestComplete clears the request. Startup restores unresolved inputs from durable events, including inputs before the snapshot cursor, and resumes verification after EffectsEnabled. The retry delay and attempt count reset on restart. Recovery continues past legacy records that the event-store router quarantines, including whole pages limited by record count or bytes. It checks the physical log head before it ends the scan. Other sequence gaps remain startup errors. A missing verifier binary, missing verification key, or local I/O error produces no peer-failure evidence. Only a completed invalid check starts the accusation path.

C1: threshold share generation

The node requests GenPkShareAndSkSss and then GenEsiSss from TrBFV. It gets its secret key contribution sksk, its public key share, its smudging noise contribution esme_{sm}, and N Shamir shares of each secret. C1 proves the BFV key generation relation for each CRT modulus qℓq_\ell:

pk0=−a⋅sk+e+r2⋅(Xd+1)+r1⋅qℓ,pk1=apk_0 = -a \cdot sk + e + r_2 \cdot (X^{d} + 1) + r_1 \cdot q_\ell, \qquad pk_1 = a

Here dd is the ring degree, and r1r_1 and r2r_2 are quotient witnesses. The circuit checks the relation at one Fiat-Shamir challenge point (the Schwartz-Zippel method). C1 outputs three commitments in this order: sk_commitment for C2a, pk_commitment for C5, and e_sm_commitment for C2b.

C2: share computation

C2a proves the secret key sharing and C2b proves the smudging noise sharing. For each coefficient ii and modulus jj, the share vector is yi,j=(f(0),f(1),…,f(N))y_{i,j} = (f(0), f(1), \dots, f(N)) with f(0)f(0) equal to the secret coefficient. Each circuit checks four properties:

  1. The secret hashes to expected_secret_commitment from C1.
  2. The first entry of each share vector equals the secret coefficient.
  3. Every share is in [0,qj)[0, q_j).
  4. The shares lie on a polynomial of degree at most T. The Reed-Solomon parity check is Hj⋅yi,j⊤≡0(modqj)H_j \cdot y_{i,j}^{\top} \equiv 0 \pmod{q_j}, with a parity matrix of size (N−T)×(N+1)(N - T) \times (N + 1).

C2 outputs one commitment for each (party, modulus) pair, an N×LN \times L grid. C3 and C4 consume this grid.

C3: share encryption

C3a encrypts one secret key share row and C3b one smudging noise share row. Each proof takes two public inputs: expected_pk_commitment from the recipient's C0 and expected_message_commitment from the sender's C2. It proves the BFV encryption relation at a Fiat-Shamir challenge point and outputs ct_commitment. The proof therefore shows that the correct share is encrypted under the correct recipient key.

Share publication

ProofRequestActor makes all C1, C2, and C3 proofs in parallel. It publishes nothing until the complete proof plan is ready. It then signs each proof and publishes one recipient-scoped ThresholdShareCreated for each recipient with a C0 key. That event carries:

  • The public key share and the recipient's encrypted shares.
  • The signed C2a and C2b proofs.
  • The signed C3a and C3b proofs for that recipient.
  • A dealer signature over the E3, dealer, recipient, share bytes, and complete proof bundle.

The signed C1 proof does not travel in ThresholdShareCreated. It travels later in KeyshareCreated.

Share verification

Before saving or collecting a share, ThresholdKeyshare checks its complete message signature against the dealer's finalized committee address. A rejected message leaves the dealer's slot available for an authenticated share. Signature rejection does not accuse the claimed dealer.

ThresholdShareCollector keeps authenticated shares addressed to this node. When all N−1N - 1 external shares arrive, it closes. At the 75% checkpoint, it starts verification if it has at least H−1H - 1 external shares, and it keeps collecting. Later shares extend the verified dealer set until all N−1N - 1 arrive, local key calculation completes, or the DKG deadline passes. Local key calculation completion stops the collector and its timers. Only the DKG deadline can emit ThresholdShareCollectionFailed from an active collector. The parent ignores that failure after it reaches ReadyForDecryption or a later phase.

ShareVerificationActor verifies each batch in three phases:

  1. Signatures. It recovers the signer of each proof, requires one signer for each party, and checks the circuit names.
  2. Commitment consistency. It sends CommitmentConsistencyCheckRequested to the per-E3 checker. Parties with inconsistent commitments skip the ZK step. See commitment consistency.
  3. ZK verification. It runs bb verify for the remaining parties and publishes ShareVerificationComplete with the dishonest parties.

A batch with no proofs that pass local prechecks still produces and saves a verification outcome. A later authenticated share can extend the batch, including after a restart.

The keyshare actor drops failed dealers and C3 proofs that target a different recipient key. It saves each verified dealer as a DkgDealer { party_id, contribution_hash }, where:

contribution_hash=keccak256⁡(abi.encode⁡(keccak256⁡(pk_share), σC2a, σC2b))\text{contribution\_hash} = \operatorname{keccak256}\big(\operatorname{abi.encode}(\operatorname{keccak256}(pk\_share),\ \sigma_{C2a},\ \sigma_{C2b})\big)

The value σ\sigma is ProofPayload.statement_digest(). It covers the E3, the proof type, the circuit, and the public signals, but not the randomized proof bytes. A replay of the same statement therefore cannot make a second dealer identity.

Readiness and roster selection

When at least H dealers, including the node itself, pass verification, the node publishes a signed DkgCoordination message of kind Ready. The message lists the verified dealers in ascending party ID order. The signature covers this digest:

keccak256(abi.encode(
  keccak256("InterfoldDkgCoordination(uint256 chainId,address interfold,uint256 e3Id,uint256 partyId,uint256 kind,bytes32 dealersHash)"),
  chainId, interfold, e3Id, partyId, kind, dealersHash))

The active aggregator runs select_ready_roster. It returns the first H-party set, in ascending party ID order, that is mutually ready:

S⊆Ready,∣S∣=H,∀ i,j∈S: dealerj∈ReadyiS \subseteq \text{Ready}, \quad |S| = H, \quad \forall\, i, j \in S:\ \text{dealer}_j \in \text{Ready}_i

Each member's own dealer entry must be in its own list, with the same contribution hash that the other members hold. The aggregator publishes DkgCoordination of kind Roster. A receiver accepts a roster only when all of these conditions are true:

  • The signer is the committee member at the proposer's party ID.
  • The proposer's party ID is at most the active party ID from AggregatorChanged.
  • The roster has exactly H dealers, and neither the proposer nor a dealer is expelled.
  • The proposer's Ready list and the receiver's own Ready list contain the roster.
  • Every Ready list that the receiver holds for a roster dealer contains the roster.

Before C4 starts, a roster from a lower party ID replaces a roster from a higher party ID. After C4 starts, the roster is fixed. A node starts C4 only if its own Ready list contains every roster contribution. On acceptance, the node emits CommitmentRosterSelected for the commitment checker.

The network actor sends the latest local Ready and Roster messages again every 30 seconds. Each repeat uses a fresh transport delivery ID, so the libp2p duplicate cache does not drop it.

This coordination is not Byzantine agreement. A malicious active aggregator can delay progress until failover. It cannot publish two accepted keys, because the H-row proof enforces one roster and publishCommittee accepts one result.

C4: share decryption and aggregation

Each roster member decrypts the shares that the H roster dealers sent to it, with its individual secret key. The decryption itself is outside the circuit. The node requests CalculateDecryptionKey and gets its secret key share and smudging noise share. C4a and C4b then prove, for the H roster rows:

  1. Each decrypted share hashes to the commitment that its dealer published in C2.
  2. The aggregate is the coefficient-wise sum: agg[ℓ][i]=∑s∈Sshare[s][ℓ][i] mod qℓ\text{agg}[\ell][i] = \sum_{s \in S} \text{share}[s][\ell][i] \bmod q_\ell.
  3. The aggregate is reduced, reversed, and centered to match the representation of C6.
  4. The output is commit(agg_sk) for C4a and commit(agg_e_sm) for C4b.

The node publishes DecryptionKeyShared with its signed C4 proofs. It collects and verifies the C4 proofs of the other H−1H - 1 roster members. It then publishes KeyshareCreated with its public key share and its signed C1 proof. A node outside the roster makes C4 only if it holds all H roster shares. Its C4 does not add a row to the key.

The C4 message also carries a dealer signature over its E3, party ID, node address, and complete ordered proof bundle. The receiver checks this signature before saving the first message or forwarding it to the C4 collector. A rejected message leaves that party's slot available.

P2: Public key aggregation (C5)

Every committee member buffers KeyshareCreated from verified members. After all H roster members submit a keyshare, each member persists VerifyingC1 and publishes AggregationInputsReady(PublicKey). Only the active aggregator then verifies the H C1 proofs. A failed C1 proof ends the DKG with DKGInvalidShares.

The active aggregator sums the H roster shares in roster order:

pk0,agg=∑s∈Spk0,s mod qℓ,pk1,agg=apk_{0,\text{agg}} = \sum_{s \in S} pk_{0,s} \bmod q_\ell, \qquad pk_{1,\text{agg}} = a

C5 checks each share against its C1 pk_commitment and proves the sum. It does not take pk1pk_1 as a witness. It uses the circuit-constant CRP aa directly. C5 outputs commit(pk0_agg, crp). This value is the pkCommitment that the registry stores.

P3: User encryption

Data providers encrypt inputs to the threshold public key with the user-data encryption circuits, which follow the Greco (opens in a new tab) design. The ct0 and ct1 proofs share a u_commitment. The wrapper user_data_encryption outputs (pk_commitment, ct_commitment, k1_commitment). An E3 program binds the input proof to the key. For example, CRISP passes the on-chain committeePublicKey commitment as a public input.

The compute provider runs the E3 program over the ciphertexts. publishCiphertextOutput then stores the output hash and its SAFE ciphertext commitment on-chain.

P4: Threshold decryption (C6, C7)

C6: partial decryption

After CiphertextOutputPublished, each roster member computes one partial decryption for each ciphertext output. For each modulus qℓq_\ell, C6 proves:

dℓ=ct0,ℓ+ct1,ℓ⋅skℓ+esm,ℓ+r2,ℓ⋅(Xd+1)+r1,ℓ⋅qℓd_\ell = ct_{0,\ell} + ct_{1,\ell} \cdot sk_\ell + e_{sm,\ell} + r_{2,\ell} \cdot (X^{d} + 1) + r_{1,\ell} \cdot q_\ell

The smudging noise esme_{sm} hides the secret key share. C6 first checks that sksk and esme_{sm} match the C4 commitments. It binds the witness ciphertext to ct_commitment and checks the relation at a Fiat-Shamir challenge point. Its public inputs are expected_sk_commitment, expected_e_sm_commitment, ct_commitment, domain_hi, and domain_lo. Its output is d_commitment, a commitment to the first 50 coefficients of dd for each modulus.

The decryption domain binds each C6 proof to one E3:

domain=keccak256⁡(abi.encode⁡(chainId, Interfold, e3Id, committeeHash, ciphertextOutputHash, committeePublicKey))\text{domain} = \operatorname{keccak256}\big(\operatorname{abi.encode}(\text{chainId},\ \text{Interfold},\ e3Id,\ \text{committeeHash},\ \text{ciphertextOutputHash},\ \text{committeePublicKey})\big)

The node publishes DecryptionshareCreated with one signed C6 proof for each ciphertext output.

Plaintext aggregation

Every committee member stores valid shares. A valid share comes from an accepted roster member. Its signature, E3, proof type, raw share commitment, ciphertext position, and canonical decryption domain must also be correct. These checks run before the share reserves a party slot. When T + 1 distinct roster shares are durable, the member publishes AggregationInputsReady(Plaintext). For small, that is 10 shares, not all 14. Shares that arrive during C6 verification go to a durable backup queue. If a proof or a raw share check fails, the active aggregator uses backups while T + 1 valid roster shares are still possible.

C7: reconstruction

C7 takes the T + 1 shares with 1-indexed party IDs xix_i in strictly increasing order. It proves three steps:

  1. Lagrange interpolation at zero for each modulus, with coefficients computed in the circuit: uℓ=∑idi,ℓ⋅λi(0) mod qℓu_\ell = \sum_i d_{i,\ell} \cdot \lambda_i(0) \bmod q_\ell, where λi(0)=∏j≠i−xjxi−xj\lambda_i(0) = \prod_{j \ne i} \frac{-x_j}{x_i - x_j}.
  2. CRT reconstruction: uℓ+rℓ⋅qℓ=uglobalu_\ell + r_\ell \cdot q_\ell = u_{\text{global}} for every modulus.
  3. Decoding with the precomputed −Q−1 mod t-Q^{-1} \bmod t, where Q=∏ℓqℓQ = \prod_\ell q_\ell and tt is the plaintext modulus.

C7 compares every decoded coefficient with the claimed message, including coefficients claimed as zero.

Crash tolerance and liveness

The DKG is crash tolerant: it can finish when some members are offline. The protocol omits missing members at defined points. It does not accuse or slash them, because a local timeout does not prove that a peer failed to publish.

Member stopsProtocol behaviorResult
Before its C0 key arrivesOther members continue at the C0 cutoff with at least H keys. They fill its C3 slots with placeholders.It cannot be a dealer or a roster member.
Before its threshold shares arriveOther members start roster work with at least H−1H - 1 external shares and accept later shares.It is not in the roster.
A member has fewer than H verified dealersIt stays outside C4. It does not fail the E3.The E3 continues if a roster forms.
Active aggregator, during roster selectionAfter 10 minutes, the next eligible party becomes active and uses the saved Ready reports.The roster forms if H members are mutually ready.
A roster member, after roster acceptance and before key publicationNo replacement exists. The fixed H-row proof needs every roster member.The E3 fails with DKGTimeout after the DKG deadline.
A roster member is expelled or excluded before key publicationThe public-key aggregator stops the DKG because the fixed H-row proof needs every selected member.The E3 fails.
A roster member is expelled or excluded after key publicationPublic-key aggregators retain canonical publication, including after restart. Decryption still needs T + 1 valid roster shares.Decryption continues if enough valid shares remain and the chain has not failed the E3.
Active aggregator, during C5 or publicationAfter 40 minutes, the next eligible party resumes from its persisted phase.Publication continues if the DKG deadline leaves time.
Up to H−(T+1)H - (T + 1) roster members, before decryptionThe aggregator starts at T + 1 valid roster shares.Decryption continues.
Active aggregator, during plaintext aggregationAfter 10 minutes, the next eligible party resumes.Decryption continues.
⚠️

Before key publication, each of the H selected members is necessary for the DKG. A roster member that stops permanently can fail the E3, even if other committee members stay online.

Public-key aggregators log raw expulsions and exclusions at INFO. Each record identifies the removal kind, node, E3, and, for an exclusion, the proof type. After canonical key publication, the record states that DKG handling is skipped because the key is already published. Chain failures and decryption failures from insufficient valid roster shares still apply.

Restart recovery

A member that restarts recovers its work from the durable event log:

  • It restores collectors with the remaining time to each cutoff. Every new threshold-share collector receives saved expulsions, then all retained authenticated contributions, including when a later share starts collection after restart.
  • Public-key aggregators restore canonical key publication from the lifecycle projection. This prevents a later expulsion or exclusion from failing completed DKG work, including on standby nodes.
  • It holds replayed GenPkShareAndSkSss and GenEsiSss responses until their prerequisites return. It does not make new random shares that conflict with its published C1 to C3 proofs.
  • NodeProofAggregator restores saved inner proofs and fold metadata, then issues the fold again or publishes the saved result.
  • A persisted Failed state publishes the same E3Failed payload again after EffectsEnabled.
  • A local prover, verifier, or worker failure retries the same request. It never counts as proof that a peer sent invalid data.

The node accepts a collector failure only for the same E3 while that collection remains current:

  • Encryption-key failures apply in Init and CollectingEncryptionKeys.
  • Threshold-share failures apply from Init through AggregatingDecryptionKey.
  • C4 failures apply in ReadyForDecryption until C4 verification completes or keyshare publication is authorized.

Canonical key publication supersedes all three collectors. A PublicKeyAggregated intent and its saved public-key context do not suppress a current collector failure. The node records canonical publication for the matching E3 even when PublicKeyAggregated has not arrived. An earlier stage does not reopen collection. On restart, the node restores publication awareness from the saved E3 lifecycle projection. An ignored failure changes no state and publishes no failure event.

When a current collector timeout is fatal, the node records a local E3Failed event. On-chain, the E3 stays at CommitteeFinalized until the DKG deadline. After that, markE3Failed records DKGTimeout.

Proof aggregation and on-chain anchoring

The contracts accept only aggregated proofs. The active aggregator submits one recursive proof for the DKG and one for the decryption. Production nodes always aggregate. The EVM writer publishes a single ciphertext output only. It refuses to publish an E3 result that has more than one decrypted output.

ZkRequestMade byOutput
NodeDkgFoldEach roster memberA NodeFold proof. The node then signs a fold attestation.
DkgAggregationActive aggregatorNodesFold, C5, and the DkgAggregator EVM proof
DecryptionAggregationActive aggregatorC6Fold, C7, and the DecryptionAggregator EVM proof for each output

Committee hash

Both final proofs bind the ordered committee. The hash covers the raw 20-byte addresses of topNodes in ascending order, without padding:

h=keccak256⁡(a0 ∥ a1 ∥ ⋯ ∥ aN−1),hhi=h≫128,hlo=h mod 2128h = \operatorname{keccak256}(a_0 \,\|\, a_1 \,\|\, \cdots \,\|\, a_{N-1}), \qquad h_{\text{hi}} = h \gg 128, \quad h_{\text{lo}} = h \bmod 2^{128}

Solidity computes it in CommitteeHashLib.hash. Rust computes it in e3_committee_hash::hash_committee_addresses (crates/committee-hash). Noir computes it in committee_hash_limbs. A shared test vector checks that the three implementations agree.

What the DKG proof enforces

node_fold verifies the five inner proofs of one node and asserts these same-party links:

  • C1 sk_commitment equals the C2a expected_secret_commitment, and C1 e_sm_commitment equals the C2b one.
  • The C3a and C3b recipient key commitments are equal for every slot.
  • For every slot except the node's own, the C3 message commitment equals the C2 share commitment.

dkg_aggregator verifies nodes_fold and C5, then asserts these cross-party links:

  • The committee hash limbs match the N committee addresses.
  • The H party IDs are strictly increasing and less than N.
  • Each folded row carries the matching party ID.
  • For each roster sender ss and recipient rr, the C3 key slot of ss for rr equals the C0 commitment of rr.
  • For the same pairs, the C4 rows of rr for ss equal the C2 commitments of ss for rr. This applies to both branches.
  • Row ii of C5 equals the C1 pk_commitment of roster member ii.

DKG proof public inputs

The DkgAggregator EVM proof has 3H+63H + 6 public inputs:

IndexValue
0nodes_fold_key_hash
1c5_key_hash
2 to 1+H1 + Hparty_ids (0-indexed committee slots)
2+H2 + Hcommittee_hash_hi
3+H3 + Hcommittee_hash_lo
4+H4 + HAggregate verification key hash
5+H5 + H to 4+2H4 + 2Hsk_agg_commits
5+2H5 + 2H to 4+3H4 + 3Hesm_agg_commits
5+3H5 + 3HAggregated public key commitment

Fold attestations

After its NodeFold proof, each roster member signs an EIP-712 attestation and gossips it in DKGRecursiveAggregationComplete. The domain and struct come from DkgFoldAttestationLib:

// EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
// name = "InterfoldDkgFoldAttestation", version = "1",
// verifyingContract = the DkgFoldAttestationVerifier frozen for this E3
DkgFoldAttestation(address registry,uint256 e3Id,uint256 partyId,bytes32 skAggCommit,bytes32 esmAggCommit)

The aggregator collects exactly H attestations and builds the bundle (Attestation[], PartySlotBinding[]). Each binding maps a partyId to an operator address.

publishCommittee verification

Any caller can call publishCommittee(e3Id, pkCommitment, proof, dkgAttestationBundle). The registry checks, in this order:

  1. The committee is Finalized, activeCount is at least H, no key is published yet, and pkCommitment is not zero.
  2. It computes the committee hash from topNodes and stores pkCommitment.
  3. The E3 pkVerifier (BfvPkVerifier) checks the public input count and the committee hash limbs. It compares the two VK hashes with its immutable expectedNodesFoldKeyHash and expectedC5KeyHash, and the last input with pkCommitment. Then the Honk verifier checks the proof.
  4. The E3's frozen DkgFoldAttestationVerifier derives H=(len−6)/3H = (\text{len} - 6) / 3 and requires strictly increasing proof party IDs and H bindings. For each binding, it requires:
    • canonicalCommitteeNodeAt(e3Id, partyId) equals the bound node.
    • The node is an active committee member.
    • The EIP-712 signer is the bound node.
    • The signed skAggCommit and esmAggCommit equal the proof values at that party's slot.
  5. The registry stores dkgPartyIds, dkgSkAggCommits, and dkgEsmAggCommits for the E3.
  6. Interfold.onCommitteePublished moves the E3 to KeyPublished. It reverts after the DKG deadline, and after the input window end with InputWindowClosedBeforeKeyPublication.
  7. The registry emits CommitteeProofPublished(e3Id, nodes, pkCommitment, proof).

BfvPkVerifier.verify accepts e3Id, committeeRoot, and sortedNodes but does not use them. The current DKG circuit has no public input for them. The proof binds to the committee through the committee hash. The fold attestations bind the registry, the E3, the chain, and the verifier.

The slot binding stops an aggregator from moving a signed attestation to a different party ID. The anchors let later steps map each DKG output to topNodes[partyId].

Public key transport

publishCommittee stores only the commitment. Committee members then send the serialized key with publishCommitteePublicKey in 90 KiB chunks, with a maximum of 512 KiB, while the E3 is at KeyPublished. The registry emits CommitteePublicKeyChunkPublished. A consumer assembles a complete chunk set, checks keccak256(publicKey) == candidateHash, and recomputes the commitment before it uses the key.

Ciphernodes also check peer key publications against the registry commitment, finalized committee, and DKG anchor party IDs. They recompute the commitment from the key bytes and retain the first matching key. Peer publications cannot replace the decryption domain or either committee roster. Nodes derive these facts from confirmed registry events. After a restart, they rebuild the projection and validated key bytes from retained event and chunk history before proof replay. They repair retained C6 intents before deduplication and reject logged compute requests with noncanonical public inputs. This recovery needs no historical registry storage reads or new key publication.

Plaintext hydration recovers deferred ciphertext from retained history, including events before the snapshot cursor. It also recovers authenticated shares received before that cursor. A missing plaintext actor can therefore resume without another ciphertext or share publication. An existing snapshot remains dormant until confirmed key authority arrives. Startup continues, and the node retains the snapshot, a durable-history range, and one effects-enabled signal. It reads deferred payloads in bounded pages when authority arrives. Hydration also checks saved C6 shares and the C6 inputs retained in later aggregation phases. If a domain is invalid, the node clears the affected verification results and derived proofs, including a retained final proof. It rebuilds collection from signed history with effects disabled. A corrected share can then occupy the same party slot. Valid retained C6 work keeps its phase. Retained C7 proofs must also match that batch and its plaintext. A mismatch regenerates C7 and the final proof, including for a saved completed actor. The EVM writer separately checks each final proof against the canonical decryption domain before it retains a publication intent. It waits for missing authority and discards mismatched intents. A corrected local result can then proceed to publication. Confirmed terminal stages retire deferred and pending plaintext publication work, including after a restart. Later intents for a terminal E3 are discarded. A waiting active E3 retains one event-log sequence range, and admission reads that range in bounded pages when canonical authority arrives.

Decryption proof

publishPlaintextOutput computes the decryption domain on-chain and calls the E3 decryption verifier (BfvDecryptionVerifier). The DecryptionAggregator EVM proof has 8+3(T+1)+1008 + 3(T + 1) + 100 public inputs:

IndexValue
0, 1c6_fold and C7 VK hashes
2, 3committee_hash_hi, committee_hash_lo
4, 5domain_hi, domain_lo
6ciphertext_commitment
7Aggregate verification key hash
next 3(T+1)3(T + 1)party_ids, then expected_sk, then expected_esm
last 50Plaintext coefficients

decryption_aggregator requires one domain and one ciphertext commitment across all C6 leaves. It requires each C6 d_commitment to equal the matching C7 input. Its party IDs must be 1-indexed, strictly increasing, and at most N. The wrapper then checks:

  • Every public input is a canonical BN254 field element, and each plaintext coefficient fits in 64 bits.
  • The VK hashes, the committee hash limbs, the domain limbs, and the stored ciphertext commitment.
  • keccak256 of the 50 coefficients, each as 8 little-endian bytes, equals the plaintext hash.
  • For each party, party_id - 1 is in the DKG anchors, and the proof commitments equal the stored dkgSkAggCommits and dkgEsmAggCommits.

Verifier sync

The Honk verifiers DkgAggregatorVerifier.sol and DecryptionAggregatorVerifier.sol are generated from the recursive VKs of dkg_aggregator and decryption_aggregator with bb write_solidity_verifier. The files in packages/interfold-contracts/contracts/verifiers/bfv/honk/ are the insecure-64 and minimum pair. The other pairs are in honk/<preset>/<committee>/. A deployment must use the pair that matches its preset and committee.

pnpm generate:verifiers has two modes. --check regenerates in memory and fails on any difference. tests/integration/lib/prebuild.sh uses it. --write overwrites the committed files. Both modes refuse to run unless dist/circuits/<preset>/<committee>/.build-stamp.json exists and circuits/bin/.active-preset.json names the same pair. The toolchain versions are pinned in crates/zk/prover/versions.json.

Verifier rotation

Each E3 freezes its registry and its DkgFoldAttestationVerifier at request time and announces both in DkgFoldAttestationContextEstablished. A node restores this context after a restart. A verifier change uses proposeDkgFoldAttestationVerifier, then commitDkgFoldAttestationVerifier after DKG_FOLD_VERIFIER_TIMELOCK (2 days). The owner can cancel a proposal with cancelDkgFoldAttestationVerifierProposal. The one-time setInitialDkgFoldAttestationVerifier works only when no verifier is set. A new verifier applies only to later E3 requests.

Test-only aggregation bypass

A binary built with the Cargo feature test-only-skip-proof-aggregation accepts the node setting skip_proof_aggregation. The node wraps C5 and C7 proof bytes in payloads for mock verifiers. The DKG placeholder carries the final roster, committee hash, and key commitment fields. The decryption placeholder carries the verified C6 domain. These fields let confirmed ingestion and the plaintext writer keep their admission checks. A production binary rejects this setting at startup, and production verifiers reject the placeholder bytes. The contracts still call their verifiers.

Proof lifecycle

Generation

A *Pending event reaches ProofRequestActor, which dispatches ComputeRequest::zk with a ZkRequest. ZkActor writes the witness and runs bb prove, then returns Proof { data, public_signals }. A failed attempt retries the same request. Attempts after the first use the Barretenberg low-memory mode.

C7 requests bind to the exact share batch and plaintext. A replacement request invalidates earlier worker responses. Plaintext aggregation checks C7 commitments, party IDs, and plaintext against the selected C6 batch. Restart recovery regenerates mismatched C7 work before final aggregation.

Signing

The node signs every proof with its operator key before it sends the proof. The digest is:

keccak256(abi.encode(
  keccak256("ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)"),
  chainId, e3Id, proofType, keccak256(proof.data), keccak256(proof.public_signals)))

SignedProofPayload::sign signs this digest as an EIP-191 personal message and stores the 65-byte signature. The signature binds the proof to one chain, one E3, and one proof type, and it identifies the prover.

Transport

EventProofsPath
EncryptionKeyCreatedC0DHT document, announced over gossip
ThresholdShareCreatedC2a, C2b, and the recipient's C3a, C3bDHT document, announced over gossip
DecryptionKeySharedC4a, C4bDHT document, announced over gossip
KeyshareCreatedC1Gossip
DecryptionshareCreatedC6 for each ciphertext outputGossip

C5 and C7 do not travel as separate proofs. They enter the recursive proofs of the active aggregator.

A node checks each document notification's structure and expiry before gossip forwards it. Valid notifications for other parties still relay. Malformed messages are rejected. A well-formed notification that expires between hops is ignored without a penalty to the relay's score. Admission does not impose message-count or byte-rate throttles on a relay. Each message must still fit the wire size limit.

Document fetches use up to eight active reads and 512 waiting documents. A peer can use all idle capacity. When peers compete, the next due peer with the fewest active reads gets the next slot; ties rotate. Each read releases its slot after one attempt, within 90 seconds. Failed reads return to the queue with backoff. At capacity, a peer below its fair share can reclaim space from the largest owner. Notifications repeat to recover queue overflow.

Each document retains up to 128 announcing peers through queueing and retries, including announcements that arrive during a read. A new announcer replaces the oldest one after the first when this limit is reached. Any retained announcer can supply its next fair slot. The node still fetches each document once at a time, and duplicates do not advance retry deadlines. The retained announcer with the least queued work supplies queue capacity, including for retries. Before eviction, shared work moves to a less loaded announcer. Queue growth from one peer cannot discard another announcer's only pending document.

Capacity planning uses four concurrent N=19, H=14 E3s and a 2x margin. Each E3 can emit 380 document notifications and 173 ordinary publications, including six Ready updates per party. This gives 4,424 publications per round with the margin. Each seen-ID cache reserves four rounds for initial traffic and early retries, plus 15 IDs/s for six hours: 341,696 entries. A node needs 54 remote documents per E3; four E3s with the margin need 432 queue slots, rounded up to 512. Early notifications have 3,040 slots because the recipient is not yet known. These entries distinguish the announcing peer, document, and party filter, so committee selection preserves each peer's attribution.

Seen-ID caches admit every new ID. Peers borrow idle space; at capacity, a peer at its fair share replaces its own oldest ID. A peer below its share reclaims space from the largest owner. Early eviction shortens retention and increments the seen_ids_early_evictions_total counter in WARN logs. Cache pressure never discards accepted startup events. Gossip tracks transport delivery IDs; storage tracks stable event IDs, so a retransmission does not add another logical contribution. A restart starts new windows. Message IDs, event IDs, and serialized data remain unchanged.

Verification

ProofVerificationActor verifies C0. ShareVerificationActor verifies the other node proofs with four kinds of batches: ShareProofs (C2, C3), DecryptionProofs (C4), PkGenerationProofs (C1), and ThresholdDecryptionProofs (C6). A passed proof emits ProofVerificationPassed, which the commitment checker and AccusationManager cache. An invalid proof emits SignedProofFailed, which starts the accusation pipeline. A local verifier process error retries and does not accuse the sender.

Commitment chain

Each circuit commits to its outputs with SAFE, a sponge API over Poseidon2 that circuits/lib/src/math/safe.nr implements. A later circuit takes the expected commitment as a public input, recomputes it from private inputs, and requires equality. Only compact commitments go on-chain.

C0  ──→ C3   (recipient pk_commitment)
C1  ──→ C2a  (sk_commitment)
C1  ──→ C2b  (e_sm_commitment)
C1  ──→ C5   (pk_commitment)
C2a ──→ C3a  (share commitment)
C2b ──→ C3b  (share commitment)
C2a ──→ C4a  (expected_commitments for all L moduli)
C2b ──→ C4b  (expected_commitments for all L moduli)
C4a ──→ C6   (expected_sk_commitment)
C4b ──→ C6   (expected_e_sm_commitment)
C6  ──→ C7   (d_commitment)

The recursive circuits enforce these links for the published result. The off-chain commitment consistency checker checks the same links to find the party at fault.

BFV parameter presets

crates/fhe-params/src/constants.rs defines the presets. Version-7 E3 requests use paramSet 0 for insecure-64 and 2 for secure-8192. Index 1 remains in the registry for older E3s.

Parameterinsecure-64secure-8192
Ring degree dd648192
Threshold CRT moduli L33
DKG CRT moduli22
Threshold plaintext modulus tt1700000017000000
Statistical security parameter λ\lambda245
UseLocal development and tests onlyProduction

Circuit artifacts are compiled separately for each preset and committee. The prover and the verifier must use the same pair.

Next steps