From 4442205d195ff43111d6e78d5cd9b6ad1579b63a Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Wed, 8 Apr 2026 21:01:57 -0700 Subject: [PATCH 1/2] fix: include chain ID audience claim in JWT created by create-api-key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verifier (aggregator/auth.go::verifyAuth, tightened in #509) requires the JWT to have an `aud` claim containing the chain ID string. But `CreateAdminKey` was building the JWT with only ExpiresAt, Issuer, Subject, and Roles. As a result, every key generated by the `create-api-key` CLI was rejected with "API key is invalid" — breaking ava-sdk-js's test:core CI which generates a fresh key per run. Dial the eth RPC inside CreateAdminKey to fetch the chain ID (NewAggregator does not run the lifecycle init that would populate agg.chainID), then add `Audience: jwt.ClaimStrings{chainID.String()}` to the registered claims. Discovered while debugging AvaProtocol/ava-sdk-js#209 CI failures. --- aggregator/key.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/aggregator/key.go b/aggregator/key.go index 8624cf5f..3b18dded 100644 --- a/aggregator/key.go +++ b/aggregator/key.go @@ -1,12 +1,14 @@ package aggregator import ( + "context" "fmt" "time" "github.com/AvaProtocol/EigenLayer-AVS/core/auth" "github.com/AvaProtocol/EigenLayer-AVS/core/config" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/golang-jwt/jwt/v5" ) @@ -44,11 +46,27 @@ func CreateAdminKey(configPath string, opt CreateApiKeyOption) error { roles[i] = auth.ApiRole(v) } + // The verifier (aggregator/auth.go::verifyAuth) requires the JWT to have an + // `aud` claim containing the chain ID string. NewAggregator does not run + // the lifecycle init that populates agg.chainID, so dial the RPC directly + // and read it here. Without this claim, every key generated by + // `create-api-key` is rejected with "API key is invalid". + rpcClient, err := ethclient.Dial(nodeConfig.EthHttpRpcUrl) + if err != nil { + return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err) + } + defer rpcClient.Close() + chainID, err := rpcClient.ChainID(context.Background()) + if err != nil { + return fmt.Errorf("failed to fetch chainId for audience claim: %w", err) + } + claims := &auth.APIClaim{ RegisteredClaims: &jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)), Issuer: "AvaProtocol", Subject: opt.Subject, + Audience: jwt.ClaimStrings{chainID.String()}, }, Roles: roles, } From 0b07f2861e40579d643b1e4c810dbca8d69351ec Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Wed, 8 Apr 2026 21:35:30 -0700 Subject: [PATCH 2/2] fix: address PR review feedback for create-api-key audience claim - Use nodeConfig.SmartWallet.ChainID directly instead of dialing the EigenLayer RPC. The verifier's r.chainID is sourced from the smart wallet RPC, so cross-chain configs (e.g. EigenLayer on Ethereum + SmartWallet on Base) would have failed with the previous approach. - Drops the extra RPC round-trip and the context-timeout concern entirely. - Use the auth.Issuer constant instead of the hard-coded "AvaProtocol" string. --- aggregator/key.go | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/aggregator/key.go b/aggregator/key.go index 3b18dded..5a63375b 100644 --- a/aggregator/key.go +++ b/aggregator/key.go @@ -1,14 +1,13 @@ package aggregator import ( - "context" "fmt" + "strconv" "time" "github.com/AvaProtocol/EigenLayer-AVS/core/auth" "github.com/AvaProtocol/EigenLayer-AVS/core/config" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" "github.com/golang-jwt/jwt/v5" ) @@ -47,26 +46,23 @@ func CreateAdminKey(configPath string, opt CreateApiKeyOption) error { } // The verifier (aggregator/auth.go::verifyAuth) requires the JWT to have an - // `aud` claim containing the chain ID string. NewAggregator does not run - // the lifecycle init that populates agg.chainID, so dial the RPC directly - // and read it here. Without this claim, every key generated by - // `create-api-key` is rejected with "API key is invalid". - rpcClient, err := ethclient.Dial(nodeConfig.EthHttpRpcUrl) - if err != nil { - return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err) - } - defer rpcClient.Close() - chainID, err := rpcClient.ChainID(context.Background()) - if err != nil { - return fmt.Errorf("failed to fetch chainId for audience claim: %w", err) + // `aud` claim containing the smart wallet chain ID. r.chainID in the + // verifier is sourced from the smart wallet RPC (see rpc_server.go), not + // the EigenLayer RPC, so we must use SmartWallet.ChainID here too — using + // the EigenLayer chain ID would silently break cross-chain configs (e.g. + // EigenLayer on Ethereum + SmartWallet on Base). config.NewConfig already + // populated SmartWallet.ChainID at startup, so no extra RPC dial is needed. + if nodeConfig.SmartWallet == nil || nodeConfig.SmartWallet.ChainID == 0 { + return fmt.Errorf("smart wallet chain ID not populated in config; cannot build audience claim") } + audienceChainID := strconv.FormatInt(nodeConfig.SmartWallet.ChainID, 10) claims := &auth.APIClaim{ RegisteredClaims: &jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)), - Issuer: "AvaProtocol", + Issuer: auth.Issuer, Subject: opt.Subject, - Audience: jwt.ClaimStrings{chainID.String()}, + Audience: jwt.ClaimStrings{audienceChainID}, }, Roles: roles, }