Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.

fix: keep the RPC URL out of sponsord errors and logs - #39

Merged
thiras merged 3 commits into
mainfrom
fix/redact-rpc-url
Oct 5, 2026
Merged

thiras merged 3 commits into
mainfrom
fix/redact-rpc-url

Conversation

@thiras

@thiras thiras commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

A failed chain call printed SPONSORD_RPC_URL in the daemon's exit error, the keeper's warnings and the HTTP API's warn/error lines. That URL often carries a provider API key in its path. The cause: reqwest's Display ends in for url (<url>), and alloy forwards it.

This PR follows decdn-node's fix (decdn/decdn#958) and closes every path I found:

  • Errors. ChainPool renders each chain-call error's source() chain with the URLs stripped (redacted(), built on decdn_common::redact::strip_urls). It strips each link separately and skips a link that repeats the one before it. Its errors therefore never contain the URL, and they now keep the failure class (connection refused, timeout, DNS, TLS), which alloy's outer Display leaves out.
  • top_up markers. top_up errors still downcast to decdn's TopUpUnconfirmed (the top-up may have mined) and AllowanceShortfall; redacted_top_up re-attaches them after redaction.
  • Log sites. keeper.rs and http.rs render errors through sanitize_err_chain as a backstop.
  • Exit. main() prints its error through sanitize_err_chain instead of returning it to std's Termination impl.
  • RUST_LOG=debug. alloy's ReqwestTransport debug span records the URL, and every reqwest and hyper event inside that span would print it. main() now builds the RUST_LOG filter with alloy_transport_http=info added, which hides only that span and alloy's own debug/trace events.
  • --help and Debug. --help hides the URL's value, as it does for the token and password. ChainConfig's Debug shows the URL's length instead of the URL.
  • Bind order. The listener is bound before the keeper starts. A bind failure can no longer drop a top-up between the approve and the top-up.

Before:

Error: read PaymentPool.usdc(): error sending request for url (http://127.0.0.1:9/v3/<key>)

After:

Error: read PaymentPool.usdc(): error sending request: client error (Connect): tcp connect error: Connection refused (os error 111)

Closes #38

Reviewer notes

  • Start with redacted and redacted_top_up in crates/sponsord-core/src/pool.rs, then init_tracing in crates/sponsord/src/main.rs.
  • Why redacted strips each link before joining. If strip_urls ran on the joined string, it would eat the : after a removed URL, because it consumes up to the next whitespace.
  • What still shows at trace. hyper still logs connecting to <host>:<port>, but nothing logs the path. trace_logging_does_not_print_the_rpc_url checks this, including RUST_LOG=alloy_transport_http=trace,trace.
  • Not changed: default log level. With RUST_LOG unset, sponsord still prints only ERROR events, as fmt::init did. That means the warnings this PR sanitizes are hidden unless operators set RUST_LOG.
  • Not changed: keeper retry after TopUpUnconfirmed. Before this PR too, the keeper retries every failure on its next tick, including TopUpUnconfirmed, so an unconfirmed top-up can be sent twice. The marker is preserved, so a follow-up can handle it.
  • Dependencies.
    • decdn-common is new as a direct dependency, but it was already in Cargo.lock at the pinned commit (7f938e6).
    • sponsord now declares tracing-subscriber's env-filter feature explicitly. Before, it was only enabled because decdn's crates enable it.
    • The cargo update -p … lists, the [patch] example in CONTRIBUTING and the deny.toml comment now include decdn-common.
  • Test support.
    • FakePool failures read like reqwest's error and name FAKE_RPC_URL.
    • New fail_remaining_reads switch.
    • New CapturedLog, which captures tracing output on the test's thread. It sits behind test-support, which enables the optional tracing-subscriber dependency.
  • No contract changes. The HTTP API, sponsor.toml and the archive names are unchanged.

Verification

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo nextest run --workspace: 131 passed
  • Pool, keeper or daemon code changed: cargo nextest run --features sponsord-core/anvil-e2e,sponsord/anvil-e2e -p sponsord-core -p sponsord --test chain_pool --test binary_anvil: 2 passed
  • User-visible change: entry in the changed crate's crates/<dir>/CHANGELOG.md (sponsord, sponsord-core, both under Security)
  • Installer archive names, ~/.decdn/sponsor.toml or an sponsord-api type touched: not touched
  • PR title uses a type the pr-title check allows

I also ran cargo test --doc --workspace, cargo doc with -D warnings, the check-*.sh scripts, cargo-deny-warn.sh, pytest .github/scripts/tests (144 passed) and markdownlint.

To check that the tests catch the leaks, I reverted the fixes and reran them. Each of these redaction tests failed with its fix reverted:

  • pool::tests::an_unreachable_rpc_url_stays_out_of_the_error
  • both keeper failure tests
  • api::failed_chain_read_is_503
  • startup_redacts_rpc_url
  • trace_logging_does_not_print_the_rpc_url, with the filter cap removed

🤖 Generated with Claude Code

reqwest names the URL in its transport errors (` for url (<url>)`) and
alloy forwards that text, so a failed chain call printed SPONSORD_RPC_URL,
often with a provider API key in its path, in the exit error and in the
keeper's and the HTTP API's warnings.

- ChainPool renders each chain-call error's cause chain with the URLs
  stripped (decdn_common::redact::strip_urls), so its errors keep the
  failure class (connection refused, timeout, DNS, TLS) and never carry
  the URL.
- The keeper and HTTP log sites go through sanitize_err_chain /
  sanitize_rpc_display as a backstop.
- main() prints its error through sanitize_err_chain instead of returning
  it to anyhow's Termination impl, as decdn-node does (decdn/decdn#958).

FakePool's failures now read like reqwest's error and name FAKE_RPC_URL,
and test_support gains CapturedLog, so the failure tests also assert that
the key never reaches a log line. A binary test checks the exit error.

Closes #38

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 11:45

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The keeper’s remaining-read warning still needs regression coverage for RPC URL redaction.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Protects RPC provider credentials by removing URLs from daemon errors and logs while preserving failure causes.

Changes:

  • Redacts chain errors, keeper/API logs, and fatal exit output.
  • Adds log capture and regression coverage.
  • Updates dependencies, maintenance instructions, and changelogs.
File Description
RELEASING.md Updates dependency refresh instructions.
crates/​sponsord/​tests/​startup_redacts_rpc_url.rs Tests startup error redaction.
crates/​sponsord/​tests/​api.rs Checks API warning redaction.
crates/​sponsord/​src/​main.rs Sanitizes fatal errors.
crates/​sponsord/​src/​http.rs Sanitizes request error logs.
crates/​sponsord/​CHANGELOG.md Documents the security fix.
crates/​sponsord/​Cargo.toml Adds redaction dependency.
crates/​sponsord-core/​src/​test_support.rs Adds log capture and URL-bearing failures.
crates/​sponsord-core/​src/​pool.rs Redacts chain errors at source.
crates/​sponsord-core/​src/​keeper.rs Sanitizes warnings and tests top-up failures.
crates/​sponsord-core/​CHANGELOG.md Documents core redaction changes.
crates/​sponsord-core/​Cargo.toml Adds redaction and test-support dependencies.
CONTRIBUTING.md Updates local dependency overrides.
Cargo.toml Declares shared redaction dependency.
Cargo.lock Records dependency additions.
AGENTS.md Updates dependency refresh guidance.
.github/​workflows/​ci.yml Updates canary dependency refresh.

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

Comment thread crates/sponsord-core/src/keeper.rs
thiras and others added 2 commits October 5, 2026 14:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups for #38:

- RUST_LOG=debug printed the URL on every chain call: alloy's
  `ReqwestTransport` debug span records it, and every event inside the
  span (reqwest's, hyper's) prints the span's fields. main() now builds
  the RUST_LOG filter itself with `alloy_transport_http=info` added,
  which only drops that span and alloy's own debug/trace events.
- `--help` printed SPONSORD_RPC_URL's value; it is now hidden like the
  token and password.
- `ChainConfig`'s Debug showed the URL; it now shows its length.
- Flattening `top_up` errors to text lost decdn's `TopUpUnconfirmed` and
  `AllowanceShortfall` markers, which callers downcast on (the former
  means the top-up may have mined). `redacted_top_up` re-attaches them.
- The http.rs log sites used `sanitize_rpc_display` on an anyhow::Error,
  which renders only the outermost message; they use
  `sanitize_err_chain` now.
- The listener is bound before the keeper starts, so a bind failure
  can no longer drop a top-up between the approve and the top-up.

Adds unit tests for `redacted()` and the marker handling, a closed-port
test of every ChainPool method, a `--help` test, and a binary test at
RUST_LOG=trace. Fixes the doc on main (the Termination impl is std's),
the changelog wording, and moves sponsord-core's Security section last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thiras
thiras merged commit 1a84f3b into main Oct 5, 2026
24 checks passed
@thiras
thiras deleted the fix/redact-rpc-url branch October 5, 2026 12:19
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sponsord logs the full RPC URL (and its API key) in chain errors

2 participants