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
24 changes: 14 additions & 10 deletions agent/flow-trace/00_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
7. SORTITION Ciphernodes compute scores, submit tickets on-chain
→ Top N lowest scores selected

8. FINALIZE finalizeCommittee() → committee locked in
8. FINALIZE Committee members schedule staggered finalizeCommittee() calls
→ first successful call locks committee in canonical on-chain order

9. DKG Selected nodes perform distributed key generation:
a. BFV keygen → C0 proof (proves keypair valid)
Expand All @@ -49,23 +50,26 @@
e. Collect shares → verify C2/C3 proofs (2-phase)
f. Decrypt shares → calc decryption key → C4a/C4b proofs
g. Exchange DecryptionKeyShared → verify C4 proofs
h. Publish KeyshareCreated → aggregator
h. Publish KeyshareCreated → all committee members buffer it

10. PK AGG Aggregator aggregates pk_shares → aggregate PK
→ C5 proof (proves aggregation correct)
→ publishCommittee() on-chain → KeyPublished stage
10. PK AGG All committee members buffer keyshares
→ Rust normalizes finalized committee into ascending ticket-score order
→ active aggregator = lowest non-expelled party_id in that normalized order
→ active aggregator aggregates pk_shares → C5 proof
→ permissionless publishCommittee() on-chain → KeyPublished stage

11. COMPUTE Data encrypted with aggregate PK, computation runs
→ Ciphertext output published on-chain

12. DECRYPT Committee members produce decryption shares
→ C6 proof per share (proves share correctly derived)
Broadcast to aggregator
broadcast to all committee members for buffering

13. AGGREGATE Aggregator combines M+1 shares → plaintext
13. AGGREGATE Active aggregator combines M+1 shares → plaintext
→ C7 proof (proves reconstruction correct)

14. COMPLETE publishPlaintextOutput() → rewards distributed
14. COMPLETE Active aggregator permissionlessly calls publishPlaintextOutput()
→ rewards distributed
→ Each active committee member gets fee / N
→ Any escrowed slashed funds split:
nodes (successSlashedNodeBps) + treasury
Expand Down Expand Up @@ -172,7 +176,7 @@ _Found during source-code cross-referencing of these trace documents._
| 4 | `activate()` calls `register()` → `registerOperator()` which has `require(!registered, AlreadyRegistered())`. So activate **reverts** for already-registered operators. It only works for re-registration after deregistration. | BondingRegistry.sol:308 | 01_REGISTRATION |
| 5 | `E3Requested` event is `(uint256 e3Id, E3 e3, IE3Program indexed e3Program)` — seed and params are inside the E3 struct, not separate parameters. | IEnclave.sol:82 | 03_E3_REQUEST |
| 6 | `finalizeCommittee()` checks `>=` deadline, not `>`. | CiphernodeRegistryOwnable.sol | 03_E3_REQUEST |
| 7 | `publishCommittee()` is `onlyOwner` restricted — centralized trust assumption acknowledged in contract TODOs. | CiphernodeRegistryOwnable.sol | 04_DKG |
| 7 | `publishCommittee()` is now permissionless. The effective access control is the C5 proof verification plus the single-publish guard `publicKeyHashes[e3Id] == 0`; the old `onlyOwner` note is obsolete. | CiphernodeRegistryOwnable.sol | 04_DKG |
| 8 | `CommitteePublished` event emits `(e3Id, nodes, publicKey, proof)` — full PK bytes and C5 proof, not just pkHash. | CiphernodeRegistryOwnable.sol | 04_DKG |
| 9 | `_validateNodeEligibility` calls `bondingRegistry.getTicketBalanceAtBlock()` (not `ticketToken.getPastVotes()` directly). | CiphernodeRegistryOwnable.sol:668 | 03_E3_REQUEST |
| 10 | Lane A slashing uses **attestation-based** verification (committee quorum votes), not direct ZK proof re-verification on-chain. `proposeSlash()` decodes voter addresses, agrees, data hashes, and ECDSA signatures — not ZK proofs. | SlashingManager.sol | 05_FAILURE |
Expand All @@ -182,7 +186,7 @@ _Found during source-code cross-referencing of these trace documents._
| # | 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 | **`publishCommittee()` is centralized** | High | Only the contract owner can publish the committee public key. A malicious or compromised owner could publish a fake key. The contract has `// TODO` and `// SECURITY` comments acknowledging this. |
| 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. |
Expand Down
48 changes: 35 additions & 13 deletions agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ EnclaveSolReader decodes IEnclave::E3Requested log
│ └─ Creates Fhe instance from BFV params
│ └─ Stores as dependency in E3Context
├─ PublicKeyAggregatorExtension.on_event(): (aggregator only)
│ └─ Spins up PublicKeyAggregator actor
│ └─ State: Collecting (waiting for N keyshares)
├─ PublicKeyAggregatorExtension.on_event():
│ └─ Spins up the per-E3 public-key aggregation pipeline
│ └─ KeyshareCreatedFilterBuffer buffers until this node becomes the active aggregator
└─ Sortition actor receives E3Requested:
Expand Down Expand Up @@ -171,7 +171,8 @@ CiphernodeSelector receives WithSortitionTicket<E3Requested>
│ ├─ Caches E3Meta { e3_id, threshold_m, threshold_n, seed, ... }
│ ├─ Publishes TicketGenerated {
│ │ e3_id,
│ │ ticket_id: TicketId::Score(ticket_number)
│ │ ticket_id: TicketId::Score(ticket_number),
│ │ party_index: index_in_local_score_ranking
│ │ }
│ └─ This event triggers on-chain ticket submission
Expand Down Expand Up @@ -233,20 +234,26 @@ CiphernodeRegistrySolWriter receives TicketGenerated event

## Step 3: Committee Finalization

### 3a. Deadline Timer (Rust-Side, Aggregator)
### 3a. Deadline Timer (Rust-Side, Committee Members)

```
CommitteeFinalizer actor receives CommitteeRequested event
├─ Stores the request during replay and waits until ALL of:
│ ├─ local TicketGenerated.party_index is known
│ └─ EffectsEnabled has fired
├─ Calculates wait time:
│ wait = committeeDeadline - currentTimestamp + buffer
│ wait = max(committeeDeadline - currentTimestamp, 0)
│ + 1 second
│ + party_index * 5 seconds
├─ Schedules timer
├─ Schedules a staggered timer
├─ When timer fires:
│ └─ Publishes CommitteeFinalizeRequested { e3_id }
└─ On E3Failed / E3StageChanged(Complete|Failed):
└─ On E3Failed / E3RequestComplete / E3StageChanged(Complete|Failed):
└─ Cancels pending timer for this e3_id (if any)
→ Prevents stale finalization attempt after E3 is already terminal
```
Expand Down Expand Up @@ -292,7 +299,7 @@ CiphernodeRegistrySolWriter receives CommitteeFinalizeRequested
│ │ │ │ } │ │
│ │ │ └──────────────────────────────────────────┘ │
│ │ │
│ │ 6. Emit CommitteeFinalized(e3Id, committee, scores)
│ │ 6. Emit CommitteeFinalized(e3Id, committee)
│ │ } │
│ └─────────────────────────────────────────────────────────┘
```
Expand All @@ -309,6 +316,7 @@ CiphernodeRegistrySolReader decodes CommitteeFinalized event
├─ Sortition actor:
│ └─ Stores finalized committee as a `Committee` struct in persistent map
│ → Provides O(1) address→party_id lookup for later expulsion handling
│ → `CommitteeFinalized` is normalized into ascending ticket-score order before storage
├─ CiphernodeSelector:
│ ├─ Checks if this node's address is in the committee list
Expand All @@ -318,12 +326,17 @@ CiphernodeRegistrySolReader decodes CommitteeFinalized event
│ │ e3_id, threshold_m, threshold_n,
│ │ seed, party_id, ...all E3 metadata
│ │ }
│ │ Publishes AggregatorChanged {
│ │ e3_id,
│ │ is_aggregator = (my node has the lowest non-expelled party_id in the
│ │ score-sorted finalized committee)
│ │ }
│ └─ If NO: does nothing for this E3
└─ KeyshareCreatedFilterBuffer:
└─ Stores committee set
└─ Flushes any buffered KeyshareCreated events
└─ Only forwards events from verified committee members
└─ Keeps buffering until AggregatorChanged(is_aggregator=true)
└─ Then flushes buffered KeyshareCreated events from verified committee members
```

---
Expand Down Expand Up @@ -358,8 +371,17 @@ If any deadline is missed → anyone can call markE3Failed()
2. **Snapshot-based eligibility**: Ticket balances are checked at `requestBlock - 1`, preventing
front-running manipulation.

3. **Permissionless finalization**: Anyone can call `finalizeCommittee()` after the deadline — no
3. **Runtime committee order**: the Rust runtime normalizes `CommitteeFinalized` into ascending
ticket-score order before `Sortition` and `CiphernodeSelector` derive `party_id`. That makes
`party_id` in the runtime equivalent to score order, even though the raw on-chain `topNodes`
array is not itself score-sorted.

4. **Active aggregator selection**: `CiphernodeSelector` derives `AggregatorChanged` from the
finalized committee plus enriched `CommitteeMemberExpelled` events. The active aggregator is the
lowest non-expelled `party_id` in the score-sorted runtime committee.

5. **Permissionless finalization**: Anyone can call `finalizeCommittee()` after the deadline — no
single point of failure.

4. **IMT root snapshot**: The Merkle tree root is captured at request time. Nodes that join/leave
6. **IMT root snapshot**: The Merkle tree root is captured at request time. Nodes that join/leave
after the request don't affect this E3's committee.
Loading
Loading