Skip to content

feat(cketh): drive the sweeper transaction pipeline from its own timer task - #11237

Merged
gregorydemay merged 7 commits into
masterfrom
greg/DEFI-2926-sweeper-send-task
Aug 25, 2026
Merged

feat(cketh): drive the sweeper transaction pipeline from its own timer task#11237
gregorydemay merged 7 commits into
masterfrom
greg/DEFI-2926-sweeper-send-task

Conversation

@gregorydemay

@gregorydemay gregorydemay commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Why

  • The sweeper address has its own transaction pipeline, but nothing moves requests through it.
  • This is the task that does, on the sweeper's own nonce sequence.

What

  • sweep::process_sweeper_transactions runs the sweeper pipeline through the same create → sign → send → resubmit → finalize cycle as withdrawals.
  • It signs with the sweeper derivation path [3] and reads the sweeper address' own transaction count.
  • It takes its own TaskType::SweeperSend timer guard, so a slow sweep round neither blocks the withdrawal task nor is blocked by it — the point of a separate nonce sequence.
  • A sweep whose prepaid gas cannot cover the current fee is logged and moved back to the end of the queue, as the withdrawal task does, rather than priced at something that could not be mined.
  • The transaction type stays the request's business: a sweep that must still install delegations is sent as EIP-7702, any other as EIP-1559.

Prerequisite commit

  • Four RPC helpers in withdraw were implicitly about the main address, calling minter_address() themselves or keying receipts by LedgerBurnIndex.
  • Reading a transaction count, broadcasting signed transactions and fetching finalized receipts are specific to neither a sender nor what a request is keyed by, so the sender becomes an argument and the receipt fetch becomes generic over the pipeline id.
  • Behaviour is unchanged: the receipt loop moves into fetch_finalized_receipts, which returns None where the loop used to return early.

Scope

  • Nothing enqueues a SweepRequest yet, so the task early-returns on an empty pipeline and is inert in production.
  • Still to come: the sweep-queue→SweepRequest source, and gating on prepaid sweep gas.

Note for reviewers

  • The sweep driver and the withdrawal driver are now near-identical five-stage loops over different request types, down to a private trio of batch-size constants that are 5 in both files.
  • Deduplicating them is the obvious follow-up now that the pipeline is generic, but it would bury this change, so it is left for its own PR.

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

Adds an independent timer-driven transaction pipeline for the ckETH sweeper address.

Changes:

  • Adds the sweeper create/sign/send/resubmit/finalize driver.
  • Generalizes withdrawal RPC helpers by sender and pipeline ID.
  • Registers a dedicated timer guard and periodic task.

Reviewed changes

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

Show a summary per file
File Description
withdraw.rs Generalizes shared RPC helpers.
sweep.rs Implements the sweeper pipeline driver.
state/transactions/tests.rs Reuses the sweeper gas-limit constant.
state.rs Adds sweeper address access and task type.
main.rs Registers the sweeper timer.
lib.rs Exports the sweep module.
deposit_address/mod.rs Exposes the sweeper derivation path internally.

💡 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/sweep.rs
Comment thread rs/ethereum/cketh/minter/src/sweep.rs
@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch 2 times, most recently from a8b30dc to 02f02c0 Compare August 21, 2026 10:57
@gregorydemay
gregorydemay changed the base branch from greg/sweeper-send-lane to greg/sweeper-eip7702 August 21, 2026 10:57
@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch from 02f02c0 to 73c453c Compare August 21, 2026 11:12
@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch from 73c453c to 68f9246 Compare August 21, 2026 14:44
@gregorydemay
gregorydemay changed the base branch from greg/sweeper-eip7702 to master August 24, 2026 13:02
@gregorydemay
gregorydemay changed the base branch from master to greg/sweeper-eip7702 August 24, 2026 13:02
pull Bot pushed a commit to bit-cook/ic that referenced this pull request Aug 24, 2026
…finity#11144)

## Why

The minter's dedicated sweeper address needs to send Ethereum
transactions, and it must not share the main address' nonce sequence: a
sweep stuck behind a fee-starved transaction would head-of-line-block
every user withdrawal.

## What

A second instance of the transaction pipeline, for the sweeper address,
on a nonce sequence of its own. A sweep burns no ckETH, so it is keyed
by a plain counter rather than a ledger burn index, and is never
reimbursed; it pays gas from the sweeper's prepaid balance, so it has no
transaction fee it can fail to cover. Five audit events record the
pipeline's transitions, with reconstruction, Candid mirrors and `.did`.

The sweeper's start nonce can be set from both lifecycle arguments. It
is optional on install where the main address' equivalent is required,
because install arguments are replayed from the event log and a required
field would be missing from every event already written.

## Scope

The pipeline only. No timer drives it and nothing enqueues a sweep, so
the new state stays empty: the sending task is
[DEFI-2926](https://dfinity.atlassian.net/browse/DEFI-2926) in dfinity#11237,
and EIP-7702 first-time delegation is dfinity#11250, stacked above. The
sweep-queue source and prepaid-gas gating are still to come.

Sweeper *funding* — burning ckETH from the minter's fee subaccount to
prepay that gas — is a separate stack (DEFI-2933) that meets this one
only in the withdrawal pipeline, where its request variant has already
landed. Hence the audit events here are numbered from `n28`, above the
funding event master now owns.

## Candid compatibility

`CI_OVERRIDE_DIDC_CHECK` is set. The check flags the five cases this PR
adds to the `Event` payload variant, since a variant returned to callers
may not grow under Candid subtyping. It is the additive shape every new
audit event has taken: existing cases keep their names and fields, and
callers matching exhaustively on the old set see the new ones only for
sweeper activity, which nothing enqueues yet. The new optional nonce
field on the two argument types is compatible on its own.

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>


[DEFI-2917]:
https://dfinity.atlassian.net/browse/DEFI-2917?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


[DEFI-2926]:
https://dfinity.atlassian.net/browse/DEFI-2926?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch from 68f9246 to 880dee7 Compare August 25, 2026 07:04
Comment thread rs/ethereum/cketh/minter/src/main.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/withdraw.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/sweep.rs
Comment thread rs/ethereum/cketh/minter/src/sweep.rs Outdated
@gregorydemay
gregorydemay marked this pull request as ready for review August 25, 2026 07:59
@gregorydemay
gregorydemay requested a review from a team as a code owner August 25, 2026 07:59
@zeropath-ai

zeropath-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 3f8f452.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/ethereum/cketh/minter/src/deposit_address/mod.rs
    pub(crate) sweeper_derivation_path() -> Vec
Enhancement ► rs/ethereum/cketh/minter/src/lib.rs
    Add sweep module to pub mod sweep;
    Add PROCESS_SWEEPER_TRANSACTIONS_INTERVAL and PROCESS_SWEEPER_TRANSACTIONS_RETRY_INTERVAL constants
Enhancement ► rs/ethereum/cketh/minter/src/main.rs
    Import sweep::process_sweeper_transactions;
    Use PROCESS_SWEEPER_TRANSACTIONS_INTERVAL;
    Update timer setup to invoke sweeper processing
Enhancement ► rs/ethereum/cketh/minter/src/state.rs
    Document sweeper_address();
    Add SweeperSend to TaskType enum
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/tests.rs
    Update tests to reference SWEEP_TRANSACTION_GAS_LIMIT;
    Adjust related gas limit usage and transaction cost calculations
Enhancement ► rs/ethereum/cketh/minter/src/sweep.rs (new file)
    Introduce sweeper transaction processing logic;
    Functions for batching, signing, sending, and finalizing sweeper transactions;
    Derivation path usage for sweeper;
    Retry scheduling and state mutation on events
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    Update imports to include DerivationPath, Address, and SignableTransaction types;
    Extend to support sweeper-related transactions and signing flow;
    Introduce send_signed_transactions helper and adjust finalization flow for sweeper
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    Modify process_retrieve_eth_requests to use sweeper sender and updated latest_transaction_count logic
    Expose latest_transaction_count(sender) and associated error handling
    Refactor send and finalize transactions to pass sender and latest_transaction_count
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    Add fetch_finalized_receipts function (generalized for prepared receipts)

@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch from 4aff994 to 5de5c17 Compare August 25, 2026 09:59
An error occurred while trying to automatically change base from greg/sweeper-eip7702 to master August 25, 2026 10:14
@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch from 5de5c17 to b499dd5 Compare August 25, 2026 11:21
@gregorydemay
gregorydemay requested review from a team as code owners August 25, 2026 11:21

@github-actions github-actions Bot 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.

This pull request changes code owned by the Governance team. Therefore, make sure that
you have considered the following (for Governance-owned code):

  1. Update unreleased_changelog.md (if there are behavior changes, even if they are
    non-breaking).

  2. Are there BREAKING changes?

  3. Is a data migration needed?

  4. Security review?

How to Satisfy This Automatic Review

  1. Go to the bottom of the pull request page.

  2. Look for where it says this bot is requesting changes.

  3. Click the three dots to the right.

  4. Select "Dismiss review".

  5. In the text entry box, respond to each of the numbered items in the previous
    section, declare one of the following:

  • Done.

  • $REASON_WHY_NO_NEED. E.g. for unreleased_changelog.md, "No
    canister behavior changes.", or for item 2, "Existing APIs
    behave as before.".

Brief Guide to "Externally Visible" Changes

"Externally visible behavior change" is very often due to some NEW canister API.

Changes to EXISTING APIs are more likely to be "breaking".

If these changes are breaking, make sure that clients know how to migrate, how to
maintain their continuity of operations.

If your changes are behind a feature flag, then, do NOT add entrie(s) to
unreleased_changelog.md in this PR! But rather, add entrie(s) later, in the PR
that enables these changes in production.

Reference(s)

For a more comprehensive checklist, see here.

GOVERNANCE_CHECKLIST_REMINDER_DEDUP

@zeropath-ai

zeropath-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

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

Security Overview
Detected Code Changes

The diff is too large to display a summary of code changes.

@gregorydemay
gregorydemay changed the base branch from greg/sweeper-eip7702 to master August 25, 2026 11:21
@zeropath-ai

zeropath-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

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

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/ethereum/cketh/minter/src/deposit_address/mod.rs
    Make sweeper_derivation_path public(crate)
Enhancement ► rs/ethereum/cketh/minter/src/lib.rs
    Add sweep module and new timer intervals for sweeper processing
Enhancement ► rs/ethereum/cketh/minter/src/main.rs
    Import sweeper processing and reference new PROCESS_SWEEPER_TRANSACTIONS_INTERVAL
Enhancement ► rs/ethereum/cketh/minter/src/state.rs
    Document sweeper_address and add SweeperSend to TaskType
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/tests.rs
    Adapt tests to new SWEEP_TRANSACTION_GAS_LIMIT constant and related usage
Enhancement ► rs/ethereum/cketh/minter/src/sweep.rs
    Add new sweeper module with process_sweeper_transactions and supporting logic
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    Update imports and types, add send_signed_transactions helper, adapt to sweeper flow and finalized handling

@zeropath-ai

zeropath-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

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

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/ethereum/cketh/minter/src/deposit_address/mod.rs
    pub(crate) fn sweeper_derivation_path() -> Vec { … }
Enhancement ► rs/ethereum/cketh/minter/src/lib.rs
    pub mod sweep;
Enhancement ► rs/ethereum/cketh/minter/src/main.rs
    use ic_cketh_minter::sweep::process_sweeper_transactions;
Enhancement ► rs/ethereum/cketh/minter/src/main.rs
    PROCESS_SWEEPER_TRANSACTIONS_INTERVAL and related usage updated;
Enhancement ► rs/ethereum/cketh/minter/src/main.rs
    PROCESS_SWEEPER_TRANSACTIONS_INTERVAL timer added;
Enhancement ► rs/ethereum/cketh/minter/src/state.rs
    Sweeper address getter comment and exposure added;
Enhancement ► rs/ethereum/cketh/minter/src/state.rs
    TaskType enum extended with SweeperSend;
Enhancement ► rs/ethereum/cketh/minter/src/state/transactions/tests.rs
    sweeper-related tests updated to use SWEEP_TRANSACTION_GAS_LIMIT and related constants;
Enhancement ► rs/ethereum/cketh/minter/src/sweep.rs (new file)
    Introduces Sweeper transaction processing: batch creation, signing, sending, and finalization; uses sweeper derivation path; includes retry logic and scheduling; defines SWEEP_TRANSACTION_GAS_LIMIT and batch sizes;
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    Imports and types updated to support sweeper flow (e.g., include SignableTransaction, Signed); add Address import; add new send_signed_transactions function generic over SignableTransaction; adjust finalize logic to accept sender;
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    process_retrieve_eth_requests now uses sweeper sender for latest transaction count; finalize_transactions_batch updated to take sender;
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    New function latest_transaction_count(sender) to fetch per-sender count;
Enhancement ► rs/ethereum/cketh/minter/src/withdraw.rs
    New function fetch_finalized_receipts to retrieve receipts and map to IDs with validation;

pull Bot pushed a commit to mikeyhodl/ic that referenced this pull request Aug 25, 2026
…ep (dfinity#11250)

## Why

- A deposit address holds ERC-20 tokens but no code, so it cannot sweep
itself.
- The minter delegates it to the sweeper contract with an EIP-7702
authorization.
- The cheapest carrier for that authorization is the sweep that needs
it: the first sweep touching an address installs the delegation on the
way.
- The delegation persists, so every later sweep of that address is a
plain EIP-1559 transaction.
- `deposit_from_cex_demo` measures 94'932 gas for the first sweep of an
address against 63'252 for the next.

## What

- The sweeper lane sends either transaction type, and a sweep carries
the signed authorizations it must install.
- Two no-op refactorings come first: finalizing and fee-bumping stop
being EIP-1559-only.
- That is also what gives EIP-7702 fee-bumping for free, with no
machinery of its own.
- Authorizations are held in the request rather than derived while the
transaction is built, so no replay and no fee bump ever re-signs one.
- An authorization covers the chain, the delegate and the authority's
nonce — nothing of the outer transaction.
- A single place decides which type a sweep becomes, from whether
anything is left to install.

## Scope

- Nothing enqueues a sweep yet, so nothing builds an authorization in
production.
- Choosing which addresses still need delegating belongs to the
sweep-queue source, along with reading each address' delegation from the
chain.
- Hence the request carries signed authorizations rather than a "needs
delegation" flag.
- The batch-dependent gas limit a delegating sweep needs is deferred
with it: dfinity#11237's flat 100'000 does not cover one.

## Candid compatibility

- Needs the `CI_OVERRIDE_DIDC_CHECK` label.
- Two sweeper event cases change the type of their `transaction` field,
and a third gains a required field.
- Neither is a Candid subtype on a returned variant.
- Those cases only reached the interface with dfinity#11144 and have never been
released, so no deployed canister has emitted them and no client can be
reading them.



[DEFI-2917]:
https://dfinity.atlassian.net/browse/DEFI-2917?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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!

Comment thread rs/ethereum/cketh/minter/src/sweep.rs Outdated
Comment thread rs/ethereum/cketh/minter/src/withdraw.rs
gregorydemay and others added 6 commits August 25, 2026 13:02
Four helpers in `withdraw` were implicitly about the main address: they called
`minter_address()` themselves, or keyed receipts by `LedgerBurnIndex`. Reading a
transaction count, broadcasting signed transactions and fetching finalized
receipts are none of them specific to a sender or to what a request is keyed by.

Take the sender as an argument and make the receipt fetch generic over the
pipeline id, so a second pipeline sending from a different address can reuse
them. `finalize_transactions_batch` keeps its behaviour: the receipt loop moves
into `fetch_finalized_receipts`, which returns `None` where the loop used to
return early.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reschedules a sweep whose prepaid gas cannot pay the current fee, the
way the withdrawal task does, and gives SweepId the Display that the
task's log lines and the shared receipt fetcher need.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The task borrowed the withdrawal task's interval and retry interval,
which said the two cadences must move together. They are equal for now
and need not stay so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both send tasks refresh the shared gas fee estimate under one guard on
an equal cadence, and the withdrawal task is registered first, so while
withdrawals are pending the sweeper systematically read None and waited
a whole interval to try again. It now retries, by which time the
refresh it lost to has cached an estimate it can reuse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keeps the Display impl dropped in #11144 dropped: the id only ever
reaches a log line, and the trait already requires Debug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregorydemay
gregorydemay force-pushed the greg/DEFI-2926-sweeper-send-task branch from b499dd5 to 7b30e89 Compare August 25, 2026 13:05
@gregorydemay
gregorydemay removed request for a team August 25, 2026 13:05
@gregorydemay
gregorydemay removed the request for review from a team August 25, 2026 13:05
The helpers serve both send lanes now, so their messages named neither
the sender nor the lane. Their own logs already carry a task prefix; it
is the shared ones that were ambiguous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregorydemay
gregorydemay added this pull request to the merge queue Aug 25, 2026
Merged via the queue into master with commit 6c76ca8 Aug 25, 2026
40 checks passed
@gregorydemay
gregorydemay deleted the greg/DEFI-2926-sweeper-send-task branch August 25, 2026 14:32
gregorydemay pushed a commit that referenced this pull request Aug 27, 2026
The funding decision reads a lower bound on the sweeper address' balance,
tracked from the minter's own events. Nothing debited it, which was true
while nothing spent from that address — and #11144 and #11237 have since
landed the pipeline that does, with #11258 about to connect it to the sweep
queue. With those in place the bound only ever grows: fundings top the
sweeper up once, sweeps spend the gas, and the bound still reports a full
sweeper, so `amount_due` declines every later funding. The sweeper drains,
sweeping stalls, and every counter says it is funded.

An accepted sweep now provisions the most it can cost that address — the ETH
it moves plus its fee ceiling, which caps every resubmission the pipeline
makes for it — and gets back what it did not need when it finalizes: the fee
it did not pay, plus the value it did not move if it failed.

Provisioning at acceptance rather than debiting at spend is what keeps this a
bound while sweeps are in flight. Gas a committed sweep will pay stops
counting as available immediately, and a sweep whose finalization is never
observed leaves the bound too low — which delays a funding — rather than too
high, which would let the minter believe in gas that is gone. It is the
discipline the withdrawal pipeline already applies to its own fees, and the
one this stack described in review before sweeping landed.

The counters are kept out of `cumulative_spent`: this ETH was counted there
once already, when the funding that delivered it finalized, and counting it
twice would make spend overtake burn and trip the burn-first invariant. The
bound floors at zero rather than trapping, since an upgrade that starts the
counters from zero — or a sweeper funded before it was tracked — can
legitimately leave provisioning above deliveries, and trapping in a state
transition would take the replay of every later event with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

4 participants