refactor(cketh): extract TransactionPipeline from WithdrawalTransactions - #11190
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors the ckETH minter’s withdrawal-transaction state by extracting the generic “send machinery” into a dedicated TransactionPipeline, leaving WithdrawalTransactions responsible for reimbursement bookkeeping and delegating transaction lifecycle operations to the pipeline.
Changes:
- Extracts transaction queue/nonce/maps logic into
TransactionPipelineand wraps it insideWithdrawalTransactions. - Splits finalization so the pipeline finalizes the on-chain transaction, while
WithdrawalTransactionshandles reimbursement side-effects on failure. - Updates unit/state tests to construct and assert against the new pipeline-containing structure.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
rs/ethereum/cketh/minter/src/state/transactions/mod.rs |
Introduces TransactionPipeline, moves core transaction lifecycle logic into it, and re-implements WithdrawalTransactions as a wrapper with reimbursement bookkeeping. |
rs/ethereum/cketh/minter/src/state/transactions/tests.rs |
Updates transaction tests to account for the new WithdrawalTransactions { pipeline, ... } structure and adds new_for_test for TransactionPipeline. |
rs/ethereum/cketh/minter/src/state/tests.rs |
Updates state equivalence tests to construct WithdrawalTransactions using TransactionPipeline::new_for_test(...). |
Suppressed comments (1)
rs/ethereum/cketh/minter/src/state/transactions/mod.rs:1216
WithdrawalTransactions::transaction_statusdirectly accessesTransactionPipeline’s privatepending_withdrawal_requests. Using the existingwithdrawal_requests_iter()accessor avoids couplingWithdrawalTransactionsto pipeline internals.
if self
.pipeline
.pending_withdrawal_requests
.iter()
.any(|r| &r.cketh_ledger_burn_index() == burn_index)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
dceea28 to
7bb9b9c
Compare
3330f0d to
340c421
Compare
340c421 to
d3cfa0b
Compare
…chine `WithdrawalTransactions` did two jobs: it drove transactions through create → sign → send → resubmit → finalize on the minter's nonce sequence, and it tracked which withdrawals still owe the user a refund. The two are only coupled at two moments — creating a transaction arms the refund, finalizing it disarms it — but everything else about sending a transaction is indifferent to whether anyone gets paid back. Split them: `TransactionPipeline` is now the send machinery alone, holding the request queues, the three transaction maps and the nonce. `WithdrawalTransactions` owns a pipeline plus the reimbursement bookkeeping, forwards the pipeline's API so callers are unchanged, and intercepts exactly those two moments so the invariant lives in one place. Reimbursement records and withdrawal status move with it. `record_finalized_transaction` splits accordingly: the pipeline exposes `finalize_transaction` for the mechanics, and the refund tail stays outside. Preparatory: the sweeper address will need the same send machinery on its own nonce sequence, and it burns no ckETH, so it must be able to reuse the pipeline without inheriting a refund path it can never use. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up. Two things the split left behind: The pipeline's doc comment still promised to reimburse the user on a failed transaction, which is precisely the responsibility that moved out. And `WithdrawalTransactions` was still reading the pipeline's fields directly. Same module, so it compiles, but it makes the wrapper depend on how the pipeline stores things rather than on what it does — and the point of the split is that a second sender address can reuse the machinery. Every access now goes through the pipeline's own API; the three lookups that had no accessor get one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d3cfa0b to
3f460ed
Compare
|
🤖 Reading guide — the diff in I tried reordering the file to make it align method-by-method — both putting the wrapper first and reordering its methods into master's order. Neither helps (11 hunks → 10, the two large hunks remain), so the file keeps the order that reads best rather than one contorted for the diff. Instead, here is what the 41 methods actually are:
The four worth your attention, which are exactly what the split is about:
Those last two are the only places where the pipeline and the refund bookkeeping must stay in step, which is why they intercept rather than delegate. Both are now guarded by To collapse the relocated code while reading: Pink and blue are removals that reappear elsewhere, cyan and yellow their arrivals; skipping all four leaves only what genuinely changed. |
|
✅ No security or compliance issues detected. Reviewed everything up to 0cd82ae. Security Overview
Detected Code Changes
|
mbjorkqvist
left a comment
There was a problem hiding this comment.
Thanks @gregorydemay! Just a few minor comments.
Restores two things the extraction dropped against master: the effective transaction fee and its explanation in the funding-failure log, without which the log no longer says how much gas the failed transaction paid; and the comment recording why the `PendingReimbursement` branch is unreachable for a funding, and what adopting a status of its own would cost. `maybe_reimburse_requests_iter` was the one place left reading a private field of `TransactionPipeline` rather than calling it, which is the encapsulation this extraction exists to create; it now goes through `get_processed_withdrawal_request`. `TransactionStage` is only reachable via `TransactionPipeline`, so it drops to `pub(in crate::state)` too. The tests reached into `sent_tx`, `created_tx`, `finalized_tx` and `next_nonce` in eight places; they now use the accessors, with `sent_transactions` and `first_sent_transaction` covering what no single accessor returns. `create_and_record_transaction` returns the transaction it recorded instead of reading it back. Bullets 5 and 6 of the lifecycle doc restated the same clause; they are now one step. A request is no longer necessarily a user's, since `SweeperFunding` is a `WithdrawalRequest`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rs/ethereum/cketh/minter/src/state/transactions/tests.rs:3082
- In
sent_transactions, thefindpredicate compares a dereferencedLedgerBurnIndex(*index) to a reference (burn_index).LedgerBurnIndexis anId<_, u64>newtype and only implementsPartialEqwith itself, so*index == burn_indexis likely a type mismatch. Comparing the references directly avoids the issue and is clearer.
transactions
.sent_transactions_iter()
.find(|(_nonce, index, _txs)| *index == burn_index)
.map(|(_nonce, _index, txs)| txs)
Why
WithdrawalTransactionsdoes two jobs. It drives transactions through create → sign → send → resubmit → finalize on the minter's nonce sequence, and it tracks which withdrawals still owe the user a refund. The two meet at only two moments — creating a transaction arms the refund, finalizing it disarms it — and everything else about sending a transaction is indifferent to whether anyone gets paid back.The minter's sweeper address will need that send machinery on a nonce sequence of its own. It burns no ckETH, so it must be able to reuse the machinery without inheriting a refund path it can never use.
What
TransactionPipelinebecomes the send machinery alone — the request queues, the three transaction maps and the nonce.WithdrawalTransactionsowns a pipeline plus the reimbursement bookkeeping, forwards the pipeline's API so callers are unchanged, and intercepts those two moments so the invariant lives in one place. Reimbursement records and withdrawal status move with it.Two things follow from the split:
record_finalized_transactionexists on both types: the pipeline's performs the finalize mechanics and hands back the finalized transaction; the wrapper's uses that to decide the refund. It keeps the same name on both, asrecord_created_transactionalready did.transaction_stagereports where a transaction sits — created, sent or finalized — returningOption, so absence is the type's business rather than a variant. The wrapper maps that toRetrieveEthStatus, consulting reimbursement only once finalized.RetrieveEthStatuscannot move to the pipeline: its finalized payload isSuccess | PendingReimbursement | Reimbursed, which is reimbursement vocabulary throughout.state::testsis untouched: it builds its fixture through a builder, so the new nesting is absorbed inbuild()and no assertion has to be restated.