@@ -82,8 +82,7 @@ Requester calls: Interfold.request({
8282│ │ request-time Interfold, committee registry, bonding registry,
8383│ │ and slashing manager
8484│ ├─ seed = uint256(keccak256(block.prevrandao, e3Id))
85- │ │ → Shared per-E3 ticket-scoring input only; not BFV key material and
86- │ │ not relied upon for cryptographic unpredictability.
85+ │ │ → Shared input for the E3 computation. Committee selection does not use it.
8786│ │
8887│ ├─ encryptionSchemeId = e3Program.validate(
8988│ │ e3Id, seed, e3ProgramParams, computeProviderParams, customParams
@@ -105,11 +104,12 @@ Requester calls: Interfold.request({
105104│ └─ _e3Stages[e3Id] = E3Stage.Requested
106105│
107106├─ COMMITTEE REQUEST:
108- │ ├─ ciphernodeRegistry.requestCommittee(e3Id, seed , threshold)
107+ │ ├─ ciphernodeRegistry.requestCommittee(e3Id, legacySeed , threshold)
109108│ │ │
110109│ │ │ ┌─── CiphernodeRegistryOwnable ──────────────────────┐
111110│ │ │ │ │
112- │ │ │ │ requestCommittee(e3Id, seed, threshold) { │
111+ │ │ │ │ requestCommittee(e3Id, legacySeed, threshold) { │
112+ │ │ │ │ → legacySeed is ignored for ticket sortition │
113113│ │ │ │ 1. require(!committees[e3Id].initialized) │
114114│ │ │ │ 2. Snapshot request-time Interfold, bonding, │
115115│ │ │ │ slashing manager, and fold verifier │
@@ -121,7 +121,8 @@ Requester calls: Interfold.request({
121121│ │ │ │ → Count and submissions use one boundary │
122122│ │ │ │ 4. committees[e3Id] = Committee { │
123123│ │ │ │ initialized: true, │
124- │ │ │ │ seed: seed, │
124+ │ │ │ │ seed: unresolved, │
125+ │ │ │ │ entropyBlock: block.number + 1, │
125126│ │ │ │ requestBlock: block.timestamp, // H-26 │
126127│ │ │ │ committeeDeadline: │
127128│ │ │ │ block.timestamp + sortitionWindow, │
@@ -136,8 +137,9 @@ Requester calls: Interfold.request({
136137│ │ │ │ → Only nodes in tree at request time eligible │
137138│ │ │ │ 7. Emit DkgFoldAttestationContextEstablished( │
138139│ │ │ │ e3Id, registry, foldVerifier) │
139- │ │ │ │ Emit CommitteeRequested(e3Id, seed, threshold,│
140- │ │ │ │ requestBlock, committeeDeadline, │
140+ │ │ │ │ Emit CommitteeRequested(e3Id, entropyBlock, │
141+ │ │ │ │ threshold, requestBlock, │
142+ │ │ │ │ committeeDeadline, │
141143│ │ │ │ ticketPrice) │
142144│ │ │ │ BondingRegistry records this request-time │
143145│ │ │ │ registry as the E3's obligation owner │
@@ -171,6 +173,11 @@ the latest snapshot then replay in order and add any newer E3 contexts.
171173CiphernodeRegistrySolReader decodes DkgFoldAttestationContextEstablished
172174│
173175└─ Stores the E3's request-time registry and verifier for signing, validation, and publication
176+ │
177+ ├─ Decodes CommitteeRequested and waits until entropyBlock is sealed
178+ ├─ Reads the entropy block through the execution RPC and derives
179+ │ keccak256(blockHash, e3Id) without sending a transaction
180+ └─ Publishes CommitteeRequested with the resolved committee seed
174181
175182InterfoldSolReader decodes IInterfold::E3Requested log
176183│
@@ -180,7 +187,7 @@ InterfoldSolReader decodes IInterfold::E3Requested log
180187│
181188├─ Publishes InterfoldEvent::E3Requested {
182189│ e3_id, threshold_m, threshold_n,
183- │ seed , params, error_size, esi_per_ct
190+ │ computation_seed , params, error_size, esi_per_ct
184191│ }
185192│
186193├─ FheExtension.on_event():
@@ -193,7 +200,9 @@ InterfoldSolReader decodes IInterfold::E3Requested log
193200│
194201└─ Sortition actor receives E3Requested:
195202 │
203+ ├─ Waits for CommitteeRequested if the delayed committee seed is not ready
196204 ├─ Loads the request timepoint and frozen ticket price from CommitteeRequested
205+ ├─ Uses the CommitteeRequested seed for ticket ranking
197206 ├─ Calculates buffer = calculate_buffer_size(M, N)
198207 │
199208 ├─ ScoreBackend.get_committee():
@@ -280,22 +289,29 @@ CiphernodeRegistrySolWriter receives TicketGenerated event
280289 │ │ require(ticketNumber >= 1) │
281290 │ │ require(ticketNumber <= availableTickets) │
282291 │ │ │
283- │ │ 7. score = uint256(keccak256( │
292+ │ │ 7. If this is the first ticket, resolve and store: │
293+ │ │ seed = keccak256( │
294+ │ │ blockhash(sortitionEntropyBlocks[e3Id]), e3Id │
295+ │ │ ) │
296+ │ │ → The entropy block is after the paid request │
297+ │ │ → No separate seed transaction is required │
298+ │ │ │
299+ │ │ 8. score = uint256(keccak256( │
284300 │ │ msg.sender, ticketNumber, e3Id, seed │
285301 │ │ )) │
286302 │ │ → SAME formula as Rust-side computation │
287303 │ │ → Both sides agree on scores │
288304 │ │ │
289- │ │ 8 . submitted[msg.sender] = true │
305+ │ │ 9 . submitted[msg.sender] = true │
290306 │ │ scoreOf[msg.sender] = score │
291307 │ │ │
292- │ │ 9 . _insertTopN(e3Id, msg.sender, score): │
308+ │ │ 10 . _insertTopN(e3Id, msg.sender, score): │
293309 │ │ Maintains array of N lowest-scoring nodes: │
294310 │ │ - If < N nodes: just insert │
295311 │ │ - If N nodes: replace highest if new score lower │
296312 │ │ - O(N) linear scan per insertion │
297313 │ │ │
298- │ │ 10 . Emit TicketSubmitted(e3Id, msg.sender, score) │
314+ │ │ 11 . Emit TicketSubmitted(e3Id, msg.sender, score) │
299315 │ │ } │
300316 │ └─────────────────────────────────────────────────────────┘
301317```
@@ -466,7 +482,9 @@ The registry must finalize a ready committee.
466482
4674831 . ** Deterministic sortition** : Both Rust and Solidity compute
468484 ` keccak256(address, ticket, e3Id, seed) ` . The on-chain contract verifies what the off-chain node
469- computed.
485+ computed. The seed comes from the committed next-block hash. The requester cannot inspect it and
486+ revert the request in the same transaction. The first ticket stores the seed, so no separate
487+ randomness transaction is needed.
470488
4714892 . ** Snapshot-based eligibility** : The eligible count, operator eligibility, and ticket balances use
472490 ` requestBlock - 1 ` . The ticket price is frozen in the request transaction. Rust and Solidity
@@ -517,6 +535,20 @@ The registry must finalize a ready committee.
517535
518536## Cluster 7 audit additions (post-fix semantics)
519537
538+ ### Z-05 — request seed grinding
539+
540+ The E3 computation seed is still created during Interfold.request, but it no longer ranks committee
541+ tickets. The registry commits the next block as the entropy block. Rust waits until that block is
542+ sealed and reads the committee seed. The first ticket stores the same seed before it calculates the
543+ score. A requester can revert the request or learn the committee seed, but it cannot do both in one
544+ transaction.
545+
546+ The basic EVM source uses a block hash, so the seed must be resolved while that hash remains in the
547+ chain's history. The contract first uses the EVM's recent-block lookup and then tries the EIP-2935
548+ history contract. Chains without EIP-2935 retain the 256-block limit. This removes requester-side
549+ conditional-revert grinding. It does not claim the stronger proposer-resistance of a verifiable
550+ randomness service.
551+
520552### H-04 — snapshot-based eligibility
521553
522554` CiphernodeRegistryOwnable._validateNodeEligibility ` derives the per-node ticket weight from the
0 commit comments