Skip to content

refactor(cketh): make the transaction pipeline generic over its request - #11178

Merged
gregorydemay merged 16 commits into
masterfrom
greg/cketh-lane-generic
Aug 24, 2026
Merged

refactor(cketh): make the transaction pipeline generic over its request#11178
gregorydemay merged 16 commits into
masterfrom
greg/cketh-lane-generic

Conversation

@gregorydemay

@gregorydemay gregorydemay commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Why

The transaction pipeline served exactly one sender, the minter's main address: one nonce sequence, every map keyed by a ckETH ledger burn index, the request type baked in. A second sender address cannot reuse any of it, and giving the sweeper its own address is the point of the stack.

What

Introduces a trait for what a request must answer in order to travel the pipeline, and makes the pipeline generic over it. Withdrawals are the only implementation here, wrapped so that every existing call site and behaviour is unchanged; the sweeper's implementation arrives in #11144.

The pipeline's vocabulary follows the type: it speaks of requests and ids rather than withdrawals, since another pipeline's requests are not withdrawals and its ids are not ledger burn indices. Reimbursement and withdrawal status keep their names — they belong to the wrapper and really are withdrawal concepts.

No behaviour change.

Stack created with GitHub Stacks CLIGive Feedback 💬

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the ckETH minter’s transaction state machine by making TransactionLane generic over a new LaneRequest trait, so the same send/resubmit/finalize machinery can later be reused for a second sender address without coupling it to withdrawal-specific concepts.

Changes:

  • Introduces LaneRequest and makes TransactionLane<R> generic over it (with WithdrawalRequest as the current implementation).
  • Moves transaction construction from a free function (create_transaction) into LaneRequest::to_transaction, and updates all call sites accordingly.
  • Renames lane-facing APIs/vocabulary from “withdrawal” to generic “request” (e.g., record_request, requests_batch, oldest_incomplete_request_timestamp) while keeping withdrawal/reimbursement concepts on EthTransactions.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
rs/ethereum/cketh/minter/src/withdraw.rs Updates batching flow to use generic request APIs and LaneRequest::to_transaction.
rs/ethereum/cketh/minter/src/state/transactions/tests.rs Updates tests for renamed APIs and moves the “accepted withdrawal request event” helper into tests.
rs/ethereum/cketh/minter/src/state/transactions/mod.rs Introduces LaneRequest, makes TransactionLane generic, migrates transaction creation into to_transaction, and updates request-centric terminology.
rs/ethereum/cketh/minter/src/state/tests.rs Updates state tests to construct transactions via to_transaction.
rs/ethereum/cketh/minter/src/state/audit.rs Updates audit transition handling to call record_request.
rs/ethereum/cketh/minter/src/state.rs Updates state methods to use renamed request accessors (record_request, get_processed_request).
rs/ethereum/cketh/minter/src/main.rs Updates metric to use oldest_incomplete_request_timestamp.
rs/ethereum/cketh/minter/src/guard/tests.rs Updates guard tests to call record_request.
rs/ethereum/cketh/minter/src/guard/mod.rs Updates pending-request count to use requests_len.
rs/ethereum/cketh/minter/src/dashboard/tests.rs Replaces removed event-conversion method with a local helper and uses to_transaction.
rs/ethereum/cketh/minter/src/dashboard.rs Updates dashboard to iterate pending requests via requests_iter.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

rs/ethereum/cketh/minter/src/state/transactions/mod.rs:1354

  • remove_request removes by full Eq (r != request), but reschedule_request and the lane invariants are keyed by id(). If a caller passes a request value with the same id() but different non-id fields, reschedule_request will assert there is exactly one matching id, but remove_request won't remove it (and record_request will then panic on duplicate id). Removing by id() makes the API consistent and robust for non-withdrawal lane types.
    fn remove_request(&mut self, request: &R) {
        self.pending_requests.retain(|r| r != request);

rs/ethereum/cketh/minter/src/state/transactions/mod.rs:599

  • This doc comment says the timestamp covers requests “awaiting a transaction or a reimbursement”, but the implementation only considers pending requests and maybe_reimburse (created-but-not-finalized). It does not include requests with a pending reimbursement_requests entry. Either adjust the wording to match the current behavior, or extend the calculation to include reimbursement backlog.
    /// Whether any request is still in flight, either awaiting a transaction or a reimbursement.

Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from db25106 to 00d183e Compare August 18, 2026 11:39
@gregorydemay gregorydemay changed the title refactor(cketh): make the transaction lane generic over its request refactor(cketh): make the transaction pipeline generic over its request Aug 18, 2026
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from 00d183e to c69e4ce Compare August 18, 2026 13:06
@gregorydemay
gregorydemay changed the base branch from greg/cketh-lane-tidy to greg/cketh-extract-pipeline August 18, 2026 13:08
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from c69e4ce to 3c390a4 Compare August 18, 2026 13:41
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from 3c390a4 to 6a2a1c8 Compare August 18, 2026 14:03
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from 6a2a1c8 to 6be91c0 Compare August 18, 2026 14:28
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from 6be91c0 to b659e58 Compare August 18, 2026 15:06
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from b659e58 to 836ce92 Compare August 19, 2026 08:18
Base automatically changed from greg/cketh-extract-pipeline to master August 20, 2026 08:11
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from 836ce92 to d483cb8 Compare August 20, 2026 08:45
Comment thread rs/ethereum/cketh/minter/src/state/tests.rs
@gregorydemay
gregorydemay marked this pull request as ready for review August 20, 2026 12:20
@gregorydemay
gregorydemay requested a review from a team as a code owner August 20, 2026 12:20
@github-actions github-actions Bot added the @defi label Aug 20, 2026
@zeropath-ai

zeropath-ai Bot commented Aug 20, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to daafc74.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/ethereum/cketh/minter/src/dashboard.rs
    Change withdrawal_requests_iter to .requests_iter; update related usage
► rs/ethereum/cketh/minter/src/dashboard.rs
    Change withdrawal_requests_iter to .requests_iter; update usage in tests
Bug Fix ► rs/ethereum/cketh/minter/src/guard/mod.rs
    Rename withdrawal_requests_len to requests_len and use .requests_len in implementation
Enhancement ► rs/ethereum/cketh/minter/src/guard/tests.rs
    Update to use .record_request instead of .record_withdrawal_request
► rs/ethereum/cketh/minter/src/guard/tests.rs
    Change withdrawal request construction to .record_request
Enhancement ► rs/ethereum/cketh/minter/src/main.rs
    Rename oldest_incomplete_withdrawal_timestamp to oldest_incomplete_request_timestamp
Bug Fix ► rs/ethereum/cketh/minter/src/state.rs
    Replace .record_withdrawal_request with .record_request; replace get_processed_withdrawal_request with .get_processed_request
Enhancement ► rs/ethereum/cketh/minter/src/state/audit.rs
    Replace .record_withdrawal_request with .record_request in AcceptedEthWithdrawalRequest and AcceptedSweeperFundingRequest branches
Enhancement ► rs/ethereum/cketh/minter/src/state/tests.rs
    Rename pending_withdrawal_requests to pending_requests and processed_withdrawal_requests to processed_requests; update related calls and constructors; switch to WithdrawalRequests-based terminology
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/mod.rs
    Introduce PipelineRequest module and generic TransactionPipeline over R; expose PipelineRequest-related types; adjust withdrawal pipeline to use generic R; add MinterTransactionPipeline alias
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/mod.rs
    Rename into_accepted_withdrawal_request_event to accepted_withdrawal_request_event helper; adjust transaction creation flow to use R::Id and Into
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/mod.rs
    Generalize withdrawal pipeline to use Id type instead of LedgerBurnIndex; update methods accordingly (record_request, reschedule_request, etc.)
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/mod.rs
    Update type signatures for transactions storage (created_tx, sent_tx, finalized_tx) to key by R::Id; adjust iterators and accessors
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/mod.rs
    Update naming: withdrawal_requests_batch -> requests_batch; withdrawal_requests_iter -> requests_iter; withdrawal_requests_len -> requests_len; transactions_to_sign_iter/sign_batch adjusted to new IDs
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/mod.rs
    Adjust finalization, processing, and retrieval methods to operate on R::Id instead of LedgerBurnIndex

gregorydemay and others added 12 commits August 21, 2026 11:04
`TransactionPipeline` served exactly one sender: the minter's main address. One
nonce, every map keyed by a ckETH `LedgerBurnIndex`, and the request type baked
in. A second sender address cannot reuse any of it.

Introduce `PipelineRequest` — an identity usable as the pipeline's alternate map key, a
destination, a creation time, a fee-bump strategy and the EIP-1559 transaction
the request turns into — and make `TransactionPipeline<R>` generic over it.
`WithdrawalRequest` is the only implementation, so `WithdrawalTransactions` now wraps
`TransactionPipeline<WithdrawalRequest>` and every existing call site and behaviour
is unchanged.

With the request type behind a trait, the pipeline's vocabulary follows: it speaks
of requests and ids rather than withdrawals, since a future pipeline's requests are
not withdrawals and its ids are not ledger burn indices. Reimbursement and
withdrawal status keep their names — they live on `WithdrawalTransactions` and really
are withdrawal concepts.

Two things fall out. `create_transaction` was a five-argument free function
reaching into a request to build its transaction; that is now
`PipelineRequest::to_transaction`, so the function goes. And
`into_accepted_withdrawal_request_event` had no production caller left, so it
moves to the test file that was its only consumer — `EventType` no longer
appears anywhere in `state::transactions`.

Preparatory, with no second pipeline yet: nothing instantiates `TransactionPipeline`
with anything but `WithdrawalRequest`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#11190 narrowed `TransactionStage` to `pub(in crate::state)`, which matched a
`TransactionPipeline` that was equally narrow. Making the pipeline generic also
makes it `pub`, so a `pub` method returning the narrower type is a
private-interface warning, and CI denies warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PipelineRequest::to_transaction` returned `Result<_, CreateTransactionError>`,
whose only variant carries a `cketh_ledger_burn_index: LedgerBurnIndex`. A
request type that burns no ckETH cannot construct that value, so the trait
promised a failure such an implementor has no way to represent — and its driver
would have to handle an `Err` that can never arrive.

The error becomes an associated type. `WithdrawalRequest` sets it to
`CreateTransactionError` and is unchanged; a request that funds its own fee can
set it to `Infallible`, which turns the unreachable arm into an uninhabited one
the compiler discharges (`Err(never) => match never {}`). Both drivers call the
method on a concrete request type, so nothing needs a new bound.

Rename `to_transaction` to `create_transaction`: `to_*` announces a cheap
borrowing conversion, but this is fallible and takes four further arguments. The
new name also matches `CreateTransactionError`, `record_created_transaction` and
`EventType::CreatedTransaction`.

Also cleaned up: dropped the doc comment orphaned by the removal of the
`create_transaction` free function, which had come to document
`ERC_20_TRANSFER_FUNCTION_SELECTOR`; its `# Errors` section now sits on the trait
method and its fee-payer note on the withdrawal implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the generalization: the methods were renamed, their parameters and
locals were not, so generic code still said `burn_index`, `ledger_burn_index` and
`withdrawal_id` — including in two public signatures, and in a `panic!` whose own
message already said "id".

- Rename those 26 lines inside `impl<R: PipelineRequest> TransactionPipeline<R>`,
  and the comment that still spoke of a withdrawal request. The withdrawal
  vocabulary stays wherever it is accurate: the `WithdrawalRequest` trait impl,
  and `WithdrawalTransactions`.
- Rename the test builder's `with_pending_withdrawal_requests` and
  `with_processed_withdrawal_requests`, whose own fields were already
  `pending_requests`/`processed_requests`.
- Drop `ResubmitTransactionError`'s unused `Id = LedgerBurnIndex` default, which
  let the bare name keep meaning the withdrawal one.
- Give `record_reimbursement_request` a doc of its own: it carried a verbatim copy
  of `record_finalized_transaction`'s, describing a `receipt` parameter it does not
  have and a finalization it does not perform. Move it next to the other
  reimbursement bookkeeping, leaving a second `impl` block that really is only the
  withdrawal-status queries, and say so.
- Move `accepted_withdrawal_request_event` into `mod eth_balance`, its only caller,
  where `WithdrawalRequest` and `EventType` are already in scope, so the helper
  needs neither a fully-qualified parameter type nor an inner `use`.

No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`transactions/mod.rs` was 1562 lines covering six concerns. Move the smallest
self-contained one out: `PipelineRequest` — what a request must answer to travel a
`TransactionPipeline` — together with the minter's implementation of it for
`WithdrawalRequest`, which is most of the bulk (the two `create_transaction` arms).

`mod.rs` drops to 1384 lines and loses three imports that existed only for the
transaction-building code (`EthereumNetwork`, `GasAmount`, `ResubmissionStrategy`).
The trait is re-exported, so no caller outside the module changes.

One test was reaching `EthereumNetwork` through `state::transactions`, which worked
only because a child module can see its parent's private `use`. It now imports from
`crate::lifecycle`, where the type actually lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Clone + Eq + fmt::Debug` were supertraits of `PipelineRequest`, so every present
and future request type owed them whether or not it ever met the code that needs
them. None of the three is part of the contract a request answers: they are what
`TransactionPipeline` needs to hand out owned requests (`requests_batch`,
`record_created_transaction`), to remove one by value (`retain(|r| r != request)`)
and to format one in an assertion.

Move them onto the impl block that uses them, leaving `PipelineRequest` to say only
what a request must answer. Purely a compile-time change: the recompiled test
binaries were bit-identical, so Bazel replayed the cached results.

`Eq` rather than `PartialEq`, though only `PartialEq` is needed to compile:
`remove_request` deletes the queue entry equal to a given request, which is correct
only if equality is reflexive.

The `Id` bounds stay on the associated type. They are not the same case — moving
them needs a `where` clause repeated on the impl and defeats the struct's derives,
which cannot prove `MultiKeyMap<_, R::Id, _>: Clone` from a bound on `R` alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Id` required `Display`, and four panic messages in the generic pipeline used it.
Those messages are shared by every sender address, so the id alone does not say
which pipeline trapped — and a bare number in a canister's trap message is exactly
where that matters.

Use `Debug` instead and drop the `Display` bound. `LedgerBurnIndex` is unaffected
(`phantom_newtype::Id` renders both through the inner `u64`), while a request type
whose id is an ordinary newtype now reports `SweepId(42)` rather than `42`, naming
the pipeline for free.

`Debug` was already required, for the `assert_eq!`s over id-keyed maps, so this
removes an obligation rather than trading one for another.

Also: the "duplicate transaction hash" assertion still said "burn indices" for what
are now ids of either kind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`record_created_transaction` cloned the pending request, checked the clone, then
deleted the original by value equality. Find its position instead: the asserts read
it in place, and `VecDeque::remove` hands over the very entry that was found.

The removal still happens after the asserts, not at the top. Two tests call this
method twice on one pipeline, each expecting its own panic, which only works while a
rejected transaction leaves the request pending.

`reschedule_request` gets the same treatment, which fixes a latent trap: it removed
by equality against the request the *caller* passed, so a caller whose copy had
drifted from the queued one would have removed nothing and then panicked on a
duplicate id from `record_request`. It now re-enqueues the entry it took out.

That leaves `remove_request` without callers, and no code that identifies a request
by anything but its id. `Eq` is still needed, but only to compare requests in
assertions and in `is_equivalent_to` — no longer to decide which queue entry to drop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checking the created transaction against the request is what
`assert_created_transaction` is for, and the destination is one of those checks.
It sat in the pipeline instead, which now asserts only the nonce — the one part of
a created transaction the pipeline, not the request, decides.

`PipelineRequest::destination` had no other caller, so the trait loses an obligation
too: the pipeline never needed to know where a request sends its funds, only that
the transaction agrees with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The second `impl WithdrawalTransactions` came in with the generic pipeline, with a
doc claiming it held the reimbursement and withdrawal-status behaviour — while eight
of the nine reimbursement methods stayed in the first block. Repairing that boundary
earlier only narrowed the claim; the split itself buys nothing. Both blocks are plain
inherent impls on the same type in the same file, so the second header was a section
comment with an `impl` attached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generic-pipeline commit hoisted five reimbursement methods above the delegating
ones. Nothing about the change needed that, and it made them read as deleted and
re-added, in a commit that should read as being about generics.

Restore master's order. `mod.rs` now differs from master by 532 changed lines rather
than 689, all of them real.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… methods

The previous commit reordered the methods by rejoining their bodies, and joined them
with a single newline instead of two, so all 35 separating blank lines were lost.

Nothing downstream caught it: rustfmt preserves blank lines between items but never
adds them, and the check that the reorder was a pure permutation compared method
bodies with surrounding whitespace stripped — precisely the thing that had changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregorydemay
gregorydemay force-pushed the greg/cketh-lane-generic branch from 5cea49a to 641c22f Compare August 21, 2026 09:13

@mbjorkqvist mbjorkqvist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gregorydemay, just a few, mainly minor comments.

Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/state/transactions/request.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/state/transactions/mod.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/state/transactions/request.rs Outdated
gregorydemay and others added 4 commits August 24, 2026 08:53
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The inherent WithdrawalRequest::created_at shadows it at every call
site, so no request ever answers it through the trait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The method only ever consumed the request to read its id, and
rescheduled whatever sat in the queue under that id, so a caller
passing a modified request had its modifications silently dropped.
Every caller already holds the id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregorydemay
gregorydemay added this pull request to the merge queue Aug 24, 2026
Merged via the queue into master with commit 948f633 Aug 24, 2026
40 checks passed
@gregorydemay
gregorydemay deleted the greg/cketh-lane-generic branch August 24, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants