diff --git a/core/scripts/go.mod b/core/scripts/go.mod index d4383bb6274..ec5bcfb4afa 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -45,7 +45,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.13 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 576f3339ecd..ca1f837539f 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1634,8 +1634,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 h1:NOUsjsMzNecbjiPWUQGlRSRAutEvCFrqqyETDJeh5q4= diff --git a/core/services/gateway/handler_factory.go b/core/services/gateway/handler_factory.go index 76172b3dc9b..c2338503eb7 100644 --- a/core/services/gateway/handler_factory.go +++ b/core/services/gateway/handler_factory.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers" "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/capabilities" v2 "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/capabilities/v2" + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/confidentialrelay" "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/functions" "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/vault" "github.com/smartcontractkit/chainlink/v2/core/services/gateway/network" @@ -25,11 +26,12 @@ import ( ) const ( - FunctionsHandlerType HandlerType = "functions" - DummyHandlerType HandlerType = "dummy" - WebAPICapabilitiesType HandlerType = "web-api-capabilities" // Handler for v0.1 HTTP capabilities for DAG workflows - HTTPCapabilityType HandlerType = "http-capabilities" // Handler for v1.0 HTTP capabilities for NoDAG workflows - VaultHandlerType HandlerType = "vault" + FunctionsHandlerType HandlerType = "functions" + DummyHandlerType HandlerType = "dummy" + WebAPICapabilitiesType HandlerType = "web-api-capabilities" // Handler for v0.1 HTTP capabilities for DAG workflows + HTTPCapabilityType HandlerType = "http-capabilities" // Handler for v1.0 HTTP capabilities for NoDAG workflows + VaultHandlerType HandlerType = "vault" + ConfidentialRelayHandlerType HandlerType = "confidential-compute-relay" ) type handlerFactory struct { @@ -87,6 +89,8 @@ func (hf *handlerFactory) NewHandler( case VaultHandlerType: requestAuthorizer := vaultcap.NewRequestAuthorizer(hf.lggr, hf.workflowRegistrySyncer) return vault.NewHandler(handlerConfig, donConfig, don, hf.capabilitiesRegistry, requestAuthorizer, hf.lggr, clockwork.NewRealClock(), hf.lf) + case ConfidentialRelayHandlerType: + return confidentialrelay.NewHandler(handlerConfig, donConfig, don, hf.lggr, clockwork.NewRealClock(), hf.lf) default: return nil, fmt.Errorf("unsupported handler type %s", handlerType) } diff --git a/core/services/gateway/handlers/confidentialrelay/aggregator.go b/core/services/gateway/handlers/confidentialrelay/aggregator.go new file mode 100644 index 00000000000..5eb00664571 --- /dev/null +++ b/core/services/gateway/handlers/confidentialrelay/aggregator.go @@ -0,0 +1,56 @@ +package confidentialrelay + +import ( + "encoding/json" + "errors" + "fmt" + + jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +var ( + errInsufficientResponsesForQuorum = errors.New("insufficient valid responses to reach quorum") + errQuorumUnobtainable = errors.New("quorum unobtainable") +) + +type aggregator struct{} + +func (a *aggregator) Aggregate(resps map[string]jsonrpc.Response[json.RawMessage], donF int, donMembersCount int, l logger.Logger) (*jsonrpc.Response[json.RawMessage], error) { + // F+1 (QuorumFPlusOne) is sufficient because each relay node calls the + // target DON (Vault or capability) through CRE's standard capability + // dispatch, which includes DON-level consensus. Every honest relay node + // receives the same consensus-aggregated response and performs deterministic + // translation, producing byte-identical outputs. F+1 matching responses + // therefore guarantees at least one honest node vouched for the result. + requiredQuorum := donF + 1 + + if len(resps) < requiredQuorum { + return nil, errInsufficientResponsesForQuorum + } + + shaToCount := map[string]int{} + maxShaToCount := 0 + for _, r := range resps { + sha, err := r.Digest() + if err != nil { + l.Errorw("failed to compute digest of response during quorum validation, skipping...", "error", err) + continue + } + shaToCount[sha]++ + if shaToCount[sha] > maxShaToCount { + maxShaToCount = shaToCount[sha] + } + if shaToCount[sha] >= requiredQuorum { + return &r, nil + } + } + + remainingResponses := donMembersCount - len(resps) + if maxShaToCount+remainingResponses < requiredQuorum { + l.Warnw("quorum unattainable for request", "requiredQuorum", requiredQuorum, "remainingResponses", remainingResponses, "maxShaToCount", maxShaToCount) + return nil, fmt.Errorf("%w: requiredQuorum=%d, maxShaToCount=%d, remainingResponses=%d", errQuorumUnobtainable, requiredQuorum, maxShaToCount, remainingResponses) + } + + return nil, errInsufficientResponsesForQuorum +} diff --git a/core/services/gateway/handlers/confidentialrelay/handler.go b/core/services/gateway/handlers/confidentialrelay/handler.go new file mode 100644 index 00000000000..3d99174da44 --- /dev/null +++ b/core/services/gateway/handlers/confidentialrelay/handler.go @@ -0,0 +1,445 @@ +package confidentialrelay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/jonboulle/clockwork" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "golang.org/x/sync/errgroup" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + relaytypes "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialrelay" + jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/api" + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/config" + gwhandlers "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers" +) + +const ( + defaultCleanUpPeriod = 5 * time.Second + + // Re-exported from chainlink-common for local use and test convenience. + MethodSecretsGet = relaytypes.MethodSecretsGet + MethodCapabilityExec = relaytypes.MethodCapabilityExec +) + +var _ gwhandlers.Handler = (*handler)(nil) + +type metrics struct { + requestInternalError metric.Int64Counter + requestUserError metric.Int64Counter + requestSuccess metric.Int64Counter +} + +func newMetrics() (*metrics, error) { + requestInternalError, err := beholder.GetMeter().Int64Counter("confidential_relay_gateway_request_internal_error") + if err != nil { + return nil, fmt.Errorf("failed to register internal error counter: %w", err) + } + + requestUserError, err := beholder.GetMeter().Int64Counter("confidential_relay_gateway_request_user_error") + if err != nil { + return nil, fmt.Errorf("failed to register user error counter: %w", err) + } + + requestSuccess, err := beholder.GetMeter().Int64Counter("confidential_relay_gateway_request_success") + if err != nil { + return nil, fmt.Errorf("failed to register success counter: %w", err) + } + + return &metrics{ + requestInternalError: requestInternalError, + requestUserError: requestUserError, + requestSuccess: requestSuccess, + }, nil +} + +type activeRequest struct { + req jsonrpc.Request[json.RawMessage] + responses map[string]*jsonrpc.Response[json.RawMessage] + mu sync.Mutex + + createdAt time.Time + gwhandlers.Callback +} + +func (ar *activeRequest) addResponseForNode(nodeAddr string, resp *jsonrpc.Response[json.RawMessage]) bool { + ar.mu.Lock() + defer ar.mu.Unlock() + _, exists := ar.responses[nodeAddr] + if exists { + return false + } + + ar.responses[nodeAddr] = resp + return true +} + +func (ar *activeRequest) copiedResponses() map[string]jsonrpc.Response[json.RawMessage] { + ar.mu.Lock() + defer ar.mu.Unlock() + copied := make(map[string]jsonrpc.Response[json.RawMessage], len(ar.responses)) + for k, response := range ar.responses { + var copiedResponse jsonrpc.Response[json.RawMessage] + if response != nil { + copiedResponse = *response + if response.Result != nil { + copiedResult := *response.Result + copiedResponse.Result = &copiedResult + } + if response.Error != nil { + copiedError := *response.Error + copiedResponse.Error = &copiedError + } + } + copied[k] = copiedResponse + } + return copied +} + +type relayAggregator interface { + Aggregate(resps map[string]jsonrpc.Response[json.RawMessage], donF int, donMembersCount int, l logger.Logger) (*jsonrpc.Response[json.RawMessage], error) +} + +type Config struct { + RequestTimeoutSec int `json:"requestTimeoutSec"` +} + +type handler struct { + services.StateMachine + donConfig *config.DONConfig + don gwhandlers.DON + codec api.JsonRPCCodec + lggr logger.Logger + mu sync.RWMutex + stopCh services.StopChan + + globalNodeRateLimiter limits.RateLimiter + perNodeRateLimiters map[string]limits.RateLimiter + requestTimeout time.Duration + + activeRequests map[string]*activeRequest + metrics *metrics + + aggregator relayAggregator + + clock clockwork.Clock +} + +func (h *handler) HealthReport() map[string]error { + return map[string]error{h.Name(): h.Healthy()} +} + +func (h *handler) Name() string { + return h.lggr.Name() +} + +func NewHandler(methodConfig json.RawMessage, donConfig *config.DONConfig, don gwhandlers.DON, lggr logger.Logger, clock clockwork.Clock, limitsFactory limits.Factory) (*handler, error) { + var cfg Config + if err := json.Unmarshal(methodConfig, &cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal method config: %w", err) + } + + if cfg.RequestTimeoutSec == 0 { + cfg.RequestTimeoutSec = 30 + } + + globalNodeRateLimiter, err := limitsFactory.MakeRateLimiter(cresettings.Default.GatewayConfidentialRelayGlobalRate) + if err != nil { + return nil, fmt.Errorf("failed to create global node rate limiter: %w", err) + } + + perNodeRateLimiters := make(map[string]limits.RateLimiter, len(donConfig.Members)) + for _, member := range donConfig.Members { + rl, makeErr := limitsFactory.MakeRateLimiter(cresettings.Default.GatewayConfidentialRelayPerNodeRate) + if makeErr != nil { + return nil, fmt.Errorf("failed to create per-node rate limiter for %s: %w", member.Address, makeErr) + } + perNodeRateLimiters[member.Address] = rl + } + + metrics, err := newMetrics() + if err != nil { + return nil, fmt.Errorf("failed to create metrics: %w", err) + } + + return &handler{ + donConfig: donConfig, + don: don, + lggr: logger.Named(lggr, "ConfidentialRelayHandler:"+donConfig.DonId), + requestTimeout: time.Duration(cfg.RequestTimeoutSec) * time.Second, + globalNodeRateLimiter: globalNodeRateLimiter, + perNodeRateLimiters: perNodeRateLimiters, + activeRequests: make(map[string]*activeRequest), + mu: sync.RWMutex{}, + stopCh: make(services.StopChan), + metrics: metrics, + aggregator: &aggregator{}, + clock: clock, + }, nil +} + +func (h *handler) Start(_ context.Context) error { + return h.StartOnce("ConfidentialRelayHandler", func() error { + h.lggr.Info("starting confidential relay handler") + go func() { + ctx, cancel := h.stopCh.NewCtx() + defer cancel() + ticker := h.clock.NewTicker(defaultCleanUpPeriod) + defer ticker.Stop() + for { + select { + case <-ticker.Chan(): + h.removeExpiredRequests(ctx) + case <-h.stopCh: + return + } + } + }() + return nil + }) +} + +func (h *handler) Close() error { + return h.StopOnce("ConfidentialRelayHandler", func() error { + h.lggr.Info("closing confidential relay handler") + close(h.stopCh) + var err error + if h.globalNodeRateLimiter != nil { + err = errors.Join(err, h.globalNodeRateLimiter.Close()) + } + for _, rl := range h.perNodeRateLimiters { + err = errors.Join(err, rl.Close()) + } + return err + }) +} + +func (h *handler) removeExpiredRequests(ctx context.Context) { + h.mu.RLock() + var expiredRequests []*activeRequest + now := h.clock.Now() + for _, userRequest := range h.activeRequests { + if now.Sub(userRequest.createdAt) > h.requestTimeout { + expiredRequests = append(expiredRequests, userRequest) + } + } + h.mu.RUnlock() + + for _, er := range expiredRequests { + responses := er.copiedResponses() + h.lggr.Debugw("request expired without quorum", "requestID", er.req.ID, "responseCount", len(responses), "required", h.donConfig.F+1) + err := h.sendResponseAndCleanup(ctx, er, h.constructErrorResponse(er.req, api.RequestTimeoutError, fmt.Errorf("request expired: got %d/%d responses", len(responses), h.donConfig.F+1))) + if err != nil { + h.lggr.Errorw("error sending response to user", "requestID", er.req.ID, "error", err) + } + } +} + +func (h *handler) Methods() []string { + return []string{MethodSecretsGet, MethodCapabilityExec} +} + +func (h *handler) HandleLegacyUserMessage(_ context.Context, _ *api.Message, _ gwhandlers.Callback) error { + return errors.New("confidential relay handler does not support legacy messages") +} + +func (h *handler) HandleJSONRPCUserMessage(ctx context.Context, req jsonrpc.Request[json.RawMessage], callback gwhandlers.Callback) error { + if req.ID == "" { + return errors.New("request ID cannot be empty") + } + if len(req.ID) > 200 { + return errors.New("request ID is too long: " + strconv.Itoa(len(req.ID)) + ". max is 200 characters") + } + + l := logger.With(h.lggr, "method", req.Method, "requestID", req.ID) + l.Debugw("handling confidential relay request") + + ar, err := h.newActiveRequest(req, callback) + if err != nil { + return err + } + + return h.fanOutToNodes(ctx, l, ar) +} + +func (h *handler) newActiveRequest(req jsonrpc.Request[json.RawMessage], callback gwhandlers.Callback) (*activeRequest, error) { + h.mu.Lock() + defer h.mu.Unlock() + if h.activeRequests[req.ID] != nil { + h.lggr.Errorw("request id already exists", "requestID", req.ID) + return nil, errors.New("request ID already exists: " + req.ID) + } + ar := &activeRequest{ + Callback: callback, + req: req, + createdAt: h.clock.Now(), + responses: map[string]*jsonrpc.Response[json.RawMessage]{}, + } + h.activeRequests[req.ID] = ar + return ar, nil +} + +func (h *handler) getActiveRequest(requestID string) *activeRequest { + h.mu.RLock() + defer h.mu.RUnlock() + return h.activeRequests[requestID] +} + +func (h *handler) HandleNodeMessage(ctx context.Context, resp *jsonrpc.Response[json.RawMessage], nodeAddr string) error { + l := logger.With(h.lggr, "method", resp.Method, "requestID", resp.ID, "nodeAddr", nodeAddr) + l.Debugw("handling node response") + + nodeRateLimiter, ok := h.perNodeRateLimiters[nodeAddr] + if !ok { + return fmt.Errorf("received message from unexpected node %s", nodeAddr) + } + if !nodeRateLimiter.Allow(ctx) { + l.Debugw("node is rate limited", "nodeAddr", nodeAddr) + return nil + } + if !h.globalNodeRateLimiter.Allow(ctx) { + l.Debug("global relay rate limit exceeded") + return nil + } + + ar := h.getActiveRequest(resp.ID) + if ar == nil { + l.Debugw("no pending request found for ID") + return nil + } + + added := ar.addResponseForNode(nodeAddr, resp) + if !added { + l.Errorw("duplicate response from node, ignoring", "nodeAddr", nodeAddr) + return nil + } + + copiedResponses := ar.copiedResponses() + aggregatedResp, err := h.aggregator.Aggregate(copiedResponses, h.donConfig.F, len(h.donConfig.Members), l) + switch { + case errors.Is(err, errInsufficientResponsesForQuorum): + l.Debugw("aggregating responses, waiting for other nodes...", "error", err) + return nil + case errors.Is(err, errQuorumUnobtainable): + l.Errorw("quorum unobtainable, returning error to user", "error", err) + return h.sendResponseAndCleanup(ctx, ar, h.constructErrorResponse(ar.req, api.FatalError, err)) + case err != nil: + l.Errorw("unexpected aggregation error", "error", err) + return h.sendResponseAndCleanup(ctx, ar, h.constructErrorResponse(ar.req, api.FatalError, err)) + } + + rawResponse, err := jsonrpc.EncodeResponse(aggregatedResp) + if err != nil { + h.lggr.Errorw("failed to encode response", "requestID", ar.req.ID, "error", err) + return h.sendResponseAndCleanup(ctx, ar, h.constructErrorResponse(ar.req, api.NodeReponseEncodingError, err)) + } + return h.sendResponseAndCleanup(ctx, ar, gwhandlers.UserCallbackPayload{ + RawResponse: rawResponse, + ErrorCode: api.NoError, + }) +} + +func (h *handler) fanOutToNodes(ctx context.Context, l logger.Logger, ar *activeRequest) error { + var ( + group errgroup.Group + nodeErrors atomic.Uint32 + ) + + for _, node := range h.donConfig.Members { + group.Go(func() error { + err := h.don.SendToNode(ctx, node.Address, &ar.req) + if err != nil { + nodeErrors.Add(1) + l.Errorw("error sending request to node", "node", node.Address, "error", err) + } + return nil + }) + } + + _ = group.Wait() + + numNodeErrors := nodeErrors.Load() + remainingPossibleResponses := len(h.donConfig.Members) - int(numNodeErrors) + if remainingPossibleResponses < h.donConfig.F+1 && numNodeErrors > 0 { + return h.sendResponseAndCleanup(ctx, ar, h.constructErrorResponse(ar.req, api.FatalError, errors.New("failed to forward user request to nodes"))) + } + + l.Debugw("successfully forwarded request to relay nodes") + return nil +} + +// sendResponseAndCleanup sends payload. +// The request is always removed from activeRequests +// regardless of whether the send succeeds, since a failed callback cannot +// be retried. +func (h *handler) sendResponseAndCleanup(ctx context.Context, ar *activeRequest, payload gwhandlers.UserCallbackPayload) error { + h.recordMetrics(ctx, payload.ErrorCode) + sendErr := ar.SendResponse(payload) + + h.mu.Lock() + delete(h.activeRequests, ar.req.ID) + h.mu.Unlock() + + if sendErr != nil { + h.lggr.Errorw("error sending response to user", "requestID", ar.req.ID, "error", sendErr) + return sendErr + } + + h.lggr.Debugw("response sent to user", "requestID", ar.req.ID, "errorCode", payload.ErrorCode) + return nil +} + +func (h *handler) recordMetrics(ctx context.Context, errorCode api.ErrorCode) { + //nolint:exhaustive // do not record other errors + switch errorCode { + case api.HandlerError: + h.metrics.requestInternalError.Add(ctx, 1, metric.WithAttributes( + attribute.String("don_id", h.donConfig.DonId), + attribute.String("error", errorCode.String()), + )) + case api.UnsupportedDONIdError: + h.metrics.requestUserError.Add(ctx, 1, metric.WithAttributes( + attribute.String("don_id", h.donConfig.DonId), + )) + case api.NoError: + h.metrics.requestSuccess.Add(ctx, 1, metric.WithAttributes( + attribute.String("don_id", h.donConfig.DonId), + )) + } +} + +func (h *handler) constructErrorResponse(req jsonrpc.Request[json.RawMessage], errorCode api.ErrorCode, err error) gwhandlers.UserCallbackPayload { + //nolint:exhaustive // do not modify other error codes + switch errorCode { + case api.NodeReponseEncodingError: + err = errors.New(errorCode.String()) + case api.InvalidParamsError: + err = fmt.Errorf("invalid params error: %w", err) + case api.UnsupportedMethodError: + err = fmt.Errorf("unsupported method(%s): %w", req.Method, err) + case api.UserMessageParseError: + err = fmt.Errorf("user message parse error: %w", err) + } + return gwhandlers.UserCallbackPayload{ + RawResponse: h.codec.EncodeNewErrorResponse( + req.ID, + api.ToJSONRPCErrorCode(errorCode), + err.Error(), + nil, + ), + ErrorCode: errorCode, + } +} diff --git a/core/services/gateway/handlers/confidentialrelay/handler_test.go b/core/services/gateway/handlers/confidentialrelay/handler_test.go new file mode 100644 index 00000000000..0cf3537405d --- /dev/null +++ b/core/services/gateway/handlers/confidentialrelay/handler_test.go @@ -0,0 +1,682 @@ +package confidentialrelay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" + + jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/api" + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/config" + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/common" + "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/mocks" +) + +type barrierDON struct { + total int + mu sync.Mutex + started int + allStarted chan struct{} + releaseOnce sync.Once +} + +func newBarrierDON(total int) *barrierDON { + return &barrierDON{ + total: total, + allStarted: make(chan struct{}), + } +} + +func (d *barrierDON) SendToNode(ctx context.Context, _ string, _ *jsonrpc.Request[json.RawMessage]) error { + d.mu.Lock() + d.started++ + if d.started == d.total { + d.releaseOnce.Do(func() { close(d.allStarted) }) + } + ch := d.allStarted + d.mu.Unlock() + + select { + case <-ch: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +var nodeOne = config.NodeConfig{ + Name: "node1", + Address: "0x1234", +} + +func setupHandler(t *testing.T, numNodes int) (*handler, *common.Callback, *mocks.DON, *clockwork.FakeClock) { + t.Helper() + lggr := logger.Test(t) + don := mocks.NewDON(t) + + members := make([]config.NodeConfig, numNodes) + for i := range numNodes { + members[i] = config.NodeConfig{ + Name: fmt.Sprintf("node%d", i), + Address: fmt.Sprintf("0x%04d", i), + } + } + + donConfig := &config.DONConfig{ + DonId: "test_relay_don", + F: 1, + Members: members, + } + handlerConfig := Config{ + RequestTimeoutSec: 30, + } + methodConfig, err := json.Marshal(handlerConfig) + require.NoError(t, err) + + clock := clockwork.NewFakeClock() + limitsFactory := limits.Factory{Settings: cresettings.DefaultGetter, Logger: lggr} + h, err := NewHandler(methodConfig, donConfig, don, lggr, clock, limitsFactory) + require.NoError(t, err) + h.aggregator = &mockAggregator{} + cb := common.NewCallback() + return h, cb, don, clock +} + +type mockAggregator struct { + err error +} + +func (m *mockAggregator) Aggregate(_ map[string]jsonrpc.Response[json.RawMessage], _ int, _ int, _ logger.Logger) (*jsonrpc.Response[json.RawMessage], error) { + return nil, m.err +} + +type respondingMockAggregator struct{} + +func (m *respondingMockAggregator) Aggregate(resps map[string]jsonrpc.Response[json.RawMessage], _ int, _ int, _ logger.Logger) (*jsonrpc.Response[json.RawMessage], error) { + if len(resps) == 0 { + return nil, errInsufficientResponsesForQuorum + } + // Return the first response we find. + for _, r := range resps { + return &r, nil + } + return nil, errInsufficientResponsesForQuorum +} + +func TestConfidentialRelayHandler_Methods(t *testing.T) { + h, _, _, _ := setupHandler(t, 4) + methods := h.Methods() + assert.Equal(t, []string{MethodSecretsGet, MethodCapabilityExec}, methods) +} + +func TestConfidentialRelayHandler_HandleLegacyUserMessage(t *testing.T) { + h, cb, _, _ := setupHandler(t, 4) + err := h.HandleLegacyUserMessage(t.Context(), nil, cb) + require.ErrorContains(t, err, "confidential relay handler does not support legacy messages") +} + +func TestConfidentialRelayHandler_RequestIDTooLong(t *testing.T) { + h, cb, _, _ := setupHandler(t, 4) + + longID := strings.Repeat("x", 201) + req := jsonrpc.Request[json.RawMessage]{ + ID: longID, + Method: MethodCapabilityExec, + } + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + expected := fmt.Sprintf("request ID is too long: %d. max is 200 characters", len(longID)) + require.EqualError(t, err, expected) +} + +func TestConfidentialRelayHandler_EmptyRequestID(t *testing.T) { + h, cb, _, _ := setupHandler(t, 4) + + req := jsonrpc.Request[json.RawMessage]{ + ID: "", + Method: MethodCapabilityExec, + } + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.EqualError(t, err, "request ID cannot be empty") +} + +func TestConfidentialRelayHandler_FanOutAndQuorumSuccess(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + h.aggregator = &respondingMockAggregator{} + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + params := json.RawMessage(`{"workflow_id":"wf1","secrets":[{"key":"k","namespace":"ns"}],"enclave_public_key":"pk"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-1", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + resultData := json.RawMessage(`{"secrets":[],"master_public_key":"mpk","threshold":1}`) + response := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-1", + Method: MethodCapabilityExec, + Result: &resultData, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.NoError, resp.ErrorCode) + var jsonResp jsonrpc.Response[json.RawMessage] + err = json.Unmarshal(resp.RawResponse, &jsonResp) + assert.NoError(t, err) + assert.Equal(t, "req-1", jsonResp.ID) + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + err = h.HandleNodeMessage(t.Context(), &response, "0x0000") + require.NoError(t, err) + wg.Wait() +} + +func TestConfidentialRelayHandler_QuorumWithRealAggregator(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + // Use the real aggregator; DON F=1 so quorum = F+1 = 2 + h.aggregator = &aggregator{} + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-quorum", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + resultData := json.RawMessage(`{"payload":"result"}`) + makeResp := func() *jsonrpc.Response[json.RawMessage] { + rd := make(json.RawMessage, len(resultData)) + copy(rd, resultData) + return &jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-quorum", + Method: MethodCapabilityExec, + Result: &rd, + } + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.NoError, resp.ErrorCode) + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + // Send 2 matching responses (F+1 = 2) + for i := range 2 { + err = h.HandleNodeMessage(t.Context(), makeResp(), fmt.Sprintf("0x%04d", i)) + require.NoError(t, err) + } + wg.Wait() +} + +func TestConfidentialRelayHandler_QuorumWithDivergentResponses(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + h.aggregator = &aggregator{} + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-diverge", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.NoError, resp.ErrorCode) + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + // One divergent response + divergentResult := json.RawMessage(`{"secrets":[],"master_public_key":"DIFFERENT","threshold":1}`) + divergentResp := &jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-diverge", + Method: MethodCapabilityExec, + Result: &divergentResult, + } + err = h.HandleNodeMessage(t.Context(), divergentResp, "0x0000") + require.NoError(t, err) + + // Two matching responses (quorum = F+1 = 2) + matchingResult := json.RawMessage(`{"secrets":[],"master_public_key":"mpk","threshold":1}`) + for i := 1; i <= 2; i++ { + rd := make(json.RawMessage, len(matchingResult)) + copy(rd, matchingResult) + resp := &jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-diverge", + Method: MethodCapabilityExec, + Result: &rd, + } + err = h.HandleNodeMessage(t.Context(), resp, fmt.Sprintf("0x%04d", i)) + require.NoError(t, err) + } + wg.Wait() +} + +func TestConfidentialRelayHandler_QuorumUnobtainable(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + h.aggregator = &mockAggregator{err: errQuorumUnobtainable} + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-unobtainable", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + response := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-unobtainable", + Method: MethodCapabilityExec, + Error: &jsonrpc.WireError{ + Code: -32603, + Message: errQuorumUnobtainable.Error(), + }, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + var jsonResp jsonrpc.Response[json.RawMessage] + err = json.Unmarshal(resp.RawResponse, &jsonResp) + assert.NoError(t, err) + assert.Equal(t, "req-unobtainable", jsonResp.ID) + assert.NotNil(t, jsonResp.Error) + assert.Contains(t, jsonResp.Error.Message, "quorum unobtainable") + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + err = h.HandleNodeMessage(t.Context(), &response, "0x0000") + require.NoError(t, err) + wg.Wait() +} + +func TestConfidentialRelayHandler_RequestTimeout(t *testing.T) { + h, cb, don, clock := setupHandler(t, 4) + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + // Use the real aggregator so responses are not immediately satisfied + h.aggregator = &aggregator{} + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-timeout", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.RequestTimeoutError, resp.ErrorCode) + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + // Advance clock past the request timeout and trigger cleanup + clock.Advance(31 * time.Second) + h.removeExpiredRequests(t.Context()) + wg.Wait() +} + +func TestConfidentialRelayHandler_DuplicateRequestID(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-dup", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + cb2 := common.NewCallback() + err = h.HandleJSONRPCUserMessage(t.Context(), req, cb2) + require.ErrorContains(t, err, "request ID already exists") +} + +func TestConfidentialRelayHandler_RateLimitedNode(t *testing.T) { + handlerConfig := Config{ + RequestTimeoutSec: 30, + } + methodConfig, err := json.Marshal(handlerConfig) + require.NoError(t, err) + + lggr := logger.Test(t) + don := mocks.NewDON(t) + donConfig := &config.DONConfig{ + DonId: "test_relay_don", + F: 1, + Members: []config.NodeConfig{nodeOne}, + } + clock := clockwork.NewFakeClock() + limitsFactory := limits.Factory{Settings: cresettings.DefaultGetter, Logger: lggr} + h, err := NewHandler(methodConfig, donConfig, don, lggr, clock, limitsFactory) + require.NoError(t, err) + h.aggregator = &respondingMockAggregator{} + h.globalNodeRateLimiter = limits.GlobalRateLimiter(rate.Limit(100), 100) + h.perNodeRateLimiters[nodeOne.Address] = limits.GlobalRateLimiter(rate.Limit(0.001), 1) + + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + cb := common.NewCallback() + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-ratelimit", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + err = h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + resultData := json.RawMessage(`{"secrets":[]}`) + response := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-ratelimit", + Method: MethodCapabilityExec, + Result: &resultData, + } + + // First response from node uses the burst allowance + err = h.HandleNodeMessage(t.Context(), &response, nodeOne.Address) + require.NoError(t, err) + + // Verify callback was called + ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) + defer cancel() + resp, err := cb.Wait(ctx) + require.NoError(t, err) + assert.Equal(t, api.NoError, resp.ErrorCode) + + // Start a new request + cb2 := common.NewCallback() + req2 := jsonrpc.Request[json.RawMessage]{ + ID: "req-ratelimit-2", + Method: MethodCapabilityExec, + Params: ¶ms, + } + err = h.HandleJSONRPCUserMessage(t.Context(), req2, cb2) + require.NoError(t, err) + + response2 := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-ratelimit-2", + Method: MethodCapabilityExec, + Result: &resultData, + } + + // Second response should be rate limited (silently dropped) + err = h.HandleNodeMessage(t.Context(), &response2, nodeOne.Address) + require.NoError(t, err) + + // Callback should NOT be called - verify with timeout + ctx2, cancel2 := context.WithTimeout(t.Context(), 50*time.Millisecond) + defer cancel2() + _, err = cb2.Wait(ctx2) + require.Error(t, err) // Should timeout +} + +func TestConfidentialRelayHandler_LateNodeResponse(t *testing.T) { + h, cb, _, _ := setupHandler(t, 4) + + resultData := json.RawMessage(`{"secrets":[]}`) + staleResponse := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "nonexistent-request", + Method: MethodCapabilityExec, + Result: &resultData, + } + + // This should not error, just silently ignore + err := h.HandleNodeMessage(t.Context(), &staleResponse, "0x0000") + require.NoError(t, err) + + // Verify callback was not triggered + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + _, err = cb.Wait(ctx) + require.Error(t, err) +} + +func TestConfidentialRelayHandler_AllNodesFanOutFail(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("connection refused")) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-allfail", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.FatalError, resp.ErrorCode) + var jsonResp jsonrpc.Response[json.RawMessage] + err = json.Unmarshal(resp.RawResponse, &jsonResp) + assert.NoError(t, err) + assert.Contains(t, jsonResp.Error.Message, "failed to forward user request to nodes") + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + wg.Wait() +} + +func TestConfidentialRelayHandler_FanOutWaitsWhileQuorumStillPossible(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return( + func(_ context.Context, nodeAddress string, _ *jsonrpc.Request[json.RawMessage]) error { + switch nodeAddress { + case "0x0000", "0x0001": + return errors.New("connection refused") + default: + return nil + } + }, + ) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-still-possible", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + require.NotNil(t, h.getActiveRequest(req.ID), "request should remain active while quorum is still possible") + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + _, err = cb.Wait(ctx) + require.Error(t, err) +} + +func TestConfidentialRelayHandler_FanOutFailsWhenQuorumBecomesImpossible(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return( + func(_ context.Context, nodeAddress string, _ *jsonrpc.Request[json.RawMessage]) error { + switch nodeAddress { + case "0x0000", "0x0001", "0x0002": + return errors.New("connection refused") + default: + return nil + } + }, + ) + + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-quorum-impossible", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.FatalError, resp.ErrorCode) + var jsonResp jsonrpc.Response[json.RawMessage] + err = json.Unmarshal(resp.RawResponse, &jsonResp) + assert.NoError(t, err) + assert.Contains(t, jsonResp.Error.Message, "failed to forward user request to nodes") + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + wg.Wait() + + require.Nil(t, h.getActiveRequest(req.ID), "request should be cleaned up once quorum is impossible") +} + +func TestConfidentialRelayHandler_FanOutToNodes_IsConcurrent(t *testing.T) { + lggr := logger.Test(t) + don := newBarrierDON(2) + donConfig := &config.DONConfig{ + DonId: "test_relay_don", + F: 1, + Members: []config.NodeConfig{ + {Name: "node0", Address: "0x0000"}, + {Name: "node1", Address: "0x0001"}, + }, + } + + methodConfig, err := json.Marshal(Config{ + RequestTimeoutSec: 30, + }) + require.NoError(t, err) + + limitsFactory := limits.Factory{Settings: cresettings.DefaultGetter, Logger: lggr} + h, err := NewHandler(methodConfig, donConfig, don, lggr, clockwork.NewFakeClock(), limitsFactory) + require.NoError(t, err) + + cb := common.NewCallback() + params := json.RawMessage(`{"workflow_id":"wf1"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-concurrent-fanout", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + done := make(chan error, 1) + go func() { + done <- h.HandleJSONRPCUserMessage(ctx, req, cb) + }() + + select { + case err := <-done: + require.NoError(t, err) + case <-time.After(100 * time.Millisecond): + cancel() + <-done + t.Fatal("HandleJSONRPCUserMessage did not fan out to nodes concurrently") + } + + don.mu.Lock() + started := don.started + don.mu.Unlock() + assert.Equal(t, 2, started) +} + +func TestConfidentialRelayHandler_CapabilityExecMethod(t *testing.T) { + h, cb, don, _ := setupHandler(t, 4) + h.aggregator = &respondingMockAggregator{} + don.On("SendToNode", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + params := json.RawMessage(`{"workflow_id":"wf1","capability_id":"cap1","payload":"data"}`) + req := jsonrpc.Request[json.RawMessage]{ + ID: "req-cap", + Method: MethodCapabilityExec, + Params: ¶ms, + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := cb.Wait(t.Context()) + assert.NoError(t, err) + assert.Equal(t, api.NoError, resp.ErrorCode) + }() + + err := h.HandleJSONRPCUserMessage(t.Context(), req, cb) + require.NoError(t, err) + + resultData := json.RawMessage(`{"payload":"result"}`) + response := jsonrpc.Response[json.RawMessage]{ + Version: jsonrpc.JsonRpcVersion, + ID: "req-cap", + Method: MethodCapabilityExec, + Result: &resultData, + } + err = h.HandleNodeMessage(t.Context(), &response, "0x0000") + require.NoError(t, err) + wg.Wait() + don.AssertCalled(t, "SendToNode", mock.Anything, mock.Anything, mock.Anything) +} diff --git a/deployment/cre/jobs/pkg/gateway_job.go b/deployment/cre/jobs/pkg/gateway_job.go index 950ebc79b41..3bc6828cac1 100644 --- a/deployment/cre/jobs/pkg/gateway_job.go +++ b/deployment/cre/jobs/pkg/gateway_job.go @@ -14,9 +14,11 @@ const ( GatewayHandlerTypeWebAPICapabilities = "web-api-capabilities" GatewayHandlerTypeHTTPCapabilities = "http-capabilities" GatewayHandlerTypeVault = "vault" + GatewayHandlerTypeConfidentialRelay = "confidential-compute-relay" - ServiceNameWorkflows = "workflows" - ServiceNameVault = "vault" + ServiceNameWorkflows = "workflows" + ServiceNameVault = "vault" + ServiceNameConfidential = "confidential" minimumRequestTimeoutSec = 5 ) @@ -28,6 +30,8 @@ func HandlerServiceName(handlerType string) string { return ServiceNameVault case GatewayHandlerTypeHTTPCapabilities, GatewayHandlerTypeWebAPICapabilities: return ServiceNameWorkflows + case GatewayHandlerTypeConfidentialRelay: + return ServiceNameConfidential default: return handlerType } @@ -226,6 +230,10 @@ func (g GatewayJob) buildLegacyDons() ([]legacyDON, error) { hs = append(hs, newDefaultVaultHandler(g.RequestTimeoutSec)) case GatewayHandlerTypeHTTPCapabilities: hs = append(hs, newDefaultHTTPCapabilitiesHandler()) + case GatewayHandlerTypeConfidentialRelay: + // -1 so the handler times out before the gateway, allowing a clean error response. + // TODO: the vault handler does the same -1 internally; unify both to use this pattern. + hs = append(hs, newDefaultConfidentialRelayHandler(g.RequestTimeoutSec-1)) default: return nil, errors.New("unknown handler type: " + ht) } @@ -266,6 +274,9 @@ func (g GatewayJob) buildServicesAndShardedDONs() ([]shardedDON, []service, erro handlers = append(handlers, newDefaultVaultHandler(g.RequestTimeoutSec)) case GatewayHandlerTypeHTTPCapabilities: handlers = append(handlers, newDefaultHTTPCapabilitiesHandler()) + case GatewayHandlerTypeConfidentialRelay: + // -1 so the handler times out before the gateway, allowing a clean error response. + handlers = append(handlers, newDefaultConfidentialRelayHandler(g.RequestTimeoutSec-1)) default: return nil, nil, errors.New("unknown handler type: " + ht) } @@ -307,8 +318,8 @@ type vaultHandlerConfig struct { func newDefaultVaultHandler(requestTimeoutSec int) handler { return handler{ - Name: "vault", - ServiceName: "vault", + Name: GatewayHandlerTypeVault, + ServiceName: ServiceNameVault, Config: vaultHandlerConfig{ // must be lower than the overall gateway request timeout. // so we allow for the response to be sent back. @@ -432,7 +443,7 @@ type httpCapabilitiesHandlerConfig struct { func newDefaultHTTPCapabilitiesHandler() handler { return handler{ Name: GatewayHandlerTypeHTTPCapabilities, - ServiceName: "workflows", + ServiceName: ServiceNameWorkflows, Config: httpCapabilitiesHandlerConfig{ CleanUpPeriodMs: 10 * 60 * 1000, // 10 minutes NodeRateLimiter: nodeRateLimiterConfig{ @@ -444,3 +455,17 @@ func newDefaultHTTPCapabilitiesHandler() handler { }, } } + +type confidentialRelayHandlerConfig struct { + RequestTimeoutSec int `toml:"requestTimeoutSec"` +} + +func newDefaultConfidentialRelayHandler(requestTimeoutSec int) handler { + return handler{ + Name: GatewayHandlerTypeConfidentialRelay, + ServiceName: ServiceNameConfidential, + Config: confidentialRelayHandlerConfig{ + RequestTimeoutSec: requestTimeoutSec, + }, + } +} diff --git a/deployment/cre/jobs/pkg/gateway_job_test.go b/deployment/cre/jobs/pkg/gateway_job_test.go index 90d438284b3..d10f56b2df1 100644 --- a/deployment/cre/jobs/pkg/gateway_job_test.go +++ b/deployment/cre/jobs/pkg/gateway_job_test.go @@ -30,6 +30,16 @@ func TestGateway_Validate_ServiceCentric(t *testing.T) { require.ErrorContains(t, g.Validate(), "must provide at least one service") } +func TestNewDefaultConfidentialRelayHandler(t *testing.T) { + t.Parallel() + + got := newDefaultConfidentialRelayHandler(14) + + assert.Equal(t, GatewayHandlerTypeConfidentialRelay, got.Name) + assert.Equal(t, ServiceNameConfidential, got.ServiceName) + assert.Equal(t, confidentialRelayHandlerConfig{RequestTimeoutSec: 14}, got.Config) +} + const ( expected = `type = 'gateway' schemaVersion = 1 diff --git a/deployment/go.mod b/deployment/go.mod index e84ed0b4b2b..550136bfd35 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -44,7 +44,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317175207-e9ff89561326 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260330133421-5151ea0c3b05 @@ -233,7 +233,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gagliardetto/utilz v0.1.3 // indirect diff --git a/deployment/go.sum b/deployment/go.sum index 96952eb652c..ee90c55a682 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -519,8 +519,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= @@ -1387,8 +1387,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317175207-e9ff github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317175207-e9ff89561326/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/go.mod b/go.mod index 85ee2f01b1a..5d54ffc1011 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/esote/minmaxheap v1.0.0 github.com/ethereum/go-ethereum v1.17.1 github.com/fatih/color v1.18.0 - github.com/fxamacker/cbor/v2 v2.7.0 + github.com/fxamacker/cbor/v2 v2.9.0 github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/solana-go v1.13.0 github.com/getsentry/sentry-go v0.27.0 @@ -85,7 +85,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 github.com/smartcontractkit/chainlink-data-streams v0.1.13 diff --git a/go.sum b/go.sum index 528e58b8358..468bdf0820d 100644 --- a/go.sum +++ b/go.sum @@ -425,8 +425,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= @@ -1235,8 +1235,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 0e76e93edb5..f92893bdfb1 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -38,7 +38,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260330133421-5151ea0c3b05 @@ -213,7 +213,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/anchor-go v1.0.0 // indirect github.com/gagliardetto/binary v0.8.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index fc524bc8e82..30491f0cda2 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -504,8 +504,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= @@ -1374,8 +1374,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 689db65f6a0..5dfdd0fa8e3 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -27,7 +27,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260330133421-5151ea0c3b05 github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.3 @@ -208,7 +208,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/anchor-go v1.0.0 // indirect github.com/gagliardetto/binary v0.8.0 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 8dae65f12b1..c25f5818c21 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -569,8 +569,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= @@ -1588,8 +1588,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 56f655f6b68..3ec3065cb5c 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -35,7 +35,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260330133421-5151ea0c3b05 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 20cce88dc92..2e7e1bf574e 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1601,8 +1601,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 4c2ef6c9094..bb7136e9246 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -62,7 +62,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.13 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 1481b3e768b..6df70e6640e 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1785,8 +1785,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843 h1:yzkeWzWoPTbpDvVIz0ohmNVqAkvE8UwuLqqcUt47gYk= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260331163339-a3c0d217e843/go.mod h1:6tlxlsiWypGdpaZI+Kz5gFm53gCAcU/pTU3PR9CiFB8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6 h1:oaXslIvcy5HD3zkWhx3nu8vRGdWGedYJ+XCsBD8mYkA= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260402120824-48154c0c65a6/go.mod h1:Ea94/OgfFPRTByGO2Qo+uZ7/4sWhwE2HKu7dDwdojME= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 h1:NOUsjsMzNecbjiPWUQGlRSRAutEvCFrqqyETDJeh5q4=