feat(execute): require chain-verified receipts in status - #95
feat(execute): require chain-verified receipts in status#95vaibhav4046 wants to merge 2 commits into
Conversation
kh execute status previously treated status=completed as final proof, but a completed execution without chain-verified receipts only proves submission, not landing. It also looped forever under --watch because the poll loop had no deadline (unlike transfer's pollExecStatus). - Parse receipts[] from the status response (hash, chainId, verified, receiptStatus, blockNumber, gasUsed, verifiedAt) and render them in the status table. - Add --require-verified: exit non-zero unless the execution completed AND at least one receipt exists AND every receipt has verified=true with receiptStatus "success". Fails closed on reverted, not_found, timeout and safe_inner_failure. - Add --timeout (default 5m) to --watch, mirroring the deadline pattern already used by kh execute transfer --wait. - Back-compat: without --require-verified, behavior is unchanged. Tests: 7 new cases in status_verified_test.go covering verified pass, no-receipts fail, unverified fail, each non-success receiptStatus, back-compat, watch + verify, and watch timeout.
suisuss
left a comment
There was a problem hiding this comment.
This repo has no CONTRIBUTING.md; the conventions that apply are enforced by CI and README - conventional-commit PR titles, go vet/gofmt clean, and tests alongside the changed command.
What this changes
cmd/execute/status.go:
ExecStatusResponsegains aReceipts []ExecReceiptfield;renderExecStatusprints one table row per receipt (hash (receiptStatus, verified|unverified)).- New
--require-verifiedflag onkh ex status:renderExecStatusCheckedcallsverifyExecReceipts, which fails unlessStatus == "completed", at least one receipt exists, and every receipt hasVerified == trueandReceiptStatus == "success". - New
--timeoutflag (default 5m) on--watch.watchExecStatusnow computesdeadline := time.Now().Add(timeout)and checks it on both the ticker branch and the idle-sleep branch, returning a timeout error once exceeded. Previously--watchpolled with no deadline. status_verified_test.go(new, 246 lines): coversverifyExecReceipts's branches (no receipts, unverified, each non-successreceiptStatus), back-compat without the flag,--watch --require-verifiedreaching a verified terminal state, and--watch --timeout 100msagainst a permanently-pending execution.
Does it match the description
Scope creep - two independently shippable changes are bundled under one PR, and the description itself names them as separate ("Separately, kh execute status --watch has no deadline..."). Split test:
- Receipts parsing/rendering +
--require-verifiedis opt-in and purely additive - it does not need the--watchtimeout to exist or be correct. - The
--timeout/deadline addition to--watchis unrelated to receipt verification and changes default behavior for every existing--watchcaller (previously unbounded polling, now capped at 5m unless overridden) - it does not need receipts or--require-verifiedto exist or be correct.
Each side deploys independently: status.go's receipt struct/rendering/--require-verified path could ship without touching watchExecStatus's deadline logic, and the deadline logic could ship without the Receipts field existing at all. Recommend splitting into (a) receipts + --require-verified and (b) --watch --timeout, each with its own subset of status_verified_test.go.
Blocking
cmd/execute/status.go:184-214(watchExecStatus) - the deadline check only runs afterfetchExecStatusreturns or in the idle-sleep branch, and nothing in the request path (fetchExecStatus->client.Do->retryablehttp.Client.Doininternal/http/client.go) attaches acontext.WithTimeout/context.WithDeadlineor setshttp.Client.Timeout-> if the server accepts the connection but never responds (stalled connection, silent network partition),client.inner.Do(req)blocks insidecase <-ticker.C:indefinitely and the deadline check at line 208 is never reached ->kh ex st <id> --watch --timeout 5shangs forever despite the flag's own doc string promising "Give up watching after this long." -> fix: make the request itself bounded, e.g. threadcontext.WithTimeout(ctx, timeout)throughclient.NewRequest/client.Do. Note this same gap already exists inpollExecStatus(transfer.go,--wait --timeout), which this PR's description says it mirrors - see Needs a decision.
Mechanical - actionable as-is
cmd/execute/status.go:209vsstatus.go:213- the ticker-branch timeout error is"timeout after %s: execution %s still %s"(includes the actual status) while the idle-branch timeout error is"execution %s still not terminal"(no status, different wording). Both fire at the same logical point (deadline exceeded mid-poll); use one consistent message.status_verified_test.go's--watch --timeouttest only exercises a server that responds promptly withpendingon every poll - it does not cover a stalled/non-responding request, so it does not catch the Blocking item above. Add a case using a handler that blocks past the timeout duration.
Needs a decision
- The hung-request gap in
watchExecStatusmirrors an identical, pre-existing gap inpollExecStatus(transfer.go,--wait --timeout) - fixing it only in the new code duplicates the defect rather than removing it. (a) Fix both call sites now by adding context-aware requests tointernal/http/client.go, touching code outside this PR's stated scope; (b) fix onlywatchExecStatushere and file thepollExecStatusgap separately; (c) leave both and document the limitation in both--timeoutflag descriptions.
Verdict
Changes requested - the --timeout flag does not reliably bound a hung request, and the PR bundles two independently shippable changes.
|
Updated per review: this PR now contains only receipt rendering and the fail-closed --require-verified\ gate. The timeout flag, implementation, and test were removed so timeout/cancellation can ship independently with request-scoped handling. \gofmt\ is clean and \go test ./cmd/execute\ passes. Ready for re-review. |
|
Review follow-up at 4e710be: the independent --timeout flag, deadline code, and timeout test are removed; this PR now contains only receipt rendering plus the opt-in fail-closed --require-verified gate. Local evidence: git diff --check, go test ./cmd/execute, and go vet ./... pass. go test ./... passes the changed package; remaining Windows user-config-path failures are in untouched cmd/doctor and internal/agentic. No unresolved inline review threads remain. Re-requesting @suisuss is permission-blocked (403), and CI run 31446213077 is action_required with zero jobs. Maintainer action needed: approve/run CI, then re-review this focused head. |
Problem
kh execute statuscurrently treatsstatus=completedas final proof of success. A completed execution without chain-verified receipts only proves that work reached a terminal API state; it does not prove that the transaction landed successfully. Agents and CI scripts need a fail-closed way to gate their next step on a verified receipt.Related: #49 asks for better execution ID to status lookup ergonomics for agent workflows. This PR makes the existing lookup safe to use as a proof gate.
Changes
receipts[]fromGET /api/execute/{id}/status(hash,chainId,verified,receiptStatus,blockNumber,gasUsed,verifiedAt) and render each receipt in the status table.--require-verified: return non-zero unless the execution iscompleted, at least one receipt exists, and every receipt hasverified=truewithreceiptStatus="success".reverted,not_found,timeout, andsafe_inner_failurereceipt states.--require-verifiedis absent; receipts are simply shown when present.Why fail closed
A transaction hash proves submission; a verified receipt proves landing. Agent pipelines can now safely gate subsequent steps:
No receipt or an unverified/failed receipt produces a non-zero exit.
Review scope correction
The earlier revision also added
--timeout. Per review, that independent feature has been removed completely from this PR so receipt verification can ship on its own. A future timeout change can then use request-scoped cancellation and stalled-handler coverage rather than a post-response deadline check.Tests
Six focused cases in
cmd/execute/status_verified_test.gocover:--require-verified-> exit 0 and render receipt;--require-verified-> non-zero;verified=false-> non-zero with the offending hash;--watch --require-verified-> pending then completed+verified -> exit 0.Validation:
gofmtclean andgo test ./cmd/executepasses.go test ./...reaches unrelated pre-existing Windows agentic-wallet/doctor failures caused by the checkout's user-config path; this PR does not touch those packages.