Skip to content

fix: KMS stale-connection retry, verified broadcast, real signer errors#104

Merged
koko1123 merged 1 commit into
mainfrom
fix/kms-retry-and-send-verify
Jul 9, 2026
Merged

fix: KMS stale-connection retry, verified broadcast, real signer errors#104
koko1123 merged 1 commit into
mainfrom
fix/kms-retry-and-send-verify

Conversation

@lukemacauley

@lukemacauley lukemacauley commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Motivation

Root-caused from a 21-minute production outage in the gator liquidator on 2026-07-09: every batch-liquidation send failed, strictly alternating error.SigningFailed / error.RpcError, then self-recovered. Tracing both error paths to source turned up three distinct defects in this repo's send path.

Changes

1. KMS: retry once on a stale keep-alive connection (src/kms.zig)

Signing calls are often minutes apart, and KMS closes idle keep-alive connections well before that. The pooled connection is therefore routinely dead when the next kms:Sign reuses it, so the first write fails; the failed fetch discards that connection and one retry dials fresh. This was the SigningFailed half of the outage — it fired on every other attempt, which is exactly the strict alternation seen in the logs.

2. Wallet: propagate the signer's real error (src/wallet.zig)

signHash catch return error.SigningFailed collapsed every signer failure into one opaque error. For a KMS signer this hid whether the cause was a failed HTTPS call (RequestFailed), bad credentials (Unauthorized), or a recovery mismatch (AddressMismatch). Now propagated. Removes the now-unused WalletError.SigningFailed.

3. Wallet: verify ambiguous broadcasts before reporting failure (src/wallet.zig)

A JSON-RPC error on eth_sendRawTransaction does not prove the transaction missed the chain: a redelivered request can be answered already known while the first copy mines. This actually happened in the outage — a send logged as RpcError liquidated its position on-chain, and the caller wrongly treated it as failed (put the position in cooldown instead of marking it liquidated). The transaction hash is fully determined by the signed bytes, so on RpcError we now poll briefly for that hash's receipt and report success if it landed.

Adds a test that cross-checks the pre-broadcast hash against viem (independent implementation) for the well-known Anvil key #0, locking the keccak256(signed_bytes) == canonical-tx-hash invariant the receipt poll depends on.

Testing

  • zig build test — 856/856 pass (was 855; +1 new cross-validation test)
  • zig fmt --check src/ tests/ — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Transaction sending is more resilient when a broadcast error occurs, with automatic confirmation checking before reporting failure.
  • Bug Fixes

    • Improved handling of flaky network connections when calling KMS services by retrying failed requests.
    • Transaction hashes are now derived consistently from signed transaction bytes before broadcast.
    • Signing errors are now reported more accurately, and transaction-not-found cases are handled explicitly.

Three fixes to the wallet/KMS send path, motivated by a 21-minute
production outage in the gator liquidator (2026-07-09) where every
liquidation send failed, strictly alternating SigningFailed / RpcError.

- kms: retry once when the HTTPS fetch to KMS fails. Signing calls are
  often minutes apart and KMS drops idle keep-alive connections, so the
  pooled connection is routinely dead on reuse and the first write fails.
  The failed fetch discards that connection; one retry dials fresh. This
  was the SigningFailed half of the outage (every other attempt).

- wallet: propagate the signer's own error instead of collapsing it into
  a blanket SigningFailed. For KMS this distinguishes a failed HTTPS call
  from bad credentials or a recovery mismatch. Removes the now-unused
  WalletError.SigningFailed.

- wallet: on an RpcError from broadcast, poll briefly for the receipt of
  the locally-computed tx hash before reporting failure. A JSON-RPC error
  does not prove the tx missed the chain -- a redelivered request can be
  answered "already known" while the first copy mines (observed in the
  outage: a send logged as RpcError actually landed). Adds a test that
  cross-checks the pre-broadcast hash against viem for Anvil key #0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Jul 9, 2026 3:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

KMS client requests now retry once on HTTP fetch failure with per-attempt response body handling and status-based error mapping. Wallet's sendTransaction handles ambiguous JSON-RPC broadcast errors by polling for a receipt via a new transactionLanded helper, signTransaction propagates signer errors directly, and WalletError set is updated accordingly with a new test.

Changes

KMS Request Retry

Layer / File(s) Summary
Retry loop and response handling
src/kms.zig
Response body allocation moves into a per-attempt retry loop; a failed fetch is retried once, and HTTP status is mapped to .ok (owned body), Unauthorized (forbidden/unauthorized), or RequestFailed (other statuses).

Estimated code review effort: 3 (Moderate) | ~20 minutes

Wallet Transaction Broadcast and Signing

Layer / File(s) Summary
Wallet error set update
src/wallet.zig
WalletError removes SigningFailed and adds ReceiptNotFound.
Ambiguous broadcast handling and receipt polling
src/wallet.zig
sendTransaction computes the tx hash before broadcasting, catches error.RpcError, and uses new transactionLanded helper polling getTransactionReceipt up to three times to decide whether to return the tx hash or the original error.
Signing error propagation and canonical hash test
src/wallet.zig
signTransaction now directly propagates the signer's signHash error instead of converting it to SigningFailed; a new test verifies signed tx bytes and keccak hash against canonical values.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Wallet
  participant Provider
  Wallet->>Wallet: compute tx_hash from signed_bytes
  Wallet->>Provider: sendRawTransaction(signed_bytes)
  Provider--xWallet: error.RpcError
  Wallet->>Wallet: transactionLanded(tx_hash)
  loop up to 3 attempts
    Wallet->>Provider: getTransactionReceipt(tx_hash)
    Provider->>Wallet: receipt or none
  end
  alt receipt found
    Wallet->>Wallet: return tx_hash
  else no receipt
    Wallet->>Wallet: return original RpcError
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main fixes: KMS retry, broadcast verification, and propagating real signer errors.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kms-retry-and-send-verify

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/wallet.zig (1)

157-171: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Short landing window (~2s) means a still-pending in-mempool tx triggers a nonce rollback.

When transactionLanded returns false, the errdefer at Lines 117–119 rolls back the managed nonce. If the ambiguous broadcast actually reached the mempool but has not mined within the ~2s poll window, the nonce is returned to the manager and can be reissued, colliding with the pending tx. This matches the "strictly no worse than before" contract, but since the whole point is disambiguation, consider widening/parameterizing the poll window (or only rolling back when a receipt lookup is authoritatively negative).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet.zig` around lines 157 - 171, The broadcast path in wallet.zig is
rolling back the managed nonce too early when the receipt check is inconclusive.
Update the `sendRawTransaction`/`transactionLanded` flow so `transactionLanded`
is not treated as a hard negative after the short poll window; instead, widen or
parameterize the polling window in `transactionLanded` and only allow the
`errdefer` nonce rollback in the caller when a receipt lookup is definitively
negative. Keep the ambiguity-handling logic around `tx_hash`,
`provider.sendRawTransaction`, and `transactionLanded` aligned so
pending-in-mempool transactions do not get reissued.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/wallet.zig`:
- Around line 157-171: The broadcast path in wallet.zig is rolling back the
managed nonce too early when the receipt check is inconclusive. Update the
`sendRawTransaction`/`transactionLanded` flow so `transactionLanded` is not
treated as a hard negative after the short poll window; instead, widen or
parameterize the polling window in `transactionLanded` and only allow the
`errdefer` nonce rollback in the caller when a receipt lookup is definitively
negative. Keep the ambiguity-handling logic around `tx_hash`,
`provider.sendRawTransaction`, and `transactionLanded` aligned so
pending-in-mempool transactions do not get reissued.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ef998d2-f45f-4dd0-af11-227a8f88ba0d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9a20d and 16ac046.

📒 Files selected for processing (2)
  • src/kms.zig
  • src/wallet.zig

@koko1123
koko1123 merged commit 0b5a320 into main Jul 9, 2026
13 checks passed
koko1123 pushed a commit to StrobeLabs/perpcity-zig-sdk that referenced this pull request Jul 9, 2026
Picks up the KMS stale-connection retry, verified broadcast, and real
signer errors from eth.zig v0.8.1 (StrobeLabs/eth.zig#104). Required so
downstream consumers (gator-liquidators) can bump their own eth to
v0.8.1 without pulling two divergent eth copies: Zig dedupes packages by
hash, so a consumer and this SDK must reference the identical eth version
or the vendored C crypto compiles twice and collides at link.

No source changes; the removed WalletError.SigningFailed is unused here.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
koko1123 added a commit that referenced this pull request Jul 9, 2026
v0.8.0 was cut out-of-band (git tag on 8c9a20d / #103 only, no GitHub
Release), so release-please's manifest and the version-marked extra-files
were never bumped and stayed at 0.7.0. With the manifest at 0.7.0,
release-please recomputes 0.8.0 from the feat commits since v0.7.0 and
collides with the existing v0.8.0 tag ("proposes wrong versions").

Adopt v0.8.0 as the released version by bumping the manifest plus the three
release-please extra-files (build.zig.zon, README.md, installation.mdx) to
match the existing tag. After this merges, release-please diffs from v0.8.0
and proposes 0.8.1 for the #104 fix.

Also fixes build.zig.zon self-reporting 0.7.0 while downstream (gator,
perpcity-zig-sdk) pins v0.8.0.

Does not delete the v0.8.0 tag (load-bearing: downstream pins its SHA) and
leaves CHANGELOG.md for the separate v0.8.0 GitHub Release backfill.
koko1123 added a commit that referenced this pull request Jul 10, 2026
Follow-up to #105. v0.8.1 was hand-tagged out-of-band at 0b5a320 (#104,
the KMS fix) after #105 had already synced main to 0.8.0, leaving main's
manifest one version behind the latest tag -- the same drift #105 fixed,
shifted up a notch. Bump the manifest and the three release-please
extra-files (build.zig.zon, README.md, installation.mdx) 0.8.0 -> 0.8.1
to match the existing v0.8.1 tag.

With the org "Actions can create PRs" setting now enabled, this lets
release-please treat 0.8.1 as already released (tag exists at 0b5a320),
retire its stranded release-please--branches--main branch, and cleanly
propose 0.8.2 for the next real change -- without re-tagging the
load-bearing v0.8.1 (pinned by perpcity-zig-sdk and gator).
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.

2 participants