feat(ethexe-consensus): two-phase compute for instant injected TX promises#5352
feat(ethexe-consensus): two-phase compute for instant injected TX promises#5352
Conversation
…mises Split announce production into two phases so TXs targeting programs created in the current block are validated against post-canonical ProgramStates instead of stale parent-block states. This eliminates the ~12s wait for same-block program TXs. Phase 1: canonical-only compute (no announce metadata writes) returns ephemeral ProgramStates. Phase 2: TX selection against those states, then build and gossip a single announce. Key changes: - New ConsensusEvent::ComputeCanonicalEvents / ComputeEvent::CanonicalEventsComputed - ComputeSubService::compute_canonical_only (assert parent computed, skip DB metadata) - TxValidityChecker::new_with_states + InjectedTxPool::select_for_announce_with_states - Producer: Delay → WaitingCanonicalComputed → ReadyForTxCollection → WaitingAnnounceComputed - accept_announce lenient for state-dependent TX validations (UnknownDestination, UninitializedDestination, InsufficientBalance) to prevent consensus split - Subordinate gossip-reordering fix (defer announces with unknown parent) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ew findings - Add accept_announce leniency tests (UnknownDestination accepted, NonZeroValue rejected) - Add compute_canonical_events test (verifies ProgramStates returned, no DB metadata writes) - Add new_head tests for WaitingCanonicalComputed and ReadyForTxCollection states - Fix poll_next to immediately poll newly created canonical computation future - Document concurrent compute slots and mem::replace placeholder in producer Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The processor's handle_injected_and_events processed injected TXs before
canonical block events. This meant TXs targeting programs created in the
same block would panic at update_state ("failed to find program in known
states") because ProgramCreated hadn't been handled yet.
Swap the order: canonical events first (establishes program state), then
injected TXs (can now reference newly created programs). Required for
two-phase compute where the producer includes TXs for same-block programs.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Changed Files
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a two-phase computation architecture to improve the responsiveness of injected transaction promise delivery. By decoupling canonical event computation from transaction selection, the system can now validate transactions against post-canonical states within the same block. The changes also include critical fixes for event ordering in the processor, improved leniency in announce validation, and robust handling of out-of-order gossip messages. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a two-phase announce production flow in ethexe. It adds support for computing canonical events to generate ephemeral ProgramStates, which are subsequently used to validate and select injected transactions. The Producer state machine is updated to manage this asynchronous process, and transaction validation now accounts for state-dependent conditions that resolve post-canonical execution. Review feedback suggests refactoring duplicated polling logic in the compute service, clarifying test helper documentation, and optimizing performance by passing program states by reference to avoid expensive clones.
| /// Skip the delay timer and complete two-phase flow: | ||
| /// 1. Timer fires → ComputeCanonicalEvents | ||
| /// 2. process_canonical_events_computed → PublishMessage + ComputeAnnounce |
There was a problem hiding this comment.
The comment describing the two-phase flow for the skip_timer test helper is a bit misleading. It oversimplifies the flow and omits the ReadyForTxCollection state and its poll timer. A more accurate description would improve clarity for future maintenance of this test helper.
| /// Skip the delay timer and complete two-phase flow: | |
| /// 1. Timer fires → ComputeCanonicalEvents | |
| /// 2. process_canonical_events_computed → PublishMessage + ComputeAnnounce | |
| /// Skip the initial producer delay and complete the full two-phase announce production flow: | |
| /// 1. produce_announce is triggered, emitting ComputeCanonicalEvents. | |
| /// 2. process_canonical_events_computed is called, transitioning to ReadyForTxCollection with a poll timer. | |
| /// 3. The poll timer fires, triggering build_announce_with_states, which emits PublishMessage and ComputeAnnounce. |
|
@claude opus review |
|
Claude finished @ukint-vs's task in 4m 22s —— View job Review: Two-Phase Compute for Instant Injected TX Promises
SummaryThis PR replaces the proposed depth-2 mini-announce approach (PR #5321) with a simpler two-phase compute model. Phase 1 runs canonical block events through the processor to get fresh The design is clean, well-tested, and significantly simpler than the alternative. The PR adds 571 lines across 13 files with 6 new tests. I found one medium-severity concern and two low-severity observations. Findingsseverity: medium — Subordinate gossip deferral has no replay mechanism within the same block File: When the subordinate receives the producer's announce but the parent isn't included yet, it defers to if !self.ctx.core.db.is_announce_included(verified_announce.data().parent) {
tracing::trace!("Announce parent not yet included, deferring to pending");
self.ctx.pending(verified_announce);
return Ok(self.into()); // stays in WaitingForAnnounce, no retry
}In practice this is still better than the previous behavior (permanent rejection), and the producer will likely re-gossip or the next block will trigger a state transition. But it means the subordinate can miss participating in validation for this block — the window depends on gossip timing. Consider whether an severity: low — File:
let tx_checker = TxValidityChecker::new_with_states(
self.db.clone(),
block,
parent_announce,
program_states.clone(), // could borrow instead
)?;severity: low — TX collection poll timer reuses File: The let mut poll_timer = Timer::new("tx-collection poll", self.ctx.core.producer_delay);This is fine for now if What looks good
|
- compute_canonical_only: replace hard assertion on parent computed with predecessor computation loop. On fast chains, the parent announce may still be computing when the producer starts. Errors would crash the service via the `?` propagation in the event loop. - accept_announce: defer set_injected_transaction until after all acceptance checks pass (touched-programs limit, duplicate inclusion). Prevents a malicious producer from forcing peers to persist junk TX blobs via announces that will be rejected by later checks. - subordinate: inline accept_announce call in process_announce instead of pre-checking is_announce_included. On UnknownParent rejection, defer to pending (gossip reordering). Other rejections handled normally. Cleaner separation: the defer guard now uses the same acceptance path as the happy path. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…essages The previous fix swapped the entire event ordering (canonical before injected), which changed execution priority. Injected TXs should be processed before canonical messages. New ordering: Router events first (ProgramCreated, CodeValidated — registers programs), then injected TXs (preserving priority), then Mirror events (canonical messages). This fixes UnknownDestination panics for same-block programs without deprioritizing injected TXs. Also refactors duplicated canonical poll logic in compute.rs per Gemini review feedback (loop instead of duplicated if-blocks). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Use Cow<ProgramStates> so new_for_announce owns the states (from DB) while new_with_states borrows them (from canonical compute). Avoids cloning the entire BTreeMap during TX selection. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Method is used by tests and accept_announce but not by the producer directly (producer uses select_for_announce_with_states instead). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
`anthropics/claude-code-action@v1` doesn't have a `model` input. Pass `--model claude-opus-4-6` through `claude_args` instead. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Reverts the processor event ordering changes (commits 35a66d6, 49d58e1). Programs only initialize via Ethereum canonical events. Injected TXs targeting same-block programs will correctly fail regardless of registration order because the program isn't initialized until process_queues executes the init message (after handle_injected_and_events). The two-phase compute is still useful for TXs targeting programs that existed before this block but whose state changed (balance top-up, etc.). The processor doesn't need to change for that case. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Problem
When someone sends a transaction to a program that was just created in the same Ethereum block, the validator rejects it: "unknown destination." The program exists on-chain, but the validator validated the TX against the previous block's state, where the program didn't exist yet. The user has to wait for the next block (~12s) before retrying.
Solution: Two-Phase Compute
Instead of validating TXs against stale state, the producer now computes canonical events first to get fresh state, then selects TXs against that fresh state.
Producer State Machine
If a new Ethereum block arrives during any waiting state, the producer drops to Initial. TXs stay in the pool for the next block.
Subordinate Side
Subordinates receive the announce and validate TXs in
accept_announce. But they validate against parent-block state (they haven't run canonical events yet). TXs targeting same-block programs would fail as "unknown destination."Fix:
accept_announceis now lenient for state-dependent conditions (UnknownDestination,UninitializedDestination,InsufficientBalance). These resolve after the subordinate computes the announce. Only structural violations still reject (Outdated,NonZeroValue,NotOnCurrentBranch). TX blobs are persisted to DB only after all acceptance checks pass (touched-programs limit, duplicate inclusion), preventing a malicious producer from forcing peers to store junk TXs.The subordinate's
process_announcenow callsaccept_announcedirectly instead of pre-checking parent inclusion. OnUnknownParentrejection (gossip reordering), the announce is deferred to pending for replay on the next block.Processor Fix
The processor previously handled all events in a flat loop (injected TXs first, then canonical events). This meant TXs targeting same-block programs would panic because the program wasn't registered yet. But simply swapping the entire order would deprioritize injected TXs over canonical messages.
Fix: split into three phases preserving injected TX priority:
Performance
TxValidityCheckerusesCow<ProgramStates>so thenew_for_announcepath owns states from DB whilenew_with_statesborrows them from canonical compute, avoiding aBTreeMapclone during TX selection.What This Replaces
PR #5321 (mini-announces) solved the same problem with depth-2 announce chains: base announce + mini-announce. That required ~1000 lines of changes across CDL counting patches, subordinate
ReadyForMoreAnnouncesstate, gossip reorder guards, and coordinator buffering.Two-phase compute achieves the same result with ~600 lines, no CDL patches, no subordinate state changes, and no coordinator changes. One announce per block, always.
Test plan
injected_prioritized_over_canonical)🤖 Generated with Claude Code