[Version-unspecific] feat(bifrost/evm): bearer and basic-auth support for EVM RPC endpoints

Summary

Bifrost can now send an Authorization header to the EVM RPC endpoint, so operators can use hosted RPC providers that require an API token. Before this, the only option was a key embedded in the URL. This ports the RPC-auth part of THOR 337d8f9317 (finding A7 of the BSC parity review) and applies it to both EVM client implementations:

  • bifrost/pkg/chainclients/evm (ARB, AVAX, and any later EVM chain on the generic client)
  • bifrost/pkg/chainclients/ethereum (ETH). THOR has no auth on its ETH client; MAYA adds it here so all EVM chains behave the same.

Bifrost-only, no consensus change, so the change is version-unspecific.

THORNode references

  • thornode!3640 "ADD: Base": where the EVM client auth was written (commit 93f751f683, "add evm client auth to support 3rd party providers").
  • thornode!3801 "ADD: Base": the same code as it landed on THOR develop (337d8f9317). This MR ports the auth part only; the Base chain itself is out of scope.
  • Gap check against both MRs: every auth change is ported or adapted (shared RPCAuth/DialEthClient helper, auth also on the ETH client, empty credentials by default, no credentials in logs or dial errors). Nothing is missing. THOR has no later change to this code on develop; thornode!4818 (multi-host providers, bearer auth for UTXO RPC) is still open and not ported.

Configuration

Per chain, under bifrost.chains.<chain> (env BIFROST_CHAINS_<CHAIN>_...):

key header sent
authorization_bearer (new) Authorization: Bearer <token>. Takes precedence over basic auth.
username + password (existing keys, both non-empty) Authorization: Basic base64(user:pass)
nothing set (default) no header: same behaviour as today

The key names match THOR. Example: BIFROST_CHAINS_ETH_AUTHORIZATION_BEARER=<token>.

Default behaviour is unchanged. The default-chain YAML anchor carries the UTXO daemon credentials (mayachain / password). With THOR's logic as-is, every EVM chain would start sending a basic-auth header. To prevent that, eth, avax and arb in config/default.yaml reset username / password to "". A new config test (TestEVMChainsDefaultToNoRPCAuth) fails if a future EVM chain section inherits them again. Any new EVM chain section (BSC etc.) must add the same two lines.

Implementation

  • shared/evm/rpc_auth.go:
    • RPCAuth{Bearer, UserName, Password} and DialEthClient(host, auth). The dial goes through rpc.DialOptions with rpc.WithHTTPAuth, so the header is sent on every HTTP request and on the WebSocket handshake.
    • RedactRPCHost masks a URL-embedded password.
  • shared/evm/eth_rpc.go: NewEthRPC now takes the dialed *ethclient.Client, as in THOR. The generic EVM client used to open two RPC clients to the same host; it now opens one, which the scanner wrapper and the client share.
  • evm/client.go and ethereum/ethereum.go build the RPCAuth from config and dial through the helper.

What is logged

  • At startup the clients log only the mode (rpc_auth=none|bearer|basic).
  • RPCAuth implements String / GoString to return only the mode, so %v / %+v / %#v cannot print a secret. A test covers this.
  • Dial errors redact the user info, path, query and fragment of the host. A host without scheme:// (an opaque URL such as user:pass@host:port) is rejected before dialing, and the error does not contain it.
  • With auth configured, the client refuses HTTP redirects, so the Authorization header is never forwarded. It also masks the configured credentials in non-2xx provider response bodies (kept up to 64 KiB) before they reach an error or a log.
  • No code path logs the chain config as a whole. A bearer token never appears in the URL.
  • API keys that a provider expects in the URL path or query are not configured credentials. An HTTP transport error can include that URL, and it is logged as received.

Tests

  • shared/evm/rpc_auth_test.go, against an httptest JSON-RPC server:
    • The zero value and half-set basic credentials send no Authorization header.
    • Bearer is sent, basic auth is sent, and bearer wins over basic.
    • The header is present on every request, not just the first.
    • NewEthRPC on the authenticated client sends the header.
    • Formatting never leaks a secret, and dial errors do not leak the URL password or the token.
  • config/config_test.go: the defaults for the ETH, AVAX and ARB EVM RPC auth are empty.
  • evm/unstuck_test.go: the mock now echoes the batch request id instead of the hard-coded 5. Because the RPC client is shared, the id changed; THOR bumped the constant to 6 in the same commit.
  • Commands run, all green: go test -tags=mocknet on bifrost/pkg/chainclients/shared/evm/..., bifrost/pkg/chainclients/evm/..., bifrost/pkg/chainclients/ethereum/... and config/.

Operator notes

  • Credentials embedded in the URL keep working. When a header is configured it takes precedence, because Go's HTTP client only adds URL credentials when no Authorization header is set.
  • node-launcher needs no change. Operators set BIFROST_CHAINS_<CHAIN>_AUTHORIZATION_BEARER (or ..._USERNAME / ..._PASSWORD) only if they use an authenticated provider.

Review follow-up (0db523ac8)

  • TestEVMChainsDefaultToNoRPCAuth takes the EVM chains from common.GetEVMChains() and finds each block by chain_id, so a new EVM chain that inherits the UTXO credentials fails the test. BSC resets them in !886 (b88d70612).
  • The bearer token is trimmed, and a pasted Bearer prefix is stripped. Any other value that is not one visible-ASCII token fails the dial with an error that does not contain it.
  • RedactRPCHost masks the user info, path, query and fragment, not only the password.
  • If only one of username / password is set (and no bearer token), both clients log a warning.
  • New tests: WebSocket handshake; configured auth replacing URL user info (HTTP and WebSocket); env overrides through config.Init; special-character basic credentials; bearer normalization and rejection; redaction.

Review follow-up (round 2, dac1d0509)

  • With auth configured, DialEthClient passes go-ethereum its own http.Client. Its CheckRedirect refuses every redirect (errRPCRedirect), because Go re-sends Authorization on a same-host HTTPS to HTTP redirect. Configs without auth are unchanged.
  • An opaque host (alice:secret@rpc.example.com:8545, no scheme) is rejected before dialing, and RedactRPCHost returns <invalid url> for it.
  • With auth configured, non-2xx response bodies are cut to 64 KiB and the bearer token, the base64 basic value, the password and a user name of at least 4 bytes are replaced with xxxxx. 2xx bodies are not touched.
  • New tests: TestDialEthClientRefusesRedirectWithAuth, TestDialEthClientFollowsRedirectWithoutAuth, TestDialEthClientScrubsCredentialsFromErrorBody, TestScrubTransportCutsLongBody, and opaque-host cases in the redaction tests.
Edited by Bitol Maya

Merge request reports

Loading
Loading