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
18 changes: 9 additions & 9 deletions agent/flow-trace/00_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ _Found during source-code cross-referencing of these trace documents._

### Protocol Design Concerns

| # | Concern | Severity | Detail |
| --- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Deregister-before-slash race** | Accepted | SlashingManager Lane B (evidence+appeal) has a window during which the operator can deregister and claim their exit. If they do, the slash executes against 0 funds. The contract comments acknowledge this as an accepted tradeoff for the appeal window design. |
| 2 | **Committee publication decentralized** | Resolved | `publishCommittee()` is permissionless. Off-chain role selection chooses the active aggregator, while on-chain C5 proof verification and the single-publish guard prevent invalid or duplicate committee publication. |
| 3 | **`gracePeriod` is dead code** | Medium | `gracePeriod` is stored and validated during config updates but never actually used in any timeout check. Either the deadlines already bake in sufficient buffer, or this is a missing feature. |
| 4 | **`activate` CLI command is misleading** | Low | Named "activate" but actually calls "register" — will fail for already-registered operators. There's no standalone way to trigger re-evaluation of active status; instead, `_updateOperatorStatus()` runs automatically inside `addTicketBalance()`, `bondLicense()`, etc. |
| 5 | **Active-job load balancing bug fixed** | Info | The Rust `NodeStateStore.available_tickets()` subtracts `active_jobs` from total tickets, reducing the chance of busy nodes being selected for new E3s. Previously, the `Sortition` actor's `Handler<EnclaveEvent>` was missing match arms for `E3Failed` and `E3StageChanged`, causing these events to fall to the default `_ => ()` — the typed handlers for decrementing jobs were dead code. This has been fixed: E3Failed and E3StageChanged are now routed to their handlers, and `finalized_committees` is cleaned up in `decrement_jobs_for_e3` to prevent unbounded memory growth. |
| 6 | **Committee member expulsion** | Info | `SlashingManager` can call `expelCommitteeMember()` mid-DKG. The `Sortition` actor enriches the raw `CommitteeMemberExpelled` event with the expelled member's `party_id` (resolved from its stored `Committee` list) and re-publishes it. `ThresholdKeyshare` then uses the enriched `party_id` to update its collectors, potentially completing DKG with fewer parties. `ThresholdKeyshare` itself does not hold committee state. |
| 7 | **NodeProofAggregator stall bridge fixed** | Info | `NodeProofAggregator` no longer drops `DKGInnerProofReady` events that arrive before `ThresholdSharePending`; it prebuffers them until collection state exists. It also converts `NodeDkgFold` `ComputeRequestError` into `DKGRecursiveAggregationComplete { aggregated_proof: None }` instead of silently discarding actor state, preventing DKG proof aggregation stalls when fold workers fail or events arrive slightly out of order. |
| # | Concern | Severity | Detail |
| --- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Deregister-before-slash race** | Accepted | SlashingManager Lane B (evidence+appeal) has a window during which the operator can deregister and claim their exit. If they do, the slash executes against 0 funds. The contract comments acknowledge this as an accepted tradeoff for the appeal window design. |
| 2 | **Committee publication decentralized** | Resolved | `publishCommittee()` is permissionless. Off-chain role selection chooses the active aggregator, while on-chain C5 proof verification and the single-publish guard prevent invalid or duplicate committee publication. |
| 3 | **`gracePeriod` is dead code** | Medium | `gracePeriod` is stored and validated during config updates but never actually used in any timeout check. Either the deadlines already bake in sufficient buffer, or this is a missing feature. |
| 4 | **`activate` CLI command is misleading** | Low | Named "activate" but actually calls "register" — will fail for already-registered operators. There's no standalone way to trigger re-evaluation of active status; instead, `_updateOperatorStatus()` runs automatically inside `addTicketBalance()`, `bondLicense()`, etc. |
| 5 | **Active-job load balancing bug fixed** | Info | The Rust `NodeStateStore.available_tickets()` subtracts `active_jobs` from total tickets, reducing the chance of busy nodes being selected for new E3s. Previously, the `Sortition` actor's `Handler<EnclaveEvent>` was missing match arms for `E3Failed` and `E3StageChanged`, causing these events to fall to the default `_ => ()` — the typed handlers for decrementing jobs were dead code. This has been fixed: E3Failed and E3StageChanged are now routed to their handlers, and `finalized_committees` is cleaned up in `decrement_jobs_for_e3` to prevent unbounded memory growth. |
| 6 | **Committee member expulsion** | Info | `SlashingManager` can call `expelCommitteeMember()` mid-DKG. The `Sortition` actor enriches the raw `CommitteeMemberExpelled` event with the expelled member's `party_id` (resolved from its stored `Committee` list) and re-publishes it. `ThresholdKeyshare` then uses the enriched `party_id` to update its collectors, potentially completing DKG with fewer parties. `ThresholdKeyshare` itself does not hold committee state. |
| 7 | **NodeProofAggregator stall bridge fixed** | Info | `NodeProofAggregator` no longer drops `DKGInnerProofReady` events that arrive before `ThresholdSharePending`; it prebuffers them until collection state exists. It also converts `NodeDkgFold` `ComputeRequestError` into `DKGRecursiveAggregationComplete { aggregated_proof: None }` instead of silently discarding actor state, preventing DKG proof aggregation stalls when fold workers fail or events arrive slightly out of order. |
27 changes: 14 additions & 13 deletions agent/flow-trace/04_DKG_AND_COMPUTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,13 @@ Both GenPkShareAndSkSss and GenEsiSss complete
├─ handle_shares_generated():
│ │
│ ├─ 1. For EACH other party j in committee:
│ │ Encrypt sk_sss[j] under party j's BFV public key
│ │ Encrypt esi_sss[*][j] under party j's BFV public key
│ │ → BfvEncryptedShares::encrypt_all()
│ │ → Only party j can decrypt their share
│ ├─ 1. For EACH collected recipient slot in sorted real-party order:
│ │ Map compact recipient slot → real party_id (expelled parties may be absent)
│ │ Encrypt sk_sss[real_party_id] under that party's BFV public key
│ │ Encrypt esi_sss[*][real_party_id] under that party's BFV public key
│ │ Leave the sender's own slot empty (own plaintext rides locally into C4)
│ │ → BfvEncryptedShares::encrypt_all_extended_for_share_indices()
│ │ → Only the mapped real party can decrypt their share
│ │
│ ├─ 2. Build ThresholdShare struct:
│ │ {
Expand Down Expand Up @@ -252,7 +254,7 @@ ProofRequestActor receives ThresholdSharePending
├─ 5. Sign all proofs via sign_and_group_proofs():
│ → Each proof gets its own SignedProofPayload with ECDSA signature
│ → C3a/C3b proofs indexed by (recipient_party_id, row_index)
│ → C3a/C3b proofs indexed by (real recipient_party_id, row_index)
├─ 6. Publish events:
│ ├─ PkGenerationProofSigned { e3_id, party_id, signed_proof(C1) }
Expand Down Expand Up @@ -289,13 +291,12 @@ implements `ZkRequest::NodeDkgFold` (full per-node pipeline to a `NodeFold` proo
`ZkRequest::DecryptionAggregation` (per-ciphertext `C6Fold` + C7 + `DecryptionAggregator`).
`NodeProofAggregator` prebuffers `DKGInnerProofReady` proofs that arrive before
`ThresholdSharePending`, drains those buffered proofs into collection state once
`ThresholdSharePending` arrives, and issues one `NodeDkgFold` request when the
full ordered proof set is available. If that `NodeDkgFold` compute request fails,
it publishes `DKGRecursiveAggregationComplete { aggregated_proof: None }` so the
downstream DKG/public-key aggregation path can terminate deterministically instead
of stalling on missing node-fold output. `PublicKeyAggregator` and
`ThresholdPlaintextAggregator` dispatch the aggregator requests instead of pairwise
folding.
`ThresholdSharePending` arrives, and issues one `NodeDkgFold` request when the full ordered proof
set is available. If that `NodeDkgFold` compute request fails, it publishes
`DKGRecursiveAggregationComplete { aggregated_proof: None }` so the downstream DKG/public-key
aggregation path can terminate deterministically instead of stalling on missing node-fold output.
`PublicKeyAggregator` and `ThresholdPlaintextAggregator` dispatch the aggregator requests instead of
pairwise folding.

### Step 6: Collect All Threshold Shares (with C2/C3 Verification)

Expand Down
25 changes: 18 additions & 7 deletions circuits/bin/recursive_aggregation/node_fold/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,33 @@ fn main(
}
}

// C2 to C3: all (party, modulus) slots must be bound - msg_a[j*L+l] == c2a_shares[j][l]
// and msg_b[j*L+l] == c2b_shares[j][l].
// C2 to C3: encrypted recipient slots must be bound - msg_a[j*L+l] == c2a_shares[j][l]
// and msg_b[j*L+l] == c2b_shares[j][l]. The node's own slot is intentionally not
// C3-encrypted; it is bound through the C4 plaintext/decryption path instead.
// c3ab layout: key-hash prefix, then pk_a, msg_a, ct_a, pk_b, msg_b, ct_b (`C3_SLOTS` each).
// c2ab layout: see `C2AB_PREFIX_LEN` / `C2_PUBLIC_LEN`.
for j in 0..N_PARTIES {
for l in 0..L_THRESHOLD {
let slot = j * L_THRESHOLD + l;
assert(c2ab_public[C2A_SHARES_START + slot] == c3ab_public[C3AB_MSG_A_START + slot]);
assert(c2ab_public[C2B_SHARES_START + slot] == c3ab_public[C3AB_MSG_B_START + slot]);
if party_id != (j as Field) {
let slot = j * L_THRESHOLD + l;
assert(
c2ab_public[C2A_SHARES_START + slot] == c3ab_public[C3AB_MSG_A_START + slot],
);
assert(
c2ab_public[C2B_SHARES_START + slot] == c3ab_public[C3AB_MSG_B_START + slot],
);
}
}
}

let mut c3_pk: [Field; N_PARTIES] = [0; N_PARTIES];
for j in 0..N_PARTIES {
// pk is per-party (same DKG key across all moduli of the same recipient): use slot j*L+0.
c3_pk[j] = c3ab_public[C3AB_PK_A_START + j * L_THRESHOLD];
if party_id == (j as Field) {
c3_pk[j] = c0_pk;
} else {
// pk is per-party (same DKG key across all moduli of the same recipient): use slot j*L+0.
c3_pk[j] = c3ab_public[C3AB_PK_A_START + j * L_THRESHOLD];
}
}

let mut c2a: [[Field; L_THRESHOLD]; N_PARTIES] = [[0; L_THRESHOLD]; N_PARTIES];
Expand Down
19 changes: 14 additions & 5 deletions crates/events/src/enclave_event/compute_request/zk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,20 +166,29 @@ impl ShareEncryptionProofRequest {

/// Request to generate a proof for DKG share decryption (C4a or C4b).
///
/// Proves that a node correctly decrypted H honest parties' BFV-encrypted
/// Shamir shares using its own BFV secret key.
/// Proves that a node correctly decrypted (H − 1) external honest parties' BFV-encrypted
/// Shamir shares using its own BFV secret key, and that its own (un-encrypted) share row
/// matches the C2-bound commitment for its slot. The own slot is supplied as plaintext
/// because parties no longer self-encrypt during DKG.
#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derivative(Debug)]
pub struct DkgShareDecryptionProofRequest {
/// BFV secret key used for decryption (witness — encrypted at rest).
pub sk_bfv: SensitiveBytes,
/// BFV ciphertexts from H honest parties, flattened [H * L] in ascending party_id order.
/// Layout: party 0 mod 0, party 0 mod 1, ..., party 1 mod 0, ...
/// BFV ciphertexts from the (H − 1) external honest parties, flattened
/// `[(H − 1) * L]` in ascending external-party_id order (own party skipped).
/// Layout: ext party 0 mod 0, ext party 0 mod 1, ..., ext party 1 mod 0, ...
pub honest_ciphertexts_raw: Vec<ArcBytes>,
/// Number of honest parties (H).
/// Total number of honest parties (H), counting the own slot.
pub num_honest_parties: usize,
/// Number of CRT moduli (L).
pub num_moduli: usize,
/// Position of the own party within the H ascending-party_id ordering. The prover
/// splices `own_share_raw` into this slot when assembling C4 inputs.
pub own_plaintext_idx: usize,
/// Bincode-serialised `Vec<Vec<u64>>` of shape `[L][N]` — the own party's plaintext
/// share row per modulus (witness — encrypted at rest).
pub own_share_raw: SensitiveBytes,
/// SecretKey or SmudgingNoise.
pub dkg_input_type: DkgInputType,
/// BFV preset for parameter resolution.
Expand Down
Loading
Loading