Skip to content

fix(evm): make evm.faucet usable — unwrap the result envelope, wait for the claim on-chain - #341

Merged
qu0b merged 2 commits into
masterfrom
qu0b/evm-faucet-onchain-confirm
Aug 19, 2026
Merged

fix(evm): make evm.faucet usable — unwrap the result envelope, wait for the claim on-chain#341
qu0b merged 2 commits into
masterfrom
qu0b/evm-faucet-onchain-confirm

Conversation

@qu0b

@qu0b qu0b commented Aug 19, 2026

Copy link
Copy Markdown
Member

Two fixes to evm.faucet, found by running the CLI faucet flow end-to-end
against a proxy built from #277.

1. evm.faucet always failed from the sandbox

The server returns object operations in an envelope
({kind: "object", data: {…claim_hash…}}). The sandbox client used
_runtime.invoke_json, which hands back the raw envelope, and then looked for
claim_hash on the envelope itself — so every claim raised

ValueError: faucet claim did not return a tx hash

even though the claim had already succeeded server-side and the funds had
landed. Users saw an error, and would naturally re-claim, burning their
30-sessions/hour budget for nothing.

Fix: _runtime.invoke_data, which unwraps data and asserts
kind == "object", matching every other object-kind operation in the library.

This is a regression-only bug in the shipped sandbox image — modules/*/python/*.py
is baked in at image build time (sandbox/Dockerfile:33), so it needs a new
sandbox image, not just a server rollout.

2. evm.faucet returned before the claim was on-chain

awaitClaim returned as soon as the faucet reported claimStatus == "confirmed",
which is PoWFaucet's own status and only means it has broadcast the
transaction. The docstring and the pollInterval/pollAttempts comment both
claimed on-chain confirmation, which was not what the code did.

Observed: a claim returned a hash, and an immediate eth_getBalance read 0 ETH;
the same read moments later returned 1 ETH. Any automated check that funds an
address and asserts its balance is therefore racy.

The operation now polls eth_getTransactionReceipt for the claim hash before
responding, via the ethnode handler's lb sentinel instance
(rpc.<network>.ethpandaops.io), so it needs no individual node name.
faucet.Result gained confirmed and block_number.

Design notes

A missing receipt is not an error. The claim is already paid for by the time
the wait runs, so a slow chain or an unreachable execution endpoint returns the
hash with confirmed: false — warned server-side and on the sandbox's stderr —
rather than reporting a funded address as a failed claim. Only a receipt saying
the transaction reverted is an error.

The wait is capped at 30s. sandbox.timeout defaults to 60s
(pkg/config/config.go:451) and mining alone measured 19–34s. A longer receipt
wait would push a successful claim past the sandbox deadline — a worse failure
than the race it fixes. For a slow network, raise sandbox.timeout rather than
the receipt timeout.

pkg/faucet stays a pure faucet REST client. Chain access lives in the
operation layer; the package's comments claiming it waited for on-chain
confirmation are corrected.

Verification

make lint 0 issues; make test green (40 packages).

Unit: TestApplyFaucetReceipt covers mined, pre-Byzantium (no status field),
reverted, missing block number, and unparseable block number.

Live, on rebuilt server + sandbox image, against glamsterdam-devnet-7:

Claim Elapsed Result Balance at t+0
0xf892c24a…bd65 38.1s confirmed: true, block 224934 1.0 ETH
0x185a904b…fbdd2 23.4s confirmed: true, block 224945 0.0 → 1.0 shortly after
0xbc0790d8…86c7 34.3s confirmed: true 1.0 ETH

Also re-ran the surrounding matrix: unknown network → clean 404; a network whose
faucet ingress is down (glamsterdam-devnet-8, 503) → clean 502 in 0.6s, no
hang and no credential in the message.

Known caveat

Claim 2 above returned confirmed: true with a valid receipt, yet an immediate
eth_getBalance read 0. rpc.<network>.ethpandaops.io fans out across nodes, so
a receipt served by one node does not guarantee the next request's node has
that state. This is a property of the endpoint, not of the wait — the receipt is
the real on-chain guarantee, and read-your-writes across an LB can't be fixed
client-side. Tests asserting a balance right after evm.faucet() should still
poll with a short retry.

Not covered

  • Auth-gate (401) and proxy rate-limit (429) cases were not re-run here — the
    first needs an interactive device-flow re-login, the second burns 30 sessions
    of the per-user hourly budget. Neither is touched by these changes.
  • The faucet path could not be exercised against the staging proxy: staging is
    still serving proxy-0.38.7 (its /faucet/* route 404s), and its Authentik is
    down, so no token can be minted for it. Testing used a local proxy built from
    this branch, which is credential-identical to staging for the faucet path (both
    fall back to the ethnode credential via ToFaucetHandlerConfig).

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🐼 Smoke eval — 454361d: ❌ 0/8 pass

📊 Interactive report — tokens p50 0 · tokens/solve 0.

Reference points: v0.38.7 98% · master@e69687e 0% · qu0b/evm-faucet-onchain-confirm@e66c034 0%.

question result tokens tools
forky_node_coverage 0 0
tracoor_node_coverage 0 0
mainnet_block_arrival_p50 0 0
list_datasources 0 0
block_count_24h 0 0
missed_slots_24h 0 0
chartkit_default_arrival_distribution 0 0
storage_upload_session_scoped 0 0
🔭 Langfuse traces (8 runs; ⚠️ = failed)

The report walks this branch's commits against the master baseline and the most recent release. A self-contained copy is in the run's eval-smoke-* artifact.

qu0b added 2 commits August 19, 2026 13:35
evm.faucet returned the raw operation envelope
({kind:"object", data:{...claim_hash...}}) and then looked for claim_hash on
the envelope itself, so every claim raised

    ValueError: faucet claim did not return a tx hash

even though the claim had already succeeded server-side and the funds landed.

Use _runtime.invoke_data, which unwraps data and asserts kind == "object",
matching every other object-kind operation in the sandbox library.
evm.faucet returned as soon as the faucet reported claimStatus "confirmed",
which only means the faucet broadcast the transaction. An immediate
eth_getBalance after a claim could therefore read 0, so any automated check
that funded an address and asserted its balance was racy.

The operation now polls eth_getTransactionReceipt for the claim hash before
responding, via the ethnode handler's "lb" sentinel instance
(rpc.<network>.ethpandaops.io), and reports the block it landed in as
confirmed/block_number on the result.

The claim is already paid for by the time the wait runs, so a missing receipt
is not fatal: a slow chain or unreachable execution endpoint returns the hash
with confirmed=false, warned server-side and on the sandbox's stderr, rather
than reporting a funded address as a failed claim. Only a reverted receipt is
an error.

The wait is capped at 30s because sandbox.timeout defaults to 60s and mining
alone measures 19-34s — a longer wait would push a successful claim past the
sandbox deadline, a worse failure than the race it fixes.

pkg/faucet stays a pure faucet REST client; its comments claiming the flow
waited for on-chain confirmation were wrong and are corrected.
@qu0b
qu0b force-pushed the qu0b/evm-faucet-onchain-confirm branch from e66c034 to 454361d Compare August 19, 2026 11:35

@redpandabot redpandabot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Two fixes to evm.faucet: switch the sandbox client to invoke_data so the object envelope's data is unwrapped before the claim_hash lookup, and extend the server operation to poll eth_getTransactionReceipt (via the documented 'lb' load-balanced ethnode instance) so the op returns only once the claim is on-chain, surfacing confirmed/block_number and gracefully degrading to unconfirmed with a warning. I traced both fixes end-to-end (runtime invoke_data, ResultKindObject, ethNodeExecutionRPC, hex parse, proxy lb handling, the sole faucet.Result caller) and the code, tests and documented tradeoffs are consistent and sound.


Reviewed 5 changed file(s) @ 454361d9 — no blocking issues found.
A red panda's tail is nearly as long as its body — balance bar by day, blanket by night.

@qu0b

qu0b commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

CI status — two failures, both pre-existing on master

lint, test, stack-smoke, build-sandbox and goreleaser-check pass.

govulncheck — pre-existing, verified. I ran the gate against a pristine
master worktree and it fails with the identical seven advisories:

GO-2026-5026 GO-2026-5972 GO-2026-6088 GO-2026-6089 GO-2026-6090 GO-2026-6091 GO-2026-6218

These are Go standard-library advisories present in go1.26.5 and fixed in
go1.26.6; scripts/govulncheck.sh has an empty ALLOWLIST, so it trips as
soon as new stdlib advisories are published, independent of any code change.
CI last went green on master on 2026-08-06, before these landed. The cited
call traces (pkg/faucet/faucet.go:373, pkg/sandbox/docker.go:180,
internal/version/version.go:46) are all pre-existing code this PR does not
touch, and the PR adds no new dependency.

Fix is a toolchain bump to go1.26.6, which felt wrong to bundle into a faucet
bugfix — happy to send it as a separate PR if that's wanted.

smoke / Eval Smoke — already red on master. The last master push run
(2026-08-06) shows Eval Smoke: failure while CI: success. This run reports
pass-rate 0% across all 8 cases, including ones untouched by this PR
(list_datasources, mainnet_block_arrival_p50, block_count_24h) — a
harness/credential problem, not a regression from these changes.

@qu0b
qu0b merged commit 30aac0d into master Aug 19, 2026
9 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant