Skip to content

feat(cketh): sweeper funding observability and the prepaid-gas gate - #11094

Draft
mbjorkqvist wants to merge 28 commits into
mathias/DEFI-2933-funding-taskfrom
mathias/DEFI-2933-observability
Draft

feat(cketh): sweeper funding observability and the prepaid-gas gate#11094
mbjorkqvist wants to merge 28 commits into
mathias/DEFI-2933-funding-taskfrom
mathias/DEFI-2933-observability

Conversation

@mbjorkqvist

@mbjorkqvist mbjorkqvist commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Part of DEFI-2933 (sweeper fee funding), second of the three remaining PRs. Targets #11086.

Why

Two things are missing once funding works: an operator cannot see whether ckETH is still fully backed, and sweeping has no way to ask whether it may spend gas at all. This adds both.

The gate

check_prepaid_sweep_gas answers "does the prepaid gas cover this sweep?" against the lower bound the minter tracks from its own events — not a reading of the chain. That is what makes it fail closed: the bound errs low, so refusing can only withhold a sweep that would in fact have been paid for, which costs delay. Nothing is ever authorised that the minter's own records do not already account for.

It is a precondition on a bound, not an allowance: it neither reserves nor deducts, so a caller issuing several sweeps must subtract what it has already committed — two 6-wei sweeps both pass against a 10-wei bound. The backing invariant does not rest on the gate at all: a funding counts its whole transfer as spent once it finalizes, so every wei at the sweeper address is already covered by a burn that preceded it.

The gate has no production caller yet; S2 (DEFI-2926) is what will consult it, exactly as the fee-subaccount burn waited for the funding task. Tests exercise it directly in the meantime.

Observability

Six metrics. Burned and spent are counters, so the invariant can be alerted on directly — burned must never fall below spent. Alongside them: how far burn currently runs ahead of spend, the prepaid-gas bound, a count of fundings that failed on chain, and the age of a funding awaiting finalization.

The burn-ahead gauge is the burn of a funding still in flight plus the fees earlier fundings provisioned but never paid. Nothing draws it down — the surplus stays as ckETH backing rather than discounting a later funding — so between fundings it only ratchets up.

The age to alert on is sweeper_in_flight_funding_age_seconds: a funding that never finalizes blocks every later one, and nothing else here reveals that, since the gas-balance gauge reports what the minter has recorded rather than what is stuck. It reads 0 when no funding is outstanding, and NaN for one whose acceptance time is unknown rather than an age measured from zero. The failed-funding counter is expected to stay at zero — funding is a bare transfer to an address derived from the minter's own key, with no code to revert in — so any non-zero value wants a look.

Proposed alert conditions are recorded on DEFI-2965, which collects observability for this pipeline rather than alerting on each piece separately.

The dashboard gains a sweeper-funding section: the address, the prepaid-gas bound, the low-water mark and target it is compared against, burned and spent, the funding in flight if there is one, and the unspent burn. It renders an in-flight funding's acceptance time as a timestamp rather than an age because DashboardTemplate::from_state must stay callable outside a canister — ic_cdk::api::time() traps in unit tests — and ages belong in metrics anyway.

Stack

Merge in order; each PR targets the one above it.

# PR Status
#11060 — Read a native ETH balance via the EVM RPC canister merged
#11065 — Burn ckETH from the minter's own fee subaccount closed; folded into #11072 and #11086
#11072 — Add the SweeperFunding withdrawal-request variant merged
#11083 — Burn-first accounting for sweeper fee funding merged
1 #11086 — Sweeper fee-funding task, with an end-to-end test draft
2 Sweeper funding observability and the prepaid-gas gate this PR
3 #11097 — Adversarial end-to-end coverage of sweeper fee funding draft

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 observability and a fail-closed prepaid-gas gate for ckETH sweeper funding.

Changes:

  • Caches sweeper balance observations and validates gas availability and freshness.
  • Exposes funding accounting, balances, and age metrics.
  • Adds dashboard reporting and tests for funding state and gate behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
templates/dashboard.html Adds sweeper-funding dashboard section.
src/sweeper.rs Caches observed sweeper balances.
src/state/tests.rs Updates state fixture.
src/state/sweeper_funding/tests.rs Tests prepaid-gas gate behavior.
src/state/sweeper_funding.rs Defines observations and fail-closed gate.
src/state.rs Stores the volatile balance observation.
src/main.rs Exports six sweeper-funding metrics.
src/lifecycle/init.rs Initializes the observation cache.
src/dashboard/tests.rs Tests sweeper dashboard output.
src/dashboard.rs Builds sweeper dashboard data.

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

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 10 out of 10 changed files in this pull request and generated 1 comment.

Comment on lines +208 to +214
if observed.balance < required {
return Err(PrepaidGasUnavailable::Insufficient {
available: observed.balance,
required,
});
}
Ok(observed.balance)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The observation about the function's semantics is correct — it is a per-request predicate against a snapshot, and two 6-wei sweeps would both pass against a 10-wei observation. I have made that explicit in the doc comment (cd36d1a), since the previous wording invited exactly this reading.

I don't think the conclusion follows, though, on two counts.

The backing invariant does not rest on this check. record_finalized_funding adds the whole transferred amount to cumulative_transferred as soon as the funding transaction finalizes, and cumulative_spent() = transferred + fees. Every wei at the sweeper address is therefore already covered by a burn that preceded the ETH moving, so drawing that balance down — even over-eagerly — cannot make cumulative spend outrun cumulative burn. What over-authorising costs is a sweep the sweeper cannot pay for: wasted signatures, nonces and outcalls, not under-backed ckETH.

There is no caller yet. Nothing in production calls check_prepaid_sweep_gas; this PR adds the observation cache and the predicate, while sweeping itself is [S2] (DEFI-2926), still in the backlog. The function takes Option<ObservedSweeperBalance> by value, so it cannot reserve anything by construction — reserving across several sweeps is the caller's job.

So rather than restructure a function whose caller does not exist, I have recorded the requirement on DEFI-2926: the sweeping task must subtract what it has already committed since the last observation, either by tracking outstanding reservations in state and including them in required or by reconciling each against its receipt. Flagging it here was useful — daily observations mean many sweeps can fall between two of them, so this would have been easy to get wrong.

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 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

rs/ethereum/cketh/minter/src/sweeper.rs:62

  • The cache-write behavior is not asserted by the new tests: the gate and dashboard tests inject ObservedSweeperBalance directly, while tests/sweeper_funding.rs only checks that funding lands. Removing this block—or accidentally placing it after the NotDue return—would therefore leave tests green but keep the gate at NeverObserved on the common no-funding path. Add task-level or integration coverage showing that every successful balance read refreshes the value and timestamp even when funding is not due, and that a failed read does not fabricate an observation.
    // Cached before deciding anything: sweeping consults this far more often than it changes, and
    // recording it even when no funding is due is what keeps the observation fresh.
    let observed_at_nanos = ic_cdk::api::time();
    mutate_state(|s| {
        s.last_observed_sweeper_balance = Some(ObservedSweeperBalance {
            balance: sweeper_balance,
            observed_at_nanos,
        });
    });

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 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

rs/ethereum/cketh/minter/src/state.rs:126

  • The safety-critical upgrade behavior is not covered: existing tests exercise initial None and successful/failed reads, but none starts with Some(...), performs replay/upgrade, and verifies the observation is absent afterward. Please add that test so this volatile cache cannot later become event-sourced or upgrade-persistent unnoticed; assert the post-upgrade metrics/gate remains fail-closed until a new balance callback completes.
    /// The sweeper address' ETH balance as last read on chain, i.e. the prepaid sweep gas.
    /// Volatile cache refreshed by the funding task, deliberately not event-sourced.
    pub last_observed_sweeper_balance: Option<ObservedSweeperBalance>,

rs/ethereum/cketh/minter/src/main.rs:1135

  • This gauge is not always the credit available to offset the next funding. Once a funding burn is accepted, burned_not_yet_spent() includes that new burn, while in_flight_funding().amount is explicitly earmarked and plan_funding refuses to reuse it. During that interval the metric can overstate usable credit by the entire funding amount. Either subtract the in-flight earmark when exporting the “outstanding credit” metric, or describe this as gross unspent burn and expose the offsettable credit separately.
                w.encode_gauge(
                    "cketh_minter_sweeper_funding_burned_not_yet_spent",
                    s.sweeper_funding.burned_not_yet_spent().as_f64(),
                    "ckETH burned for sweeping but not yet spent, i.e. the credit that offsets \
                     the next funding.",

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 12 out of 12 changed files in this pull request and generated no new comments.

Rachit2323 pushed a commit to Rachit2323/ic that referenced this pull request Aug 12, 2026
…ity#11060)

Part of [DEFI-2933](https://dfinity.atlassian.net/browse/DEFI-2933)
(sweeper fee funding), first of a seven-PR stack.

## Why

Funding the sweeper address with gas requires knowing how much gas it
already holds. The EVM RPC canister exposes no endpoint for a native ETH
balance, and its Rust client offers no getter for one, so the minter
currently has no way to ask.

## What

Reads the balance through the EVM RPC canister's generic JSON-RPC
passthrough, which forwards a payload to every provider and agrees on
one answer under the configured consensus strategy. That strategy is a
threshold of the providers — 3 of 4 on mainnet, 2 of 4 on Sepolia — and
it is the only agreement accepted: there is no client-side reduction, so
a result the canister reports as inconsistent stays an error rather than
being resolved by picking a winner.

Because the canister deserializes each response's `result` field, what
the minter receives is the quantity itself rather than any surrounding
JSON. It is therefore decoded exactly: quotes, padding, leading zeros
and sign characters are the provider's own malformation and are rejected
rather than repaired.

A failed read is an error, never a zero. This is the decision the rest
of the stack depends on: confusing "could not read the balance" with "no
gas left" would burn ckETH to top up an address that is already funded,
which is pure loss. The request builder and the result decoder are pure
functions so both sides of that guarantee are pinned directly, including
a test asserting that no error input can decode to a zero balance.

The route was also proven end to end against a live EVM RPC canister and
a local anvil node, reaching 3-of-4 consensus at both `latest` and
`finalized`.

## Stack

Merge in order; each PR targets the one above it.

| # | PR | Status |
|---|----|--------|
| 1 | Read a native ETH balance via the EVM RPC canister | **this PR** |
| 2 | dfinity#11065 — Burn ckETH from the minter's own fee subaccount | ready
for review |
| 3 | dfinity#11072 — Add the SweeperFunding withdrawal-request variant | ready
for review |
| 4 | dfinity#11083 — Burn-first accounting for sweeper fee funding | ready for
review |
| 5 | dfinity#11086 — Sweeper fee-funding task, with an end-to-end test |
Copilot re-review pending, CI green incl. long tests |
| 6 | dfinity#11094 — Sweeper funding observability and the prepaid-gas gate |
open |
| 7 | dfinity#11097 — Adversarial end-to-end coverage of sweeper fee funding |
open |

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-funding-task branch 2 times, most recently from 6320ea9 to 283069e Compare August 14, 2026 14:27
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-observability branch from 4d829c1 to e5adda2 Compare August 14, 2026 14:32
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-funding-task branch from 283069e to 17d0c30 Compare August 18, 2026 16:05
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-observability branch from b633db9 to 9d7cb6d Compare August 18, 2026 16:17
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-funding-task branch from 17d0c30 to 12c7666 Compare August 18, 2026 17:54
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-observability branch from 9d7cb6d to c062a60 Compare August 18, 2026 17:54
mbjorkqvist and others added 5 commits August 24, 2026 13:57
The fixture no longer answers it, so a test that needs an observation to exist
asks for one. Which is the point of making that opt-in: this test is about the
observation being forgotten across an upgrade, and it now says out loud that
the pre-upgrade read is answered while the post-upgrade one is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A funding transaction cannot fail on Ethereum unless an assumption broke: it
is a bare transfer to an address derived from the minter's own key, with no
code to revert in. The log line says as much, but a log cannot be alerted on,
so the count is exposed as cketh_minter_sweeper_funding_failed_total and is
expected to stay at zero.

Counted where the receipt status is already matched, so it cannot drift from
the transferred amount it sits beside, and saturating rather than checked: an
alert that stops incrementing is a worse outcome than a wrapped count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gauge's help text promised credit a later funding could offset against.
Nothing draws the surplus down any more, so it names its two parts instead --
the burn of a funding in flight, and fees provisioned but never paid.

The earmark's acceptance time follows the request's own field in being optional,
so the age gauge reports NaN when a funding is outstanding with no time recorded,
rather than an age measured from zero. The dashboard renders it through the
existing optional-timestamp macro, which also takes two unwraps out of the
template.
The dashboard row and the funding-age gauge read the outstanding funding off the
withdrawal pipeline now that the accounting no longer keeps one. Their age still
comes from the request's own acceptance time, which stays optional, so the NaN
reading for "outstanding but undated" is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the accessor moving onto `State` in the parent PR.
@mbjorkqvist
mbjorkqvist force-pushed the mathias/DEFI-2933-observability branch from 0bcd43c to 7a048b0 Compare August 24, 2026 14:19
mbjorkqvist and others added 5 commits August 24, 2026 14:59
…observability

The funding task no longer reads the sweeper address' balance over an
HTTPS outcall, so the harness helpers that answered that read are gone
and with them the two tests that only exercised recording an
observation. The observation itself is removed next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prepaid-gas gate rested on a cached on-chain reading of the sweeper
address' balance, which brought with it a freshness policy: an
observation older than two days stopped sweeping, an upgrade dropped the
cache, and a task that failed to read left the gate closed. All of that
existed to make a value the minter did not own safe to spend against.

It owns one already. The lower bound its own recorded fundings imply
needs no refreshing, survives an upgrade because it is event-sourced,
and cannot overstate what is there. The gate becomes a comparison
against it, still fail-closed in the sense that matters: it authorises
nothing the minter's records do not already account for, and refusing
too often costs only delay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cketh_minter_sweeper_gas_balance` exported a cached on-chain reading,
NaN until one had been taken, alongside an age gauge to say how much to
trust it. Neither has anything to describe now: the gauge reports the
lower bound the minter derives from its own recorded fundings, which is
always defined and never stale, so the age gauge goes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prepaid-gas row reported a cached on-chain reading with the time it
was taken, and "never observed" before the first one. It now shows the
lower bound the minter tracks from its recorded fundings, which is
defined from the start, so neither the timestamp nor the never-observed
case has anything left to say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mbjorkqvist added a commit that referenced this pull request Aug 25, 2026
mbjorkqvist added a commit that referenced this pull request Aug 25, 2026

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 8 out of 8 changed files in this pull request and generated 4 comments.

Suppressed comments (1)

rs/ethereum/cketh/minter/src/state/sweeper_funding.rs:135

  • The gate no longer matches the contract stated in the PR: it accepts an event-derived Wei bound and can only report Insufficient, so it cannot distinguish never-observed or stale observations, and replayed funding state remains usable after an upgrade. Either implement the described non-event-sourced observation/timestamp checks (including the two-day limit) or update the PR’s requirements to this materially different design.
pub fn check_prepaid_sweep_gas(bound: Wei, required: Wei) -> Result<Wei, PrepaidGasUnavailable> {
    if bound < required {
        return Err(PrepaidGasUnavailable::Insufficient {
            available: bound,
            required,
        });

Comment on lines +1229 to +1235
w.encode_gauge(
"cketh_minter_sweeper_gas_balance",
s.sweeper_funding.sweeper_balance_lower_bound().as_f64(),
"Prepaid sweep gas: a lower bound on the sweeper address' ETH balance, from the \
fundings the minter has recorded as finalized -- not an observation of the \
chain, and never above the true balance.",
)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right on both counts, and the fix is the description: it still described the design this stack started with, where the funding task read the balance through the EVM RPC canister and cached it, and the gate refused on "never observed" or "stale". That was replaced by the tracked lower bound (the eth_getBalance thread on #11086), which removed the cache, the staleness limit and the age metric along with it. The description now says what the code does.

There is deliberately no balance-age metric in the new design: there is nothing to go stale, since the bound is derived from the minter's own events and cannot be older than the last funding it recorded. What can go wrong is a funding that never finalizes, which is what cketh_minter_sweeper_in_flight_funding_age_seconds is for.

Comment on lines +191 to +193
<tr id="sweeper-prepaid-gas">
<th>Prepaid gas (lower bound)</th>
<td>{{ sweeper_funding.prepaid_gas_lower_bound }} Wei</td>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same stale description, now corrected — there is no observation to show a timestamp for.

On the conflation: with a tracked bound, 0 Wei is not "never looked". It means no funding has been recorded as finalized yet, which is a fact about the minter's own history rather than a missing reading, and the row above it shows the low-water mark and target it is compared against. The distinction that mattered in the old design — "no gas" versus "never read the chain" — cannot arise when nothing reads the chain.

DashboardAssert::assert_that(dashboard)
.has_sweeper_cketh_burned("1_000_000 Wei")
.has_sweeper_eth_spent("950_000 Wei")
// 1_000_000 burned - 950_000 spent: the credit that offsets the next funding.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and fixed. That comment survived from a design where the surplus was discounted from the next burn; it now says the surplus stays as backing. The metric help text and the accounting docs already said so, which is what made the comment stand out.

Comment on lines +25 to +28
// What the caller does on a failure receipt: no ETH transferred, and the count bumped.
accounting.record_burn(Wei::new(BURN));
accounting.record_failed_funding();
accounting.record_finalized_funding(Wei::ZERO, Wei::new(FEE));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A real gap, thank you — the counter was only ever incremented by a test calling record_failed_funding directly, so the wiring in record_finalized_transaction could have been deleted with the suite still green.

Covered now by should_record_a_funding_whose_transaction_failed_on_chain, which drives the events rather than the accounting: it accepts a funding, creates and signs the transaction, finalizes it with a failure receipt, and asserts the counter went 0 to 1, that the bound stays at zero because nothing was delivered, that the gas the failed transaction still paid is recorded as spent, and that the burn stays put. The success paths in the same module assert the counter stays at zero.

mbjorkqvist and others added 3 commits August 25, 2026 08:06
The failed-funding counter was only ever incremented by a test calling it
directly, so the wiring in the state transition — the counter, the transfer
that delivered nothing, and the gas a failed transaction still pays — could
have been removed with the suite staying green. Copilot spotted it.

Also drops a comment describing an accounting rule this design deliberately
does not have: the unspent burn stays as backing, it does not discount a
later funding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
)?;
w.encode_gauge(
"cketh_minter_sweeper_funding_burned_not_yet_spent",
s.sweeper_funding.burned_not_yet_spent().as_f64(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A note on how this gauge sits next to the pre-existing cketh_minter_total_unspent_tx_fees, since this is the PR that puts the funding numbers beside it.

For a user withdrawal the ckETH burned is withdrawal_amount while the ETH leaving the main address is delivered + effective, so ETH at main address − ckETH supply == Σ (charged − effective) == total_unspent_tx_fees — the figure docs/cketh.adoc describes as fees charged but in retrospect not needed. A funding behaves the same way, since it burns its full amount and never discounts a later burn, so the identity still holds with funding live.

What does not hold is reading the two side by side. For a funding that succeeds, burned_not_yet_spent grows by exactly charged − effective — the same wei total_unspent_tx_fees just counted — so summing them double counts. For one that fails they diverge instead: the whole undelivered burn stays as backing, while the unspent-fee figure only sees the fee difference. Composing them means picking the user-withdrawal part of the older counter, which nothing exposes separately today.

I would rather not add a derived over-backing gauge here for the sake of it. Flagging it so it is on the record when the alerts on DEFI-2965 are written: alert on burned versus spent, not on a sum of these two.

Comment on lines +1229 to +1235
w.encode_gauge(
"cketh_minter_sweeper_gas_balance",
s.sweeper_funding.sweeper_balance_lower_bound().as_f64(),
"Prepaid sweep gas: a lower bound on the sweeper address' ETH balance, from the \
fundings the minter has recorded as finalized -- not an observation of the \
chain, and never above the true balance.",
)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right on both counts, and the fix is the description: it still described the design this stack started with, where the funding task read the balance through the EVM RPC canister and cached it, and the gate refused on "never observed" or "stale". That was replaced by the tracked lower bound (the eth_getBalance thread on #11086), which removed the cache, the staleness limit and the age metric along with it. The description now says what the code does.

There is deliberately no balance-age metric in the new design: there is nothing to go stale, since the bound is derived from the minter's own events and cannot be older than the last funding it recorded. What can go wrong is a funding that never finalizes, which is what cketh_minter_sweeper_in_flight_funding_age_seconds is for.

Comment on lines +191 to +193
<tr id="sweeper-prepaid-gas">
<th>Prepaid gas (lower bound)</th>
<td>{{ sweeper_funding.prepaid_gas_lower_bound }} Wei</td>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same stale description, now corrected — there is no observation to show a timestamp for.

On the conflation: with a tracked bound, 0 Wei is not "never looked". It means no funding has been recorded as finalized yet, which is a fact about the minter's own history rather than a missing reading, and the row above it shows the low-water mark and target it is compared against. The distinction that mattered in the old design — "no gas" versus "never read the chain" — cannot arise when nothing reads the chain.

DashboardAssert::assert_that(dashboard)
.has_sweeper_cketh_burned("1_000_000 Wei")
.has_sweeper_eth_spent("950_000 Wei")
// 1_000_000 burned - 950_000 spent: the credit that offsets the next funding.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and fixed. That comment survived from a design where the surplus was discounted from the next burn; it now says the surplus stays as backing. The metric help text and the accounting docs already said so, which is what made the comment stand out.

Comment on lines +25 to +28
// What the caller does on a failure receipt: no ETH transferred, and the count bumped.
accounting.record_burn(Wei::new(BURN));
accounting.record_failed_funding();
accounting.record_finalized_funding(Wei::ZERO, Wei::new(FEE));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A real gap, thank you — the counter was only ever incremented by a test calling record_failed_funding directly, so the wiring in record_finalized_transaction could have been deleted with the suite still green.

Covered now by should_record_a_funding_whose_transaction_failed_on_chain, which drives the events rather than the accounting: it accepts a funding, creates and signs the transaction, finalizes it with a failure receipt, and asserts the counter went 0 to 1, that the bound stays at zero because nothing was delivered, that the gas the failed transaction still paid is recorded as spent, and that the burn stays put. The success paths in the same module assert the counter stays at zero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants