[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/DialEthClienthelper, 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}andDialEthClient(host, auth). The dial goes throughrpc.DialOptionswithrpc.WithHTTPAuth, so the header is sent on every HTTP request and on the WebSocket handshake.RedactRPCHostmasks a URL-embedded password.
shared/evm/eth_rpc.go:NewEthRPCnow 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.goandethereum/ethereum.gobuild theRPCAuthfrom config and dial through the helper.
What is logged
- At startup the clients log only the mode (
rpc_auth=none|bearer|basic). RPCAuthimplementsString/GoStringto return only the mode, so%v/%+v/%#vcannot 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 asuser:pass@host:port) is rejected before dialing, and the error does not contain it. - With auth configured, the client refuses HTTP redirects, so the
Authorizationheader 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 anhttptestJSON-RPC server:- The zero value and half-set basic credentials send no
Authorizationheader. - Bearer is sent, basic auth is sent, and bearer wins over basic.
- The header is present on every request, not just the first.
NewEthRPCon the authenticated client sends the header.- Formatting never leaks a secret, and dial errors do not leak the URL password or the token.
- The zero value and half-set basic credentials send no
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-coded5. Because the RPC client is shared, the id changed; THOR bumped the constant to6in the same commit.- Commands run, all green:
go test -tags=mocknetonbifrost/pkg/chainclients/shared/evm/...,bifrost/pkg/chainclients/evm/...,bifrost/pkg/chainclients/ethereum/...andconfig/.
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
Authorizationheader 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)
TestEVMChainsDefaultToNoRPCAuthtakes the EVM chains fromcommon.GetEVMChains()and finds each block bychain_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
Bearerprefix is stripped. Any other value that is not one visible-ASCII token fails the dial with an error that does not contain it. RedactRPCHostmasks 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,
DialEthClientpasses go-ethereum its ownhttp.Client. ItsCheckRedirectrefuses every redirect (errRPCRedirect), because Go re-sendsAuthorizationon 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, andRedactRPCHostreturns<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.