diff --git a/core/scripts/go.mod b/core/scripts/go.mod index c61677316b7..4c2f42a4891 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -46,7 +46,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260303102708-6caf8c4ea3b4 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 1243dad7b85..6746d968fe5 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1610,8 +1610,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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/ocr2/plugins/vault/kvstore_test.go b/core/services/ocr2/plugins/vault/kvstore_test.go index 38f6cc6684c..a98742f7122 100644 --- a/core/services/ocr2/plugins/vault/kvstore_test.go +++ b/core/services/ocr2/plugins/vault/kvstore_test.go @@ -42,8 +42,9 @@ func (k *kv) Write(key []byte, data []byte) error { } type blobber struct { - blobs [][]byte - cnt int + blobs [][]byte + cnt int + pendingIdx *int } func (b *blobber) BroadcastBlob(_ context.Context, data []byte, _ ocr3_1types.BlobExpirationHint) (ocr3_1types.BlobHandle, error) { @@ -52,11 +53,22 @@ func (b *blobber) BroadcastBlob(_ context.Context, data []byte, _ ocr3_1types.Bl } func (b *blobber) FetchBlob(_ context.Context, _ ocr3_1types.BlobHandle) ([]byte, error) { + if b.pendingIdx != nil { + return b.blobs[*b.pendingIdx], nil + } blob := b.blobs[b.cnt] b.cnt++ return blob, nil } +func (b *blobber) unmarshalBlob(data []byte) (ocr3_1types.BlobHandle, error) { + if len(data) > 0 { + idx := int(data[0]) + b.pendingIdx = &idx + } + return ocr3_1types.BlobHandle{}, nil +} + var _ (ocr3_1types.BlobBroadcastFetcher) = (*blobber)(nil) var _ (ocr3_1types.KeyValueReadWriter) = (*kv)(nil) diff --git a/core/services/ocr2/plugins/vault/plugin.go b/core/services/ocr2/plugins/vault/plugin.go index 6717548a82d..c701a30db30 100644 --- a/core/services/ocr2/plugins/vault/plugin.go +++ b/core/services/ocr2/plugins/vault/plugin.go @@ -80,11 +80,9 @@ type ReportingPluginConfig struct { MaxIdentifierKeyLengthBytes limits.BoundLimiter[pkgconfig.Size] MaxIdentifierOwnerLengthBytes limits.BoundLimiter[pkgconfig.Size] MaxIdentifierNamespaceLengthBytes limits.BoundLimiter[pkgconfig.Size] + MaxShareLengthBytes limits.BoundLimiter[pkgconfig.Size] + MaxRequestBatchSize limits.BoundLimiter[int] - // Note: BatchSize and MaxBatchSize are somewhat duplicated, - // with BatchSize being the default value, and MaxBatchSize being - // the limiter. - BatchSize settings.Setting[int] MaxBatchSize limits.BoundLimiter[int] } @@ -167,7 +165,7 @@ func (r *ReportingPluginFactory) makeSizeLimiter(defaultSize settings.Setting[pk defaultSize.DefaultValue = pkgconfig.Size(configSize) * pkgconfig.Byte } - return limits.MakeBoundLimiter[pkgconfig.Size](r.limitsFactory, defaultSize) + return limits.MakeUpperBoundLimiter[pkgconfig.Size](r.limitsFactory, defaultSize) } func logLimit[N limits.Number](ctx context.Context, lggr logger.Logger, limiter limits.BoundLimiter[N]) N { @@ -190,7 +188,7 @@ func (r *ReportingPluginFactory) NewReportingPlugin(ctx context.Context, config maxSecretsPerOwnerLimit.DefaultValue = int(configProto.MaxSecretsPerOwner) } - maxSecretsPerOwnerLimiter, err := limits.MakeBoundLimiter(r.limitsFactory, maxSecretsPerOwnerLimit) + maxSecretsPerOwnerLimiter, err := limits.MakeUpperBoundLimiter(r.limitsFactory, maxSecretsPerOwnerLimit) if err != nil { return nil, ocr3_1types.ReportingPluginInfo1{}, fmt.Errorf("could not create max secrets per owner limiter: %w", err) } @@ -200,7 +198,7 @@ func (r *ReportingPluginFactory) NewReportingPlugin(ctx context.Context, config batchSize.DefaultValue = int(configProto.BatchSize) } - maxBatchSizeLimiter, err := limits.MakeBoundLimiter(r.limitsFactory, batchSize) + maxBatchSizeLimiter, err := limits.MakeUpperBoundLimiter(r.limitsFactory, batchSize) if err != nil { return nil, ocr3_1types.ReportingPluginInfo1{}, fmt.Errorf("could not create max batch size limiter: %w", err) } @@ -225,6 +223,16 @@ func (r *ReportingPluginFactory) NewReportingPlugin(ctx context.Context, config return nil, ocr3_1types.ReportingPluginInfo1{}, fmt.Errorf("could not create default max identifier namespace length limiter: %w", err) } + maxShareLengthBytesLimiter, err := r.makeSizeLimiter(cresettings.Default.VaultShareSizeLimit, 0) + if err != nil { + return nil, ocr3_1types.ReportingPluginInfo1{}, fmt.Errorf("could not create default max share length bytes limiter: %w", err) + } + + maxRequestBatchSizeLimiter, err := limits.MakeUpperBoundLimiter(r.limitsFactory, cresettings.Default.VaultRequestBatchSizeLimit) + if err != nil { + return nil, ocr3_1types.ReportingPluginInfo1{}, fmt.Errorf("could not create default max request batch size limiter: %w", err) + } + if configProto.LimitsMaxQueryLength == 0 { configProto.LimitsMaxQueryLength = defaultLimitsMaxQueryLength } @@ -283,6 +291,8 @@ func (r *ReportingPluginFactory) NewReportingPlugin(ctx context.Context, config "maxIdentifierKeyLengthBytes", logLimit(ctx, r.lggr, maxIdentifierKeyLengthBytesLimiter), "maxIdentifierOwnerLengthBytes", logLimit(ctx, r.lggr, maxIdentifierOwnerLengthBytesLimiter), "maxIdentifierNamespaceLengthBytes", logLimit(ctx, r.lggr, maxIdentifierNamespaceLengthBytesLimiter), + "maxRequestBatchSize", logLimit(ctx, r.lggr, maxRequestBatchSizeLimiter), + "maxShareLengthBytes", logLimit(ctx, r.lggr, maxShareLengthBytesLimiter), "batchSize", logLimit(ctx, r.lggr, maxBatchSizeLimiter), ) @@ -290,11 +300,12 @@ func (r *ReportingPluginFactory) NewReportingPlugin(ctx context.Context, config PublicKey: publicKey, PrivateKeyShare: privateKeyShare, MaxSecretsPerOwner: maxSecretsPerOwnerLimiter, + MaxShareLengthBytes: maxShareLengthBytesLimiter, + MaxRequestBatchSize: maxRequestBatchSizeLimiter, MaxCiphertextLengthBytes: maxCiphertextLengthBytesLimiter, MaxIdentifierKeyLengthBytes: maxIdentifierKeyLengthBytesLimiter, MaxIdentifierOwnerLengthBytes: maxIdentifierOwnerLengthBytesLimiter, MaxIdentifierNamespaceLengthBytes: maxIdentifierNamespaceLengthBytesLimiter, - BatchSize: batchSize, MaxBatchSize: maxBatchSizeLimiter, } @@ -371,7 +382,8 @@ func (r *ReportingPlugin) Observation(ctx context.Context, seqNr uint64, aq type } // Avoid log spam by only logging if we have any requests to process. if len(batch) > 0 { - r.lggr.Debugw("observation started", "seqNr", seqNr, "batchSize", r.cfg.BatchSize) + mbs, _ := r.cfg.MaxBatchSize.Limit(ctx) + r.lggr.Debugw("observation started", "seqNr", seqNr, "batchSize", mbs) } ids := []string{} @@ -481,11 +493,16 @@ func (r *ReportingPlugin) Observation(ctx context.Context, seqNr uint64, aq type observedLocalQueue = append(observedLocalQueue, blobHandleBytes) - if len(observedLocalQueue) > 2*r.cfg.BatchSize.DefaultValue { + l, ierr2 := r.cfg.MaxBatchSize.Limit(ctx) + if ierr2 != nil { + return nil, fmt.Errorf("could not fetch max batch size limit: %w", ierr2) + } + + if len(observedLocalQueue) > 2*l { r.lggr.Warnw("Observed local queue exceeds batch size limit, truncating", "queueSize", len(observedLocalQueue), - "batchSizeLimit", 2*r.cfg.BatchSize.DefaultValue) - r.metrics.trackQueueOverflow(ctx, len(observedLocalQueue), r.cfg.BatchSize.DefaultValue) + "batchSizeLimit", 2*l) + r.metrics.trackQueueOverflow(ctx, len(observedLocalQueue), 2*l) break } } @@ -547,63 +564,85 @@ func (r *ReportingPlugin) observeGetSecrets(ctx context.Context, reader ReadKVSt } } -func (r *ReportingPlugin) observeGetSecretsRequest(ctx context.Context, reader ReadKVStore, secretRequest *vaultcommon.SecretRequest) (*vaultcommon.SecretResponse, error) { - id, err := r.validateSecretIdentifier(ctx, secretRequest.Id) +type share struct { + data []byte +} + +func (s *share) encryptWithKey(pk string) (string, error) { + publicKey, err := hex.DecodeString(pk) if err != nil { - return nil, err + return "", newUserError("failed to convert public key to bytes: " + err.Error()) } - secret, err := reader.GetSecret(id) - if err != nil { - return nil, fmt.Errorf("failed to read secret from key-value store: %w", err) + if len(publicKey) != curve25519.PointSize { + return "", newUserError(fmt.Sprintf("invalid public key size: expected %d bytes, got %d bytes", curve25519.PointSize, len(publicKey))) } - if secret == nil { - return nil, newUserError("key does not exist") + + publicKeyLength := [curve25519.PointSize]byte(publicKey) + encrypted, err := box.SealAnonymous(nil, s.data, &publicKeyLength, rand.Reader) + if err != nil { + return "", fmt.Errorf("failed to encrypt decryption share: %w", err) } + return hex.EncodeToString(encrypted), nil +} + +func generatePlaintextShare(publicKey *tdh2easy.PublicKey, privateKeyShare *tdh2easy.PrivateShare, encryptedSecret []byte, owner string) (*share, error) { ct := &tdh2easy.Ciphertext{} - err = ct.UnmarshalVerify(secret.EncryptedSecret, r.cfg.PublicKey) + err := ct.UnmarshalVerify(encryptedSecret, publicKey) if err != nil { return nil, fmt.Errorf("failed to unmarshal ciphertext: %w", err) } - encryptedSecret := hex.EncodeToString(secret.EncryptedSecret) - err = vaultcap.EnsureRightLabelOnSecret(r.cfg.PublicKey, encryptedSecret, secretRequest.Id.Owner) + es := hex.EncodeToString(encryptedSecret) + err = vaultcap.EnsureRightLabelOnSecret(publicKey, es, owner) if err != nil { return nil, errors.New("failed to verify label on secret. error: " + err.Error()) } - share, err := tdh2easy.Decrypt(ct, r.cfg.PrivateKeyShare) + s, err := tdh2easy.Decrypt(ct, privateKeyShare) if err != nil { return nil, fmt.Errorf("could not generate decryption share: %w", err) } - shareb, err := share.Marshal() + sb, err := s.Marshal() if err != nil { return nil, errors.New("could not marshal decryption share") } - shares := []*vaultcommon.EncryptedShares{} - for _, pk := range secretRequest.EncryptionKeys { - publicKey, err := hex.DecodeString(pk) - if err != nil { - return nil, newUserError("failed to convert public key to bytes: " + err.Error()) - } + return &share{data: sb}, nil +} - if len(publicKey) != curve25519.PointSize { - return nil, newUserError(fmt.Sprintf("invalid public key size: expected %d bytes, got %d bytes", curve25519.PointSize, len(publicKey))) - } +func (r *ReportingPlugin) observeGetSecretsRequest(ctx context.Context, reader ReadKVStore, secretRequest *vaultcommon.SecretRequest) (*vaultcommon.SecretResponse, error) { + id, err := r.validateSecretIdentifier(ctx, secretRequest.Id) + if err != nil { + return nil, err + } + + secret, err := reader.GetSecret(id) + if err != nil { + return nil, fmt.Errorf("failed to read secret from key-value store: %w", err) + } + if secret == nil { + return nil, newUserError("key does not exist") + } - publicKeyLength := [curve25519.PointSize]byte(publicKey) - encrypted, err := box.SealAnonymous(nil, shareb, &publicKeyLength, rand.Reader) + sh, err := generatePlaintextShare(r.cfg.PublicKey, r.cfg.PrivateKeyShare, secret.EncryptedSecret, secretRequest.Id.Owner) + if err != nil { + return nil, err + } + + shares := []*vaultcommon.EncryptedShares{} + for _, pk := range secretRequest.EncryptionKeys { + encShare, err := sh.encryptWithKey(pk) if err != nil { - return nil, fmt.Errorf("failed to encrypt decryption share: %w", err) + return nil, err } shares = append(shares, &vaultcommon.EncryptedShares{ EncryptionKey: pk, Shares: []string{ - hex.EncodeToString(encrypted), + encShare, }, }) } @@ -612,7 +651,7 @@ func (r *ReportingPlugin) observeGetSecretsRequest(ctx context.Context, reader R Id: id, Result: &vaultcommon.SecretResponse_Data{ Data: &vaultcommon.SecretData{ - EncryptedValue: encryptedSecret, + EncryptedValue: hex.EncodeToString(secret.EncryptedSecret), EncryptedDecryptionKeyShares: shares, }, }, @@ -684,23 +723,11 @@ func (r *ReportingPlugin) observeCreateSecretRequest(ctx context.Context, reader return id, newUserError("duplicate request for secret identifier " + vaulttypes.KeyFor(id)) } - rawCiphertext := secretRequest.EncryptedValue - rawCiphertextB, err := hex.DecodeString(rawCiphertext) - if err != nil { - return id, newUserError("invalid hex encoding for ciphertext: " + err.Error()) + if ierr := r.validateCiphertextSize(ctx, secretRequest.Id.Owner, secretRequest.EncryptedValue); ierr != nil { + return id, newUserError(ierr.Error()) } - ctx = contexts.WithCRE(ctx, contexts.CRE{Owner: secretRequest.Id.Owner}) - if ierr := r.cfg.MaxCiphertextLengthBytes.Check(ctx, pkgconfig.Size(len(rawCiphertextB))*pkgconfig.Byte); ierr != nil { - var errBoundLimited limits.ErrorBoundLimited[pkgconfig.Size] - if errors.As(ierr, &errBoundLimited) { - return id, newUserError(fmt.Sprintf("ciphertext size exceeds maximum allowed size: %s", errBoundLimited.Limit)) - } - return id, newUserError("failed to check ciphertext size limit: " + ierr.Error()) - } - - ct := &tdh2easy.Ciphertext{} - err = ct.UnmarshalVerify(rawCiphertextB, r.cfg.PublicKey) + err = vaultcap.EnsureRightLabelOnSecret(r.cfg.PublicKey, secretRequest.EncryptedValue, secretRequest.Id.Owner) if err != nil { return id, newUserError("failed to verify ciphertext: " + err.Error()) } @@ -999,7 +1026,7 @@ func (r *ReportingPlugin) ValidateObservation(ctx context.Context, seqNr uint64, idToObs := map[string]*vaultcommon.Observation{} for _, o := range obs.Observations { - err := validateObservation(o) + err := r.validateObservation(ctx, o) if err != nil { return errors.New("invalid observation: " + err.Error()) } @@ -1034,6 +1061,18 @@ func (r *ReportingPlugin) ValidateObservation(ctx context.Context, seqNr uint64, } } + l, err := r.cfg.MaxBatchSize.Limit(ctx) + if err != nil { + return fmt.Errorf("could not fetch max batch size limit: %w", err) + } + + // The Observation method enforces a max pending queue batch size of 2x the batch size. + // We can therefore reject any observation with a higher number of observations as invalid. + maxBatchSize := 2 * l + if len(obs.PendingQueueItems) > maxBatchSize { + return fmt.Errorf("invalid observation: too many pending queue items provided, have %d, want max %d", len(obs.PendingQueueItems), maxBatchSize) + } + seen := map[string]bool{} for _, i := range obs.PendingQueueItems { bh, err := r.unmarshalBlob(i) @@ -1087,86 +1126,271 @@ func shaForObservation(o *vaultcommon.Observation) (string, error) { } } -func validateObservation(o *vaultcommon.Observation) error { +func (r *ReportingPlugin) checkRequestBatchLimit(ctx context.Context, batchSize int) error { + if err := r.cfg.MaxRequestBatchSize.Check(ctx, batchSize); err != nil { + var errBoundLimited limits.ErrorBoundLimited[int] + if errors.As(err, &errBoundLimited) { + return fmt.Errorf("max batch size exceeded for request: %w", err) + } + // Fail closed here: this could cause a loss of liveness but + // the current implementation would only return an error that's + // not a ErrorBoundLimited if the limiter has been closed. + return errors.New("failed to check batch size") + } + + return nil +} + +func (r *ReportingPlugin) validateObservation(ctx context.Context, o *vaultcommon.Observation) error { if o.Id == "" { return errors.New("observation id cannot be empty") } switch o.RequestType { case vaultcommon.RequestType_GET_SECRETS: - if o.GetGetSecretsRequest() == nil || o.GetGetSecretsResponse() == nil { - return errors.New("GetSecrets observation must have both request and response") + return r.validateGetSecretsObservation(ctx, o) + case vaultcommon.RequestType_CREATE_SECRETS: + return r.validateCreateSecretsObservation(ctx, o) + case vaultcommon.RequestType_UPDATE_SECRETS: + return r.validateUpdateSecretsObservation(ctx, o) + case vaultcommon.RequestType_DELETE_SECRETS: + return r.validateDeleteSecretsObservation(ctx, o) + case vaultcommon.RequestType_LIST_SECRET_IDENTIFIERS: + return r.validateListSecretIdentifiersObservation(ctx, o) + default: + return errors.New("invalid observation type: " + o.RequestType.String()) + } +} + +func (r *ReportingPlugin) validateGetSecretsObservation(ctx context.Context, o *vaultcommon.Observation) error { + if o.GetGetSecretsRequest() == nil || o.GetGetSecretsResponse() == nil { + return errors.New("GetSecrets observation must have both request and response") + } + + if err := r.checkRequestBatchLimit(ctx, len(o.GetGetSecretsRequest().Requests)); err != nil { + return err + } + + if len(o.GetGetSecretsRequest().Requests) != len(o.GetGetSecretsResponse().Responses) { + return errors.New("GetSecrets request and response must have the same number of items") + } + + // check for that we have an entry per encrypted key in the request + // we should have max 1 share per observation per encrypted key + req, resp := o.GetGetSecretsRequest(), o.GetGetSecretsResponse() + reqMap := map[string]*vaultcommon.SecretRequest{} + for _, r := range req.Requests { + if r.Id == nil { + return errors.New("GetSecrets request contains nil secret identifier") } + key := vaulttypes.KeyFor(r.Id) + if _, ok := reqMap[key]; ok { + return fmt.Errorf("duplicate request found for item %s", key) + } + reqMap[key] = r + } - if len(o.GetGetSecretsRequest().Requests) != len(o.GetGetSecretsResponse().Responses) { - return errors.New("GetSecrets request and response must have the same number of items") + respMap := map[string]*vaultcommon.SecretResponse{} + for _, r := range resp.Responses { + if r.Id == nil { + return errors.New("GetSecrets response contains nil secret identifier") } - case vaultcommon.RequestType_CREATE_SECRETS: - if o.GetCreateSecretsRequest() == nil || o.GetCreateSecretsResponse() == nil { - return errors.New("CreateSecrets observation must have both request and response") + key := vaulttypes.KeyFor(r.Id) + if _, ok := respMap[key]; ok { + return fmt.Errorf("duplicate response found for item %s", key) } + respMap[key] = r + } - if len(o.GetCreateSecretsRequest().EncryptedSecrets) != len(o.GetCreateSecretsResponse().Responses) { - return errors.New("CreateSecrets request and response must have the same number of items") + if len(reqMap) != len(respMap) { + return errors.New("observation doesn't contain matching number of requests and responses") + } + + for _, rq := range reqMap { + key := vaulttypes.KeyFor(rq.Id) + rsp, ok := respMap[key] + if !ok { + return fmt.Errorf("missing response for request with id %s", key) } - // We disallow duplicate create requests within a single batch request. - // This prevents users from clobbering their own writes. - idSet := map[string]bool{} - for _, r := range o.GetCreateSecretsRequest().EncryptedSecrets { - _, ok := idSet[vaulttypes.KeyFor(r.Id)] - if ok { - return fmt.Errorf("CreateSecrets requests cannot contain duplicate request for a given secret identifier: %s", r.Id) + d := rsp.GetData() + if d != nil { + decryptionShares := d.GetEncryptedDecryptionKeyShares() + if len(rq.EncryptionKeys) != len(d.GetEncryptedDecryptionKeyShares()) { + return errors.New("observation must contain a share per encryption key provided") } - idSet[vaulttypes.KeyFor(r.Id)] = true + innerCtx := contexts.WithCRE(ctx, contexts.CRE{Owner: rq.Id.Owner}) + for _, ds := range decryptionShares { + if len(ds.Shares) != 1 { + return errors.New("observation must have exactly 1 share per encryption key") + } + + share := ds.Shares[0] + if err := r.cfg.MaxShareLengthBytes.Check(innerCtx, pkgconfig.Size(len(share))*pkgconfig.Byte); err != nil { + var errBoundLimited limits.ErrorBoundLimited[pkgconfig.Size] + if errors.As(err, &errBoundLimited) { + return fmt.Errorf("share provided exceeds maximum size allowed: %w", err) + } + return errors.New("failed to check share size") + } + } } - case vaultcommon.RequestType_UPDATE_SECRETS: - if o.GetUpdateSecretsRequest() == nil || o.GetUpdateSecretsResponse() == nil { - return errors.New("UpdateSecrets observation must have both request and response") + } + + return nil +} + +func (r *ReportingPlugin) validateCiphertextSize(ctx context.Context, owner string, encryptedValue string) error { + rawCiphertextB, err := hex.DecodeString(encryptedValue) + if err != nil { + return fmt.Errorf("invalid hex encoding for ciphertext: %w", err) + } + innerCtx := contexts.WithCRE(ctx, contexts.CRE{Owner: owner}) + if err := r.cfg.MaxCiphertextLengthBytes.Check(innerCtx, pkgconfig.Size(len(rawCiphertextB))*pkgconfig.Byte); err != nil { + var errBoundLimited limits.ErrorBoundLimited[pkgconfig.Size] + if errors.As(err, &errBoundLimited) { + return fmt.Errorf("ciphertext size exceeds maximum allowed size: %s", errBoundLimited.Limit) } + return errors.New("failed to check ciphertext size") + } + return nil +} + +func (r *ReportingPlugin) validateCreateSecretsObservation(ctx context.Context, o *vaultcommon.Observation) error { + if o.GetCreateSecretsRequest() == nil || o.GetCreateSecretsResponse() == nil { + return errors.New("CreateSecrets observation must have both request and response") + } + + if err := r.checkRequestBatchLimit(ctx, len(o.GetCreateSecretsRequest().EncryptedSecrets)); err != nil { + return err + } + + if len(o.GetCreateSecretsRequest().EncryptedSecrets) != len(o.GetCreateSecretsResponse().Responses) { + return errors.New("CreateSecrets request and response must have the same number of items") + } - if len(o.GetUpdateSecretsRequest().EncryptedSecrets) != len(o.GetUpdateSecretsResponse().Responses) { - return errors.New("UpdateSecrets request and response must have the same number of items") + // We disallow duplicate create requests within a single batch request. + // This prevents users from clobbering their own writes. + idSet := map[string]bool{} + for _, s := range o.GetCreateSecretsRequest().EncryptedSecrets { + if s.Id == nil { + return errors.New("CreateSecrets request contains nil secret identifier") + } + _, ok := idSet[vaulttypes.KeyFor(s.Id)] + if ok { + return fmt.Errorf("CreateSecrets requests cannot contain duplicate request for a given secret identifier: %s", s.Id) } - // We disallow duplicate update requests within a single batch request. - // This prevents users from clobbering their own writes. - idSet := map[string]bool{} - for _, r := range o.GetUpdateSecretsRequest().EncryptedSecrets { - _, ok := idSet[vaulttypes.KeyFor(r.Id)] - if ok { - return fmt.Errorf("UpdateSecrets requests cannot contain duplicate request for a given secret identifier: %s", r.Id) - } + idSet[vaulttypes.KeyFor(s.Id)] = true - idSet[vaulttypes.KeyFor(r.Id)] = true + if err := r.validateCiphertextSize(ctx, s.Id.Owner, s.EncryptedValue); err != nil { + return fmt.Errorf("CreateSecrets request: %w", err) } - case vaultcommon.RequestType_DELETE_SECRETS: - if o.GetDeleteSecretsRequest() == nil || o.GetDeleteSecretsResponse() == nil { - return errors.New("DeleteSecrets observation must have both request and response") + } + + for _, r := range o.GetCreateSecretsResponse().Responses { + if r.Id == nil { + return errors.New("CreateSecrets response contains nil secret identifier") + } + } + + return nil +} + +func (r *ReportingPlugin) validateUpdateSecretsObservation(ctx context.Context, o *vaultcommon.Observation) error { + if o.GetUpdateSecretsRequest() == nil || o.GetUpdateSecretsResponse() == nil { + return errors.New("UpdateSecrets observation must have both request and response") + } + + if err := r.checkRequestBatchLimit(ctx, len(o.GetUpdateSecretsRequest().EncryptedSecrets)); err != nil { + return err + } + + if len(o.GetUpdateSecretsRequest().EncryptedSecrets) != len(o.GetUpdateSecretsResponse().Responses) { + return errors.New("UpdateSecrets request and response must have the same number of items") + } + + // We disallow duplicate update requests within a single batch request. + // This prevents users from clobbering their own writes. + idSet := map[string]bool{} + for _, s := range o.GetUpdateSecretsRequest().EncryptedSecrets { + if s.Id == nil { + return errors.New("UpdateSecrets request contains nil secret identifier") + } + _, ok := idSet[vaulttypes.KeyFor(s.Id)] + if ok { + return fmt.Errorf("UpdateSecrets requests cannot contain duplicate request for a given secret identifier: %s", s.Id) } - if len(o.GetDeleteSecretsRequest().Ids) != len(o.GetDeleteSecretsResponse().Responses) { - return errors.New("DeleteSecrets request and response must have the same number of items") + idSet[vaulttypes.KeyFor(s.Id)] = true + + if err := r.validateCiphertextSize(ctx, s.Id.Owner, s.EncryptedValue); err != nil { + return fmt.Errorf("UpdateSecrets request: %w", err) } + } - // We disallow duplicate delete requests within a single batch request. - // This prevents users from clobbering their own writes. - idSet := map[string]bool{} - for _, r := range o.GetDeleteSecretsRequest().Ids { - _, ok := idSet[vaulttypes.KeyFor(r)] - if ok { - return fmt.Errorf("DeleteSecrets requests cannot contain duplicate request for a given secret identifier: %s", r) - } + for _, r := range o.GetUpdateSecretsResponse().Responses { + if r.Id == nil { + return errors.New("UpdateSecrets response contains nil secret identifier") + } + } + + return nil +} - idSet[vaulttypes.KeyFor(r)] = true +func (r *ReportingPlugin) validateDeleteSecretsObservation(ctx context.Context, o *vaultcommon.Observation) error { + if o.GetDeleteSecretsRequest() == nil || o.GetDeleteSecretsResponse() == nil { + return errors.New("DeleteSecrets observation must have both request and response") + } + + if err := r.checkRequestBatchLimit(ctx, len(o.GetDeleteSecretsRequest().Ids)); err != nil { + return err + } + + if len(o.GetDeleteSecretsRequest().Ids) != len(o.GetDeleteSecretsResponse().Responses) { + return errors.New("DeleteSecrets request and response must have the same number of items") + } + + // We disallow duplicate delete requests within a single batch request. + // This prevents users from clobbering their own writes. + idSet := map[string]bool{} + for _, r := range o.GetDeleteSecretsRequest().Ids { + if r == nil { + return errors.New("DeleteSecrets request contains nil secret identifier") } - case vaultcommon.RequestType_LIST_SECRET_IDENTIFIERS: - if o.GetListSecretIdentifiersRequest() == nil || o.GetListSecretIdentifiersResponse() == nil { - return errors.New("ListSecretIdentifiers observation must have both request and response") + _, ok := idSet[vaulttypes.KeyFor(r)] + if ok { + return fmt.Errorf("DeleteSecrets requests cannot contain duplicate request for a given secret identifier: %s", r) + } + + idSet[vaulttypes.KeyFor(r)] = true + } + + for _, r := range o.GetDeleteSecretsResponse().Responses { + if r.Id == nil { + return errors.New("DeleteSecrets response contains nil secret identifier") + } + } + + return nil +} + +func (r *ReportingPlugin) validateListSecretIdentifiersObservation(ctx context.Context, o *vaultcommon.Observation) error { + if o.GetListSecretIdentifiersRequest() == nil || o.GetListSecretIdentifiersResponse() == nil { + return errors.New("ListSecretIdentifiers observation must have both request and response") + } + + resp := o.GetListSecretIdentifiersResponse() + if resp.Success { + ctx = contexts.WithCRE(ctx, contexts.CRE{Owner: o.GetListSecretIdentifiersRequest().Owner}) + if err := r.cfg.MaxSecretsPerOwner.Check(ctx, len(resp.Identifiers)); err != nil { + var errBoundLimited limits.ErrorBoundLimited[int] + if errors.As(err, &errBoundLimited) { + return fmt.Errorf("ListSecretIdentifiers response exceeds maximum number of secrets per owner (have=%d, limit=%d)", len(resp.Identifiers), errBoundLimited.Limit) + } + return fmt.Errorf("failed to check max secrets per owner limit: %w", err) } - default: - return errors.New("invalid observation type: " + o.RequestType.String()) } return nil @@ -1250,7 +1474,7 @@ func (r *ReportingPlugin) StateTransition(ctx context.Context, seqNr uint64, aq case o.RequestType != vaultcommon.RequestType_GET_SECRETS && len(obs) >= r.onchainCfg.F+1: // F+1 means that at least 1 honest node has provided this observation, so that's enough for all other request // types. - // Technically we could have two shas with F+1 observations. If that happens we'll pick the first one. + // Technically we could have two shas with F+1 observations. If that happens we'll pick the last one. // This is deterministic since we're sorting by shas above. chosen = shaToObs[sha] r.lggr.Debugw("sufficient observations for sha", "sha", sha, "count", len(obs), "threshold", r.onchainCfg.F+1, "id", id) @@ -1481,9 +1705,26 @@ func (r *ReportingPlugin) stateTransitionGetSecrets(ctx context.Context, chosen keyToShares[s.EncryptionKey] = s } + innerCtx := contexts.WithCRE(ctx, contexts.CRE{Owner: rsp.Id.Owner}) for _, existing := range rsp.GetData().EncryptedDecryptionKeyShares { + if len(existing.Shares) != 1 { + // This should not happen because we validate against this in ValidateObservation. + r.lggr.Errorw("exactly 1 share must be provided in the response, skipping", "id", rsp.Id) + continue + } + share := existing.Shares[0] + if err := r.cfg.MaxShareLengthBytes.Check(innerCtx, pkgconfig.Size(len(share))*pkgconfig.Byte); err != nil { + var errBoundLimited limits.ErrorBoundLimited[pkgconfig.Size] + if errors.As(err, &errBoundLimited) { + r.lggr.Errorw("share exceeds max allowed size, skipping...", "id", rsp.Id, "encryptionKey", existing.EncryptionKey, "err", err) + } else { + r.lggr.Errorw("could not check max allowed share size, skipping...", "id", rsp.Id, "encryptionKey", existing.EncryptionKey, "err", err) + } + continue + } + if shares, ok := keyToShares[existing.EncryptionKey]; ok { - shares.Shares = append(shares.Shares, existing.Shares...) + shares.Shares = append(shares.Shares, share) } else { // This shouldn't happen -- this is because we're aggregating // requests that have a matching sha (excluding the decryption share). diff --git a/core/services/ocr2/plugins/vault/plugin_test.go b/core/services/ocr2/plugins/vault/plugin_test.go index 78731be9b86..c3c78ea7b4c 100644 --- a/core/services/ocr2/plugins/vault/plugin_test.go +++ b/core/services/ocr2/plugins/vault/plugin_test.go @@ -3,6 +3,8 @@ package vault import ( "crypto/rand" "encoding/hex" + "fmt" + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -92,7 +94,7 @@ func TestPlugin_ReportingPluginFactory_UsesDefaultsIfNotProvidedInOffchainConfig require.NoError(t, err) typedRP := rp.(*ReportingPlugin) - assert.Equal(t, 20, typedRP.cfg.BatchSize.DefaultValue) + assertLimit(t, 20, typedRP.cfg.MaxBatchSize) assert.NotNil(t, typedRP.cfg.PublicKey) assert.NotNil(t, typedRP.cfg.PrivateKeyShare) assertLimit(t, 100, typedRP.cfg.MaxSecretsPerOwner) @@ -135,7 +137,7 @@ func TestPlugin_ReportingPluginFactory_UsesDefaultsIfNotProvidedInOffchainConfig require.NoError(t, err) typedRP = rp.(*ReportingPlugin) - assert.Equal(t, 2, typedRP.cfg.BatchSize.DefaultValue) + assertLimit(t, 2, typedRP.cfg.MaxBatchSize) assertLimit(t, 2, typedRP.cfg.MaxSecretsPerOwner) assertLimit(t, 2, typedRP.cfg.MaxCiphertextLengthBytes) assertLimit(t, 2, typedRP.cfg.MaxCiphertextLengthBytes) @@ -186,7 +188,7 @@ func TestPlugin_ReportingPluginFactory_UseDKGResult(t *testing.T) { require.NoError(t, err) typedRP := rp.(*ReportingPlugin) - assert.Equal(t, 20, typedRP.cfg.BatchSize.DefaultValue) + assertLimit(t, 20, typedRP.cfg.MaxBatchSize) pkBytes, err := typedRP.cfg.PublicKey.Marshal() require.NoError(t, err) @@ -237,36 +239,44 @@ func makeReportingPluginConfig( maxIdentifierOwnerLengthBytes int, maxIdentifierNamespaceOwnerLengthBytes int, maxIdentifierKeyLengthBytes int, + maxRequestBatchSize int, ) *ReportingPluginConfig { - msl, err := limits.MakeBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Int(maxSecretsPerOwner)) + msl, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Int(maxSecretsPerOwner)) require.NoError(t, err) - cipherTextLimiter, err := limits.MakeBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxCipherTextLengthBytes)*pkgconfig.Byte)) + cipherTextLimiter, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxCipherTextLengthBytes)*pkgconfig.Byte)) require.NoError(t, err) - ownerLimiter, err := limits.MakeBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxIdentifierOwnerLengthBytes)*pkgconfig.Byte)) + shareLimiter, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, cresettings.Default.VaultShareSizeLimit) require.NoError(t, err) - namespaceOwnerLimiter, err := limits.MakeBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxIdentifierNamespaceOwnerLengthBytes)*pkgconfig.Byte)) + ownerLimiter, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxIdentifierOwnerLengthBytes)*pkgconfig.Byte)) require.NoError(t, err) - keyLimiter, err := limits.MakeBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxIdentifierKeyLengthBytes)*pkgconfig.Byte)) + namespaceOwnerLimiter, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxIdentifierNamespaceOwnerLengthBytes)*pkgconfig.Byte)) require.NoError(t, err) - bsl, err := limits.MakeBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Int(batchSize)) + keyLimiter, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Size(pkgconfig.Size(maxIdentifierKeyLengthBytes)*pkgconfig.Byte)) + require.NoError(t, err) + + bsl, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Int(batchSize)) + require.NoError(t, err) + + requestBatchSizeLimiter, err := limits.MakeUpperBoundLimiter(limits.Factory{Settings: cresettings.DefaultGetter}, settings.Int(maxRequestBatchSize)) require.NoError(t, err) return &ReportingPluginConfig{ - BatchSize: settings.Int(batchSize), MaxBatchSize: bsl, PublicKey: publicKey, PrivateKeyShare: privateKeyShare, MaxSecretsPerOwner: msl, + MaxShareLengthBytes: shareLimiter, MaxCiphertextLengthBytes: cipherTextLimiter, MaxIdentifierOwnerLengthBytes: ownerLimiter, MaxIdentifierNamespaceLengthBytes: namespaceOwnerLimiter, MaxIdentifierKeyLengthBytes: keyLimiter, + MaxRequestBatchSize: requestBatchSizeLimiter, } } @@ -286,6 +296,7 @@ func TestPlugin_Observation_NothingInBatch(t *testing.T) { 100, 100, 100, + 10, ), } @@ -319,6 +330,7 @@ func TestPlugin_Observation_PendingQueueEnabled_EmptyPendingQueue(t *testing.T) 100, 100, 100, + 10, ), unmarshalBlob: mockUnmarshalBlob, marshalBlob: mockMarshalBlob, @@ -393,6 +405,7 @@ func TestPlugin_Observation_PendingQueueEnabled_WithPendingQueueProvided(t *test 100, 100, 100, + 10, ), marshalBlob: mockMarshalBlob, unmarshalBlob: mockUnmarshalBlob, @@ -487,6 +500,7 @@ func TestPlugin_Observation_PendingQueueEnabled_ItemBothInPendingQueueAndLocalQu 100, 100, 100, + 10, ), marshalBlob: mockMarshalBlob, unmarshalBlob: mockUnmarshalBlob, @@ -630,6 +644,7 @@ func TestPlugin_Observation_GetSecretsRequest_SecretIdentifierInvalid(t *testing maxIDLen/3, maxIDLen/3, maxIDLen/3, + 10, ), marshalBlob: mockMarshalBlob, unmarshalBlob: mockUnmarshalBlob, @@ -696,6 +711,7 @@ func TestPlugin_Observation_GetSecretsRequest_FillsInNamespace(t *testing.T) { 100, 100, 100, + 10, ), marshalBlob: mockMarshalBlob, unmarshalBlob: mockUnmarshalBlob, @@ -784,6 +800,7 @@ func TestPlugin_Observation_GetSecretsRequest_SecretDoesNotExist(t *testing.T) { 100, 100, 100, + 10, ), marshalBlob: mockMarshalBlob, unmarshalBlob: mockUnmarshalBlob, @@ -857,6 +874,7 @@ func TestPlugin_Observation_GetSecretsRequest_SecretExistsButIsIncorrect(t *test 100, 100, 100, + 10, ), } @@ -942,6 +960,7 @@ func TestPlugin_Observation_GetSecretsRequest_PublicKeyIsInvalid(t *testing.T) { 100, 100, 100, + 10, ), } @@ -1025,6 +1044,7 @@ func TestPlugin_Observation_GetSecretsRequest_SecretLabelIsInvalid(t *testing.T) 100, 100, 100, + 10, ), } @@ -1116,6 +1136,7 @@ func TestPlugin_Observation_GetSecretsRequest_Success(t *testing.T) { 100, 100, 100, + 10, ), } @@ -1272,6 +1293,7 @@ func TestPlugin_Observation_CreateSecretsRequest_SecretIdentifierInvalid(t *test maxIDLen/3, maxIDLen/3, maxIDLen/3, + 10, ), } @@ -1337,6 +1359,7 @@ func TestPlugin_Observation_CreateSecretsRequest_DisallowsDuplicateRequests(t *t 30, 30, 30, + 10, ), } @@ -1413,6 +1436,7 @@ func TestPlugin_StateTransition_CreateSecretsRequest_CorrectlyTracksLimits(t *te 30, 30, 30, + 10, ), } @@ -1527,6 +1551,7 @@ func TestPlugin_Observation_CreateSecretsRequest_InvalidCiphertext(t *testing.T) 100, 100, 30, + 10, ), } @@ -1596,6 +1621,7 @@ func TestPlugin_Observation_CreateSecretsRequest_InvalidCiphertext_TooLong(t *te 100, 100, 100, + 10, ), } @@ -1672,6 +1698,7 @@ func TestPlugin_Observation_CreateSecretsRequest_InvalidCiphertext_EncryptedWith 100, 100, 100, + 10, ), } @@ -1729,6 +1756,168 @@ func TestPlugin_Observation_CreateSecretsRequest_InvalidCiphertext_EncryptedWith assert.Contains(t, resp.GetError(), "failed to verify ciphertext") } +func TestPlugin_Observation_CreateSecretsRequest_SecretLabelIsInvalid(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + marshalBlob: mockMarshalBlob, + unmarshalBlob: mockUnmarshalBlob, + cfg: makeReportingPluginConfig( + t, + 10, + pk, + shares[0], + 1, + 1024, + 100, + 100, + 100, + 10, + ), + } + + seqNr := uint64(1) + rdr := &kv{ + m: make(map[string]response), + } + + owner := "0x1234567890abcdef1234567890abcdef12345678" + wrongOwner := "0x0001020304050607080900010203040506070809" + + id := &vaultcommon.SecretIdentifier{ + Owner: owner, + Namespace: "main", + Key: "secret", + } + + var wrongLabel [32]byte + wrongOwnerAddr := common.HexToAddress(wrongOwner) + copy(wrongLabel[12:], wrongOwnerAddr.Bytes()) + ct, err := tdh2easy.EncryptWithLabel(pk, []byte("my secret value"), wrongLabel) + require.NoError(t, err) + + ciphertextBytes, err := ct.Marshal() + require.NoError(t, err) + + p := &vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + { + Id: id, + EncryptedValue: hex.EncodeToString(ciphertextBytes), + }, + }, + } + anyp, err := anypb.New(p) + require.NoError(t, err) + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyp}, + }, + ) + require.NoError(t, err) + data, err := r.Observation(t.Context(), seqNr, types.AttributedQuery{}, rdr, &blobber{}) + require.NoError(t, err) + + obs := &vaultcommon.Observations{} + err = proto.Unmarshal(data, obs) + require.NoError(t, err) + + assert.Len(t, obs.Observations, 1) + o := obs.Observations[0] + + assert.Equal(t, vaultcommon.RequestType_CREATE_SECRETS, o.RequestType) + + batchResp := o.GetCreateSecretsResponse() + require.Len(t, batchResp.Responses, 1) + resp := batchResp.Responses[0] + assert.Contains(t, resp.GetError(), "failed to verify ciphertext") +} + +func TestPlugin_Observation_UpdateSecretsRequest_SecretLabelIsInvalid(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + marshalBlob: mockMarshalBlob, + unmarshalBlob: mockUnmarshalBlob, + cfg: makeReportingPluginConfig( + t, + 10, + pk, + shares[0], + 1, + 1024, + 100, + 100, + 100, + 10, + ), + } + + seqNr := uint64(1) + rdr := &kv{ + m: make(map[string]response), + } + + owner := "0x1234567890abcdef1234567890abcdef12345678" + wrongOwner := "0x0001020304050607080900010203040506070809" + + id := &vaultcommon.SecretIdentifier{ + Owner: owner, + Namespace: "main", + Key: "secret", + } + + var wrongLabel [32]byte + wrongOwnerAddr := common.HexToAddress(wrongOwner) + copy(wrongLabel[12:], wrongOwnerAddr.Bytes()) + ct, err := tdh2easy.EncryptWithLabel(pk, []byte("my secret value"), wrongLabel) + require.NoError(t, err) + + ciphertextBytes, err := ct.Marshal() + require.NoError(t, err) + + p := &vaultcommon.UpdateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + { + Id: id, + EncryptedValue: hex.EncodeToString(ciphertextBytes), + }, + }, + } + anyp, err := anypb.New(p) + require.NoError(t, err) + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyp}, + }, + ) + require.NoError(t, err) + data, err := r.Observation(t.Context(), seqNr, types.AttributedQuery{}, rdr, &blobber{}) + require.NoError(t, err) + + obs := &vaultcommon.Observations{} + err = proto.Unmarshal(data, obs) + require.NoError(t, err) + + assert.Len(t, obs.Observations, 1) + o := obs.Observations[0] + + assert.Equal(t, vaultcommon.RequestType_UPDATE_SECRETS, o.RequestType) + + batchResp := o.GetUpdateSecretsResponse() + require.Len(t, batchResp.Responses, 1) + resp := batchResp.Responses[0] + assert.Contains(t, resp.GetError(), "failed to verify ciphertext") +} + func TestPlugin_StateTransition_CreateSecretsRequest_TooManySecretsForOwner(t *testing.T) { lggr := logger.TestLogger(t) store := requests.NewStore[*vaulttypes.Request]() @@ -1747,6 +1936,7 @@ func TestPlugin_StateTransition_CreateSecretsRequest_TooManySecretsForOwner(t *t 100, 100, 100, + 10, ), } @@ -1837,6 +2027,7 @@ func TestPlugin_StateTransition_CreateSecretsRequest_SecretExistsForKey(t *testi 100, 100, 100, + 10, ), } @@ -1923,6 +2114,7 @@ func TestPlugin_Observation_CreateSecretsRequest_Success(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2076,6 +2268,7 @@ func TestPlugin_StateTransition_InsufficientObservations(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2149,6 +2342,7 @@ func TestPlugin_ValidateObservations_InvalidObservations(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2237,6 +2431,7 @@ func TestPlugin_ValidateObservations_IncludesAllItemsInPendingQueue(t *testing.T 100, 100, 100, + 10, ), } @@ -2281,7 +2476,7 @@ func TestPlugin_ValidateObservations_IncludesAllItemsInPendingQueue(t *testing.T resp := &vaultcommon.GetSecretsResponse{ Responses: []*vaultcommon.SecretResponse{ { - Id: id, + Id: id2, }, }, } @@ -2339,6 +2534,7 @@ func TestPlugin_ValidateObservations_DisallowsDuplicateBlobHandles(t *testing.T) 100, 100, 100, + 10, ), unmarshalBlob: mockUnmarshalBlob, marshalBlob: mockMarshalBlob, @@ -2397,6 +2593,7 @@ func TestPlugin_StateTransition_ShasDontMatch(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2479,6 +2676,7 @@ func TestPlugin_StateTransition_AggregatesValidationErrors(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2557,6 +2755,7 @@ func TestPlugin_StateTransition_GetSecretsRequest_CombinesShares(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2700,6 +2899,7 @@ func TestPlugin_StateTransition_CreateSecretsRequest_WritesSecrets(t *testing.T) 100, 100, 100, + 10, ), } @@ -2870,6 +3070,7 @@ func TestPlugin_Reports(t *testing.T) { 100, 100, 100, + 10, ), } @@ -2966,6 +3167,7 @@ func TestPlugin_Observation_UpdateSecretsRequest_SecretIdentifierInvalid(t *test maxIDLen/3, maxIDLen/3, maxIDLen/3, + 10, ), } @@ -3031,6 +3233,7 @@ func TestPlugin_Observation_UpdateSecretsRequest_DisallowsDuplicateRequests(t *t 30, 30, 30, + 10, ), } @@ -3107,6 +3310,7 @@ func TestPlugin_Observation_UpdateSecretsRequest_InvalidCiphertext(t *testing.T) 100, 100, 100, + 10, ), } @@ -3176,6 +3380,7 @@ func TestPlugin_Observation_UpdateSecretsRequest_InvalidCiphertext_TooLong(t *te 100, 100, 100, + 10, ), } @@ -3252,6 +3457,7 @@ func TestPlugin_Observation_UpdateSecretsRequest_InvalidCiphertext_EncryptedWith 100, 100, 100, + 10, ), } @@ -3331,6 +3537,7 @@ func TestPlugin_StateTransition_UpdateSecretsRequest_SecretDoesntExist(t *testin 100, 100, 100, + 10, ), } @@ -3426,6 +3633,7 @@ func TestPlugin_StateTransition_UpdateSecretsRequest_WritesSecrets(t *testing.T) 100, 100, 100, + 10, ), } @@ -3581,6 +3789,7 @@ func TestPlugin_Reports_UpdateSecretsRequest(t *testing.T) { 100, 100, 100, + 10, ), } @@ -3622,6 +3831,7 @@ func TestPlugin_Observation_DeleteSecrets(t *testing.T) { 30, 30, 30, + 10, ), } @@ -3707,6 +3917,7 @@ func TestPlugin_Observation_DeleteSecrets_IdDoesntExist(t *testing.T) { 30, 30, 30, + 10, ), } @@ -3771,6 +3982,7 @@ func TestPlugin_Observation_DeleteSecrets_InvalidRequestDuplicateIds(t *testing. 30, 30, 30, + 10, ), } @@ -3844,6 +4056,7 @@ func TestPlugin_StateTransition_DeleteSecretsRequest(t *testing.T) { 100, 100, 100, + 10, ), } @@ -3953,6 +4166,7 @@ func TestPlugin_StateTransition_DeleteSecretsRequest_SecretDoesNotExist(t *testi 100, 100, 100, + 10, ), } @@ -4089,6 +4303,7 @@ func TestPlugin_Reports_DeleteSecretsRequest(t *testing.T) { 100, 100, 100, + 10, ), } @@ -4130,6 +4345,7 @@ func TestPlugin_Observation_ListSecretIdentifiers_OwnerRequired(t *testing.T) { 30, 30, 30, + 10, ), } @@ -4186,6 +4402,7 @@ func TestPlugin_Observation_ListSecretIdentifiers_NoNamespaceProvided(t *testing 30, 30, 30, + 10, ), } @@ -4288,6 +4505,7 @@ func TestPlugin_Observation_ListSecretIdentifiers_FilterByNamespace(t *testing.T 30, 30, 30, + 10, ), } @@ -4424,6 +4642,7 @@ func TestPlugin_Reports_ListSecretIdentifiersRequest(t *testing.T) { 100, 100, 100, + 10, ), } @@ -4469,6 +4688,7 @@ func TestPlugin_StateTransition_ListSecretIdentifiers(t *testing.T) { 100, 100, 100, + 10, ), } @@ -4554,11 +4774,12 @@ func TestPlugin_StateTransition_StoresPendingQueue(t *testing.T) { 10, pk, shares[0], - 1, + 5, 1024, 30, 30, 30, + 10, ), unmarshalBlob: mockUnmarshalBlob, } @@ -4633,11 +4854,13 @@ func TestPlugin_StateTransition_StoresPendingQueue(t *testing.T) { }, } + r.unmarshalBlob = bf.unmarshalBlob + o1 := &vaultcommon.Observations{ PendingQueueItems: [][]byte{ - {}, // maps to item 0 in the blobs - {}, // maps to item 1 in the blobs - {}, // maps to item 2 in the blobs + {0}, // maps to item 0 in the blobs + {1}, // maps to item 1 in the blobs + {2}, // maps to item 2 in the blobs }, } o1b, err := proto.Marshal(o1) @@ -4645,7 +4868,7 @@ func TestPlugin_StateTransition_StoresPendingQueue(t *testing.T) { o2 := &vaultcommon.Observations{ PendingQueueItems: [][]byte{ - {}, // maps to item 3 in the blobs + {3}, // maps to item 3 in the blobs }, } o2b, err := proto.Marshal(o2) @@ -4653,9 +4876,9 @@ func TestPlugin_StateTransition_StoresPendingQueue(t *testing.T) { o3 := &vaultcommon.Observations{ PendingQueueItems: [][]byte{ - {}, // maps to item 4 in the blobs - {}, // maps to item 5 in the blobs - {}, // maps to item 6 in the blobs + {4}, // maps to item 4 in the blobs + {5}, // maps to item 5 in the blobs + {6}, // maps to item 6 in the blobs }, } o3b, err := proto.Marshal(o3) @@ -4715,8 +4938,8 @@ func TestPlugin_StateTransition_StoresPendingQueue_LimitedToBatchSize(t *testing 30, 30, 30, + 10, ), - unmarshalBlob: mockUnmarshalBlob, } seqNr := uint64(1) @@ -4756,12 +4979,35 @@ func TestPlugin_StateTransition_StoresPendingQueue_LimitedToBatchSize(t *testing areq4, err := anypb.New(req4) require.NoError(t, err) + bf := &blobber{ + blobs: [][]byte{ + protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ + Id: "request-id", + Item: areq1, + }), + protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ + Id: "request-id2", + Item: areq2, + }), + protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ + Id: "request-id3", + Item: areq3, + }), + protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ + Id: "request-id4", + Item: areq4, + }), + }, + } + + r.unmarshalBlob = bf.unmarshalBlob + o1 := &vaultcommon.Observations{ PendingQueueItems: [][]byte{ - {}, // maps to item 0 in the blobs - {}, // maps to item 1 in the blobs - {}, // maps to item 2 in the blobs - {}, // maps to item 3 in the blobs + {0}, // maps to item 0 in the blobs + {1}, // maps to item 1 in the blobs + {2}, // maps to item 2 in the blobs + {3}, // maps to item 3 in the blobs }, } o1b, err := proto.Marshal(o1) @@ -4769,10 +5015,10 @@ func TestPlugin_StateTransition_StoresPendingQueue_LimitedToBatchSize(t *testing o2 := &vaultcommon.Observations{ PendingQueueItems: [][]byte{ - {}, // maps to item 0 in the blobs - {}, // maps to item 1 in the blobs - {}, // maps to item 2 in the blobs - {}, // maps to item 3 in the blobs + {0}, // maps to item 0 in the blobs + {1}, // maps to item 1 in the blobs + {2}, // maps to item 2 in the blobs + {3}, // maps to item 3 in the blobs }, } o2b, err := proto.Marshal(o2) @@ -4780,106 +5026,859 @@ func TestPlugin_StateTransition_StoresPendingQueue_LimitedToBatchSize(t *testing o3 := &vaultcommon.Observations{ PendingQueueItems: [][]byte{ - {}, // maps to item 0 in the blobs - {}, // maps to item 1 in the blobs - {}, // maps to item 2 in the blobs - {}, // maps to item 3 in the blobs + {0}, // maps to item 0 in the blobs + {1}, // maps to item 1 in the blobs + {2}, // maps to item 2 in the blobs + {3}, // maps to item 3 in the blobs }, } o3b, err := proto.Marshal(o3) require.NoError(t, err) - bf := &blobber{ - blobs: [][]byte{ - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id", - Item: areq1, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id2", - Item: areq2, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id3", - Item: areq3, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id4", - Item: areq4, - }), + reportPrecursor, err := r.StateTransition( + t.Context(), + seqNr, + types.AttributedQuery{}, + []types.AttributedObservation{ + {Observer: 0, Observation: o1b}, + {Observer: 1, Observation: o2b}, + {Observer: 2, Observation: o3b}, + }, + rdr, + bf, + ) + require.NoError(t, err) + + os := &vaultcommon.Outcomes{} + err = proto.Unmarshal(reportPrecursor, os) + require.NoError(t, err) + + assert.Empty(t, os.Outcomes) + + pq, err := NewReadStore(rdr).GetPendingQueue() + require.NoError(t, err) + assert.Len(t, pq, 1) + + ids := []string{} + for _, item := range pq { + ids = append(ids, item.Id) + } + + // Batch size is 1, so only one item should be stored. + assert.ElementsMatch(t, []string{"request-id"}, ids) +} + +func TestPlugin_StateTransition_StoresPendingQueue_DoesntDoubleCountObservationsFromOneNode(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 1, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + 10, + ), + } + + seqNr := uint64(1) + rdr := &kv{ + m: make(map[string]response), + } + + req1 := &vaultcommon.ListSecretIdentifiersRequest{ + Owner: "owner", + Namespace: "main", + RequestId: "request-id", + } + areq1, err := anypb.New(req1) + require.NoError(t, err) + + bf := &blobber{ + blobs: [][]byte{ protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ Id: "request-id", Item: areq1, }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id2", - Item: areq2, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id3", - Item: areq3, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id4", - Item: areq4, - }), + }, + } + + r.unmarshalBlob = bf.unmarshalBlob + + o1 := &vaultcommon.Observations{ + PendingQueueItems: [][]byte{ + {0}, // maps to item 0 in the blobs + {0}, // maps to item 0 in the blobs (duplicate) + {0}, // maps to item 0 in the blobs (duplicate) + }, + } + o1b, err := proto.Marshal(o1) + require.NoError(t, err) + + reportPrecursor, err := r.StateTransition( + t.Context(), + seqNr, + types.AttributedQuery{}, + []types.AttributedObservation{ + {Observer: 0, Observation: o1b}, + }, + rdr, + bf, + ) + require.NoError(t, err) + + os := &vaultcommon.Outcomes{} + err = proto.Unmarshal(reportPrecursor, os) + require.NoError(t, err) + + assert.Empty(t, os.Outcomes) + + pq, err := NewReadStore(rdr).GetPendingQueue() + require.NoError(t, err) + assert.Empty(t, pq, 0) + + ids := []string{} + for _, item := range pq { + ids = append(ids, item.Id) + } + + // 1 oracle submitted duplicates, so skipping + assert.ElementsMatch(t, []string{}, ids) +} + +func TestPlugin_ValidateObservation_RejectsIfMoreThan2xBatchSize(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 1, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + 10, + ), + unmarshalBlob: mockUnmarshalBlob, + } + + seqNr := uint64(1) + rdr := &kv{ + m: make(map[string]response), + } + + req1 := &vaultcommon.ListSecretIdentifiersRequest{ + Owner: "owner", + Namespace: "main", + RequestId: "request-id", + } + areq1, err := anypb.New(req1) + require.NoError(t, err) + + o1 := &vaultcommon.Observations{ + PendingQueueItems: [][]byte{ + {}, // maps to item 0 in the blobs + {}, // maps to item 1 in the blobs + {}, // maps to item 2 in the blobs + {}, // maps to item 3 in the blobs + }, + } + + o1b, err := proto.Marshal(o1) + require.NoError(t, err) + + bf := &blobber{ + blobs: [][]byte{ protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ Id: "request-id", Item: areq1, }), protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id2", - Item: areq2, + Id: "request-id", + Item: areq1, }), protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id3", - Item: areq3, + Id: "request-id", + Item: areq1, }), protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id4", - Item: areq4, + Id: "request-id", + Item: areq1, }), }, } - reportPrecursor, err := r.StateTransition( + err = r.ValidateObservation( t.Context(), seqNr, types.AttributedQuery{}, - []types.AttributedObservation{ - {Observer: 0, Observation: o1b}, - {Observer: 1, Observation: o2b}, - {Observer: 2, Observation: o3b}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, }, rdr, bf, ) + require.ErrorContains(t, err, "invalid observation: too many pending queue items provided, have 4, want max 2") +} + +func TestPlugin_ValidateObservation_GetSecretsRequest(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 1, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + 10, + ), + unmarshalBlob: mockUnmarshalBlob, + } - os := &vaultcommon.Outcomes{} - err = proto.Unmarshal(reportPrecursor, os) + seqNr := uint64(1) + rdr := &kv{ + m: make(map[string]response), + } + + id := &vaultcommon.SecretIdentifier{ + Owner: "owner", + Namespace: "main", + Key: "secret", + } + ek, _, err := box.GenerateKey(rand.Reader) require.NoError(t, err) + pks := hex.EncodeToString(ek[:]) + req := &vaultcommon.GetSecretsRequest{ + Requests: []*vaultcommon.SecretRequest{ + { + Id: id, + EncryptionKeys: []string{pks}, + }, + }, + } + resp := &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + { + Id: id, + Result: &vaultcommon.SecretResponse_Data{ + Data: &vaultcommon.SecretData{ + EncryptedValue: "encrypted-value", + EncryptedDecryptionKeyShares: []*vaultcommon.EncryptedShares{ + { + EncryptionKey: "my-encryption-key", + Shares: []string{"encrypted-share-1", "encrypted-share-2"}, + }, + }, + }, + }, + }, + }, + } + anyp, err := anypb.New(req) + require.NoError(t, err) + + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyp}, + }, + ) + require.NoError(t, err) + + bf := &blobber{} + + o1 := &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + { + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: req, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: resp, + }, + }, + }, + } + o1b := protoMarshal(t, o1) + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + require.ErrorContains(t, err, "invalid observation: observation must have exactly 1 share per encryption key") + + resp = &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + { + Id: id, + Result: &vaultcommon.SecretResponse_Error{ + Error: "foo", + }, + }, + }, + } + + o1 = &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + { + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: req, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: resp, + }, + }, + }, + } + o1b = protoMarshal(t, o1) + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + require.NoError(t, err) + + resp = &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + { + Id: id, + Result: &vaultcommon.SecretResponse_Error{ + Error: "foo", + }, + }, + { + Id: id, + Result: &vaultcommon.SecretResponse_Error{ + Error: "foo", + }, + }, + }, + } + + o1 = &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + { + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: req, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: resp, + }, + }, + }, + } + o1b = protoMarshal(t, o1) + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + require.ErrorContains(t, err, "invalid observation: GetSecrets request and response must have the same number of items") + + resp = &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + { + Id: id, + Result: &vaultcommon.SecretResponse_Data{ + Data: &vaultcommon.SecretData{ + EncryptedValue: "encrypted-value", + EncryptedDecryptionKeyShares: []*vaultcommon.EncryptedShares{}, + }, + }, + }, + }, + } + + o1 = &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + { + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: req, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: resp, + }, + }, + }, + } + o1b = protoMarshal(t, o1) + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + require.ErrorContains(t, err, "invalid observation: observation must contain a share per encryption key provided") + + resp = &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + { + Id: id, + Result: &vaultcommon.SecretResponse_Data{ + Data: &vaultcommon.SecretData{ + EncryptedValue: "encrypted-value", + EncryptedDecryptionKeyShares: []*vaultcommon.EncryptedShares{ + { + Shares: []string{strings.Repeat("1", 1000)}, + }, + }, + }, + }, + }, + }, + } + + o1 = &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + { + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: req, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: resp, + }, + }, + }, + } + o1b = protoMarshal(t, o1) + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + require.ErrorContains(t, err, "invalid observation: share provided exceeds maximum size allowed") +} + +func TestPlugin_ValidateObservation_PanicsOnEmptyShares(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 1, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + 10, + ), + unmarshalBlob: mockUnmarshalBlob, + } + + seqNr := uint64(1) + rdr := &kv{m: make(map[string]response)} + + id := &vaultcommon.SecretIdentifier{ + Owner: "owner", + Namespace: "main", + Key: "secret", + } + ek, _, err := box.GenerateKey(rand.Reader) + require.NoError(t, err) + pks := hex.EncodeToString(ek[:]) + + req := &vaultcommon.GetSecretsRequest{ + Requests: []*vaultcommon.SecretRequest{ + { + Id: id, + EncryptionKeys: []string{pks}, + }, + }, + } + // Malicious observation: EncryptedShares with an empty Shares slice. + // This triggers an index-out-of-bounds panic at ds.Shares[0] in validateObservation. + resp := &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + { + Id: id, + Result: &vaultcommon.SecretResponse_Data{ + Data: &vaultcommon.SecretData{ + EncryptedValue: "encrypted-value", + EncryptedDecryptionKeyShares: []*vaultcommon.EncryptedShares{ + { + EncryptionKey: pks, + Shares: []string{}, // empty — triggers panic + }, + }, + }, + }, + }, + }, + } + + anyp, err := anypb.New(req) + require.NoError(t, err) + + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyp}, + }, + ) + require.NoError(t, err) + + bf := &blobber{} + + o1 := &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + { + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: req, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: resp, + }, + }, + }, + } + o1b := protoMarshal(t, o1) + + // This should return an error, not panic. + require.NotPanics(t, func() { + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + }) + require.Error(t, err) +} + +func TestPlugin_ValidateObservation_NilSecretIdentifier(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 1, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + 10, + ), + unmarshalBlob: mockUnmarshalBlob, + } + + seqNr := uint64(1) + bf := &blobber{} + + id := &vaultcommon.SecretIdentifier{ + Owner: "owner", + Namespace: "main", + Key: "secret", + } + + tests := []struct { + name string + obs *vaultcommon.Observation + }{ + { + name: "GetSecrets request with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: &vaultcommon.GetSecretsRequest{ + Requests: []*vaultcommon.SecretRequest{ + {Id: nil}, + }, + }, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + {Id: id, Result: &vaultcommon.SecretResponse_Error{Error: "err"}}, + }, + }, + }, + }, + }, + { + name: "GetSecrets response with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_GET_SECRETS, + Request: &vaultcommon.Observation_GetSecretsRequest{ + GetSecretsRequest: &vaultcommon.GetSecretsRequest{ + Requests: []*vaultcommon.SecretRequest{ + {Id: id}, + }, + }, + }, + Response: &vaultcommon.Observation_GetSecretsResponse{ + GetSecretsResponse: &vaultcommon.GetSecretsResponse{ + Responses: []*vaultcommon.SecretResponse{ + {Id: nil, Result: &vaultcommon.SecretResponse_Error{Error: "err"}}, + }, + }, + }, + }, + }, + { + name: "CreateSecrets with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_CREATE_SECRETS, + Request: &vaultcommon.Observation_CreateSecretsRequest{ + CreateSecretsRequest: &vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: nil, EncryptedValue: "deadbeef"}, + }, + }, + }, + Response: &vaultcommon.Observation_CreateSecretsResponse{ + CreateSecretsResponse: &vaultcommon.CreateSecretsResponse{ + Responses: []*vaultcommon.CreateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + }, + { + name: "UpdateSecrets with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_UPDATE_SECRETS, + Request: &vaultcommon.Observation_UpdateSecretsRequest{ + UpdateSecretsRequest: &vaultcommon.UpdateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: nil, EncryptedValue: "deadbeef"}, + }, + }, + }, + Response: &vaultcommon.Observation_UpdateSecretsResponse{ + UpdateSecretsResponse: &vaultcommon.UpdateSecretsResponse{ + Responses: []*vaultcommon.UpdateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + }, + { + name: "CreateSecrets response with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_CREATE_SECRETS, + Request: &vaultcommon.Observation_CreateSecretsRequest{ + CreateSecretsRequest: &vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: "deadbeef"}, + }, + }, + }, + Response: &vaultcommon.Observation_CreateSecretsResponse{ + CreateSecretsResponse: &vaultcommon.CreateSecretsResponse{ + Responses: []*vaultcommon.CreateSecretResponse{ + {Id: nil}, + }, + }, + }, + }, + }, + { + name: "UpdateSecrets response with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_UPDATE_SECRETS, + Request: &vaultcommon.Observation_UpdateSecretsRequest{ + UpdateSecretsRequest: &vaultcommon.UpdateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: "deadbeef"}, + }, + }, + }, + Response: &vaultcommon.Observation_UpdateSecretsResponse{ + UpdateSecretsResponse: &vaultcommon.UpdateSecretsResponse{ + Responses: []*vaultcommon.UpdateSecretResponse{ + {Id: nil}, + }, + }, + }, + }, + }, + { + name: "DeleteSecrets response with nil Id", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_DELETE_SECRETS, + Request: &vaultcommon.Observation_DeleteSecretsRequest{ + DeleteSecretsRequest: &vaultcommon.DeleteSecretsRequest{ + Ids: []*vaultcommon.SecretIdentifier{id}, + }, + }, + Response: &vaultcommon.Observation_DeleteSecretsResponse{ + DeleteSecretsResponse: &vaultcommon.DeleteSecretsResponse{ + Responses: []*vaultcommon.DeleteSecretResponse{ + {Id: nil}, + }, + }, + }, + }, + }, + } - assert.Empty(t, os.Outcomes) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + rdr := &kv{m: make(map[string]response)} + + anyp, err := anypb.New(tc.obs.GetGetSecretsRequest()) + if anyp == nil { + // For non-GetSecrets types, use the appropriate request + switch tc.obs.RequestType { + case vaultcommon.RequestType_CREATE_SECRETS: + anyp, err = anypb.New(tc.obs.GetCreateSecretsRequest()) + case vaultcommon.RequestType_UPDATE_SECRETS: + anyp, err = anypb.New(tc.obs.GetUpdateSecretsRequest()) + case vaultcommon.RequestType_DELETE_SECRETS: + anyp, err = anypb.New(tc.obs.GetDeleteSecretsRequest()) + default: + t.FailNow() + } + } + require.NoError(t, err) - pq, err := NewReadStore(rdr).GetPendingQueue() - require.NoError(t, err) - assert.Len(t, pq, 1) + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyp}, + }, + ) + require.NoError(t, err) - ids := []string{} - for _, item := range pq { - ids = append(ids, item.Id) + o := &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{tc.obs}, + } + ob := protoMarshal(t, o) + + require.NotPanics(t, func() { + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: ob, + }, + rdr, + bf, + ) + }) + require.Error(t, err, "expected an error for nil secret identifier, not a panic") + }) } - - // Batch size is 1, so only one item should be stored. - assert.ElementsMatch(t, []string{"request-id"}, ids) } -func TestPlugin_StateTransition_StoresPendingQueue_DoesntDoubleCountObservationsFromOneNode(t *testing.T) { +func TestPlugin_ValidateObservation_CiphertextSize(t *testing.T) { lggr := logger.TestLogger(t) store := requests.NewStore[*vaulttypes.Request]() _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) require.NoError(t, err) + + // maxCipherTextLengthBytes = 10 bytes, so any ciphertext > 10 decoded bytes should be rejected r := &ReportingPlugin{ lggr: lggr, store: store, @@ -4893,83 +5892,211 @@ func TestPlugin_StateTransition_StoresPendingQueue_DoesntDoubleCountObservations pk, shares[0], 1, - 1024, + 10, 30, 30, 30, + 10, ), unmarshalBlob: mockUnmarshalBlob, } seqNr := uint64(1) - rdr := &kv{ - m: make(map[string]response), - } + bf := &blobber{} - req1 := &vaultcommon.ListSecretIdentifiersRequest{ + id := &vaultcommon.SecretIdentifier{ Owner: "owner", Namespace: "main", - RequestId: "request-id", + Key: "secret", } - areq1, err := anypb.New(req1) - require.NoError(t, err) - o1 := &vaultcommon.Observations{ - PendingQueueItems: [][]byte{ - {}, // maps to item 0 in the blobs - {}, // maps to item 1 in the blobs - {}, // maps to item 2 in the blobs - }, - } - o1b, err := proto.Marshal(o1) - require.NoError(t, err) + // 11 bytes hex-encoded = 22 hex chars, exceeds the 10-byte limit + oversizedCiphertext := hex.EncodeToString(make([]byte, 11)) + validCiphertext := hex.EncodeToString(make([]byte, 5)) - bf := &blobber{ - blobs: [][]byte{ - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id", - Item: areq1, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id", - Item: areq1, - }), - protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ - Id: "request-id", - Item: areq1, - }), + tests := []struct { + name string + obs *vaultcommon.Observation + errSubstr string + }{ + { + name: "CreateSecrets with oversized ciphertext", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_CREATE_SECRETS, + Request: &vaultcommon.Observation_CreateSecretsRequest{ + CreateSecretsRequest: &vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: oversizedCiphertext}, + }, + }, + }, + Response: &vaultcommon.Observation_CreateSecretsResponse{ + CreateSecretsResponse: &vaultcommon.CreateSecretsResponse{ + Responses: []*vaultcommon.CreateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + errSubstr: "ciphertext size exceeds maximum allowed size", }, - } - - reportPrecursor, err := r.StateTransition( - t.Context(), - seqNr, - types.AttributedQuery{}, - []types.AttributedObservation{ - {Observer: 0, Observation: o1b}, + { + name: "UpdateSecrets with oversized ciphertext", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_UPDATE_SECRETS, + Request: &vaultcommon.Observation_UpdateSecretsRequest{ + UpdateSecretsRequest: &vaultcommon.UpdateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: oversizedCiphertext}, + }, + }, + }, + Response: &vaultcommon.Observation_UpdateSecretsResponse{ + UpdateSecretsResponse: &vaultcommon.UpdateSecretsResponse{ + Responses: []*vaultcommon.UpdateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + errSubstr: "ciphertext size exceeds maximum allowed size", }, - rdr, - bf, - ) - require.NoError(t, err) + { + name: "CreateSecrets with invalid hex ciphertext", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_CREATE_SECRETS, + Request: &vaultcommon.Observation_CreateSecretsRequest{ + CreateSecretsRequest: &vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: "not-valid-hex!"}, + }, + }, + }, + Response: &vaultcommon.Observation_CreateSecretsResponse{ + CreateSecretsResponse: &vaultcommon.CreateSecretsResponse{ + Responses: []*vaultcommon.CreateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + errSubstr: "invalid hex encoding for ciphertext", + }, + { + name: "UpdateSecrets with invalid hex ciphertext", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_UPDATE_SECRETS, + Request: &vaultcommon.Observation_UpdateSecretsRequest{ + UpdateSecretsRequest: &vaultcommon.UpdateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: "not-valid-hex!"}, + }, + }, + }, + Response: &vaultcommon.Observation_UpdateSecretsResponse{ + UpdateSecretsResponse: &vaultcommon.UpdateSecretsResponse{ + Responses: []*vaultcommon.UpdateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + errSubstr: "invalid hex encoding for ciphertext", + }, + { + name: "CreateSecrets with valid ciphertext passes", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_CREATE_SECRETS, + Request: &vaultcommon.Observation_CreateSecretsRequest{ + CreateSecretsRequest: &vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: validCiphertext}, + }, + }, + }, + Response: &vaultcommon.Observation_CreateSecretsResponse{ + CreateSecretsResponse: &vaultcommon.CreateSecretsResponse{ + Responses: []*vaultcommon.CreateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + }, + { + name: "UpdateSecrets with valid ciphertext passes", + obs: &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_UPDATE_SECRETS, + Request: &vaultcommon.Observation_UpdateSecretsRequest{ + UpdateSecretsRequest: &vaultcommon.UpdateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + {Id: id, EncryptedValue: validCiphertext}, + }, + }, + }, + Response: &vaultcommon.Observation_UpdateSecretsResponse{ + UpdateSecretsResponse: &vaultcommon.UpdateSecretsResponse{ + Responses: []*vaultcommon.UpdateSecretResponse{ + {Id: id}, + }, + }, + }, + }, + }, + } - os := &vaultcommon.Outcomes{} - err = proto.Unmarshal(reportPrecursor, os) - require.NoError(t, err) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + rdr := &kv{m: make(map[string]response)} - assert.Empty(t, os.Outcomes) + var anyp *anypb.Any + switch tc.obs.RequestType { + case vaultcommon.RequestType_CREATE_SECRETS: + anyp, err = anypb.New(tc.obs.GetCreateSecretsRequest()) + case vaultcommon.RequestType_UPDATE_SECRETS: + anyp, err = anypb.New(tc.obs.GetUpdateSecretsRequest()) + default: + t.FailNow() + } + require.NoError(t, err) - pq, err := NewReadStore(rdr).GetPendingQueue() - require.NoError(t, err) - assert.Empty(t, pq, 0) + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyp}, + }, + ) + require.NoError(t, err) - ids := []string{} - for _, item := range pq { - ids = append(ids, item.Id) + o := &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{tc.obs}, + } + ob := protoMarshal(t, o) + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: ob, + }, + rdr, + bf, + ) + + if tc.errSubstr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errSubstr) + } else { + require.NoError(t, err) + } + }) } - - // 1 oracle submitted duplicates, so skipping - assert.ElementsMatch(t, []string{}, ids) } func TestPlugin_StateTransition_PendingQueueEnabled_NewQuora_NotGetRequest(t *testing.T) { @@ -4994,6 +6121,7 @@ func TestPlugin_StateTransition_PendingQueueEnabled_NewQuora_NotGetRequest(t *te 100, 100, 100, + 10, ), } @@ -5067,6 +6195,7 @@ func TestPlugin_StateTransition_PendingQueueEnabled_GetRequest(t *testing.T) { 100, 100, 100, + 10, ), } @@ -5131,3 +6260,268 @@ func TestPlugin_StateTransition_PendingQueueEnabled_GetRequest(t *testing.T) { assert.Equal(t, 1, observed.FilterMessage("sufficient observations for sha").Len()) } + +func TestPlugin_MaxShareSize(t *testing.T) { + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + + owner := "0x0001020304050607080900010203040506070809" + ownerAddress := common.HexToAddress(owner) + var label [32]byte + copy(label[12:], ownerAddress.Bytes()) // left-pad with 12 zero + + recipientPub, _, err := box.GenerateKey(rand.Reader) + require.NoError(t, err) + + expectedSize := cresettings.Default.VaultShareSizeLimit.DefaultValue + for i := range 10 { + plaintext := make([]byte, i*1024/9) // 0 to ~1kb + + ciphertext, err := tdh2easy.EncryptWithLabel(pk, plaintext, label) + require.NoError(t, err) + + ctb, err := ciphertext.Marshal() + require.NoError(t, err) + + share, err := generatePlaintextShare(pk, shares[0], ctb, owner) + require.NoError(t, err) + + eds, err := share.encryptWithKey(hex.EncodeToString(recipientPub[:])) + require.NoError(t, err) + + assert.GreaterOrEqual(t, expectedSize, len(eds), "share size should be constant regardless of plaintext size (plaintext=%d bytes)", len(plaintext)) + } +} + +func makeObservation(t *testing.T, reqType vaultcommon.RequestType, count int) *vaultcommon.Observation { + ids := make([]*vaultcommon.SecretIdentifier, count) + for i := range ids { + ids[i] = &vaultcommon.SecretIdentifier{Owner: "owner", Namespace: "main", Key: "secret" + string(rune('0'+i))} + } + + switch reqType { + case vaultcommon.RequestType_GET_SECRETS: + reqs := make([]*vaultcommon.SecretRequest, count) + resps := make([]*vaultcommon.SecretResponse, count) + for i, id := range ids { + reqs[i] = &vaultcommon.SecretRequest{Id: id} + resps[i] = &vaultcommon.SecretResponse{Id: id, Result: &vaultcommon.SecretResponse_Error{Error: "err"}} + } + return &vaultcommon.Observation{ + Id: "request-1", + RequestType: reqType, + Request: &vaultcommon.Observation_GetSecretsRequest{GetSecretsRequest: &vaultcommon.GetSecretsRequest{Requests: reqs}}, + Response: &vaultcommon.Observation_GetSecretsResponse{GetSecretsResponse: &vaultcommon.GetSecretsResponse{Responses: resps}}, + } + case vaultcommon.RequestType_CREATE_SECRETS: + secrets := make([]*vaultcommon.EncryptedSecret, count) + resps := make([]*vaultcommon.CreateSecretResponse, count) + for i, id := range ids { + secrets[i] = &vaultcommon.EncryptedSecret{Id: id, EncryptedValue: "deadbeef"} + resps[i] = &vaultcommon.CreateSecretResponse{Id: id} + } + return &vaultcommon.Observation{ + Id: "request-1", + RequestType: reqType, + Request: &vaultcommon.Observation_CreateSecretsRequest{CreateSecretsRequest: &vaultcommon.CreateSecretsRequest{EncryptedSecrets: secrets}}, + Response: &vaultcommon.Observation_CreateSecretsResponse{CreateSecretsResponse: &vaultcommon.CreateSecretsResponse{Responses: resps}}, + } + case vaultcommon.RequestType_UPDATE_SECRETS: + secrets := make([]*vaultcommon.EncryptedSecret, count) + resps := make([]*vaultcommon.UpdateSecretResponse, count) + for i, id := range ids { + secrets[i] = &vaultcommon.EncryptedSecret{Id: id, EncryptedValue: "deadbeef"} + resps[i] = &vaultcommon.UpdateSecretResponse{Id: id} + } + return &vaultcommon.Observation{ + Id: "request-1", + RequestType: reqType, + Request: &vaultcommon.Observation_UpdateSecretsRequest{UpdateSecretsRequest: &vaultcommon.UpdateSecretsRequest{EncryptedSecrets: secrets}}, + Response: &vaultcommon.Observation_UpdateSecretsResponse{UpdateSecretsResponse: &vaultcommon.UpdateSecretsResponse{Responses: resps}}, + } + case vaultcommon.RequestType_DELETE_SECRETS: + resps := make([]*vaultcommon.DeleteSecretResponse, count) + for i, id := range ids { + resps[i] = &vaultcommon.DeleteSecretResponse{Id: id} + } + return &vaultcommon.Observation{ + Id: "request-1", + RequestType: reqType, + Request: &vaultcommon.Observation_DeleteSecretsRequest{DeleteSecretsRequest: &vaultcommon.DeleteSecretsRequest{Ids: ids}}, + Response: &vaultcommon.Observation_DeleteSecretsResponse{DeleteSecretsResponse: &vaultcommon.DeleteSecretsResponse{Responses: resps}}, + } + default: + t.Fatalf("unsupported request type: %s", reqType) + return nil + } +} + +func TestPlugin_ValidateObservation_RequestBatchLimit(t *testing.T) { + maxRequestBatchSize := 2 + + tests := []struct { + name string + reqType vaultcommon.RequestType + batchSize int + wantErr string + }{ + { + name: "GetSecrets exceeding batch limit", + reqType: vaultcommon.RequestType_GET_SECRETS, + batchSize: maxRequestBatchSize + 1, + wantErr: "max batch size exceeded for request", + }, + { + name: "CreateSecrets exceeding batch limit", + reqType: vaultcommon.RequestType_CREATE_SECRETS, + batchSize: maxRequestBatchSize + 1, + wantErr: "max batch size exceeded for request", + }, + { + name: "UpdateSecrets exceeding batch limit", + reqType: vaultcommon.RequestType_UPDATE_SECRETS, + batchSize: maxRequestBatchSize + 1, + wantErr: "max batch size exceeded for request", + }, + { + name: "DeleteSecrets exceeding batch limit", + reqType: vaultcommon.RequestType_DELETE_SECRETS, + batchSize: maxRequestBatchSize + 1, + wantErr: "max batch size exceeded for request", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 10, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + maxRequestBatchSize, + ), + unmarshalBlob: mockUnmarshalBlob, + } + rdr := &kv{m: make(map[string]response)} + + obs := &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{ + makeObservation(t, tc.reqType, tc.batchSize), + }, + } + ob := protoMarshal(t, obs) + + err = r.ValidateObservation( + t.Context(), + 1, + types.AttributedQuery{}, + types.AttributedObservation{Observer: 0, Observation: ob}, + rdr, + &blobber{}, + ) + + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestPlugin_ValidateObservation_ListSecretIdentifiersExceedsMaxSecretsPerOwner(t *testing.T) { + maxSecretsPerOwner := 3 + + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + 10, + pk, + shares[0], + maxSecretsPerOwner, + 1024, + 30, + 30, + 30, + 10, + ), + unmarshalBlob: mockUnmarshalBlob, + } + + listReq := &vaultcommon.ListSecretIdentifiersRequest{ + Owner: "owner", + Namespace: "main", + RequestId: "request-1", + } + + identifiers := make([]*vaultcommon.SecretIdentifier, maxSecretsPerOwner+1) + for i := range identifiers { + identifiers[i] = &vaultcommon.SecretIdentifier{Owner: "owner", Namespace: "main", Key: fmt.Sprintf("secret%d", i)} + } + + observation := &vaultcommon.Observation{ + Id: "request-1", + RequestType: vaultcommon.RequestType_LIST_SECRET_IDENTIFIERS, + Request: &vaultcommon.Observation_ListSecretIdentifiersRequest{ + ListSecretIdentifiersRequest: listReq, + }, + Response: &vaultcommon.Observation_ListSecretIdentifiersResponse{ + ListSecretIdentifiersResponse: &vaultcommon.ListSecretIdentifiersResponse{ + Identifiers: identifiers, + Success: true, + }, + }, + } + + rdr := &kv{m: make(map[string]response)} + anyReq, err := anypb.New(listReq) + require.NoError(t, err) + err = NewWriteStore(rdr).WritePendingQueue( + []*vaultcommon.StoredPendingQueueItem{ + {Id: "request-1", Item: anyReq}, + }, + ) + require.NoError(t, err) + + obs := &vaultcommon.Observations{ + Observations: []*vaultcommon.Observation{observation}, + } + ob := protoMarshal(t, obs) + + err = r.ValidateObservation( + t.Context(), + 1, + types.AttributedQuery{}, + types.AttributedObservation{Observer: 0, Observation: ob}, + rdr, + &blobber{}, + ) + + require.ErrorContains(t, err, "ListSecretIdentifiers response exceeds maximum number of secrets per owner") +} diff --git a/core/services/ocr2/plugins/vault/transmitter_test.go b/core/services/ocr2/plugins/vault/transmitter_test.go index 129de09d672..c36f12edff0 100644 --- a/core/services/ocr2/plugins/vault/transmitter_test.go +++ b/core/services/ocr2/plugins/vault/transmitter_test.go @@ -87,6 +87,7 @@ func TestTransmitter(t *testing.T) { 100, 100, 100, + 10, ), } diff --git a/deployment/go.mod b/deployment/go.mod index 9359e306c25..e2fcf1239e5 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -43,7 +43,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863 diff --git a/deployment/go.sum b/deployment/go.sum index 8b85bd7ef41..7756c7e8371 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1373,8 +1373,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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 8666fd5ffbe..bc6e749c960 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc 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.12-0.20260227110503-42b236799872 diff --git a/go.sum b/go.sum index b1fd96ab2d3..916ea3acbf2 100644 --- a/go.sum +++ b/go.sum @@ -1183,8 +1183,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-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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 d268a810c59..8d720eef065 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -50,7 +50,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260303102708-6caf8c4ea3b4 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index eb9dfe86eed..1fddccf88d0 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1614,8 +1614,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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 a3f487f6429..6a5ac3df480 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -32,7 +32,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260303102708-6caf8c4ea3b4 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 188a6ef1525..64b794080db 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1592,8 +1592,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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 a18ec2cb8d9..cb7833f3daf 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -32,7 +32,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index c02635fcdd8..95cfd1843e6 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1573,8 +1573,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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 6de72aa965d..f48a0e7a5fb 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -54,7 +54,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.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 23267a101b5..332a346ad30 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1780,8 +1780,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619 h1:mM4TnyNkRnNXZ+3WkjL+B1/CvepoQ+Aw+Y1AuRIzJQY= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260306124118-a6395a14f619/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc h1:euYwd49PgzksFd9RBQ+qEObafDDz7fJu/9Oibc0G3Fk= +github.com/smartcontractkit/chainlink-common v0.10.1-0.20260309113338-432602d809cc/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= 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=