diff --git a/Dockerfile b/Dockerfile index 4cd54cee0a..ec1ded69cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,13 +43,6 @@ RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ go build -o ./bin/dataapi ./cmd/dataapi -# Batcher build stage -FROM common-builder AS batcher-builder -WORKDIR /app/disperser -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - go build -o ./bin/batcher ./cmd/batcher - # Retriever build stage FROM common-builder AS retriever-builder WORKDIR /app/retriever @@ -146,10 +139,6 @@ FROM alpine:3.22 AS dataapi COPY --from=dataapi-builder /app/disperser/bin/dataapi /usr/local/bin ENTRYPOINT ["dataapi"] -FROM alpine:3.22 AS batcher -COPY --from=batcher-builder /app/disperser/bin/batcher /usr/local/bin -ENTRYPOINT ["batcher"] - FROM alpine:3.22 AS retriever COPY --from=retriever-builder /app/retriever/bin/retriever /usr/local/bin ENTRYPOINT ["retriever"] diff --git a/core/indexer/state_test.go b/core/indexer/state_test.go index a35a169c8f..90dfba3531 100644 --- a/core/indexer/state_test.go +++ b/core/indexer/state_test.go @@ -128,7 +128,7 @@ func mustMakeChainState( logger logging.Logger, ) *coreindexer.IndexedChainState { t.Helper() - client, rpcClient := mustMakeTestClients(t, env, env.Batcher[0].BATCHER_PRIVATE_KEY, logger) + client, rpcClient := mustMakeTestClients(t, env, env.Churner.CHURNER_PRIVATE_KEY, logger) tx, err := eth.NewWriter(logger, client, env.EigenDA.OperatorStateRetriever, env.EigenDA.ServiceManager) require.NoError(t, err, "failed to create writer") diff --git a/disperser/Makefile b/disperser/Makefile index dd7ec27ace..444020b053 100644 --- a/disperser/Makefile +++ b/disperser/Makefile @@ -1,7 +1,7 @@ build: # We build the apiserver individually to change its name to "server" instead of "apiserver" go build -o ./bin/server ./cmd/apiserver -# All the other binaries (dataapi, encoder, batcher, etc) are then built together. +# All the other binaries (dataapi, encoder, etc) are then built together. go build -o ./bin ./... clean: @@ -9,30 +9,6 @@ clean: # Below are example run commands. They are not maintained so likely to be out of date. -run_batcher: build - ./bin/batcher \ - --batcher.pull-interval 10s \ - --batcher.bls-operator-state-retriever 0x9d4454B023096f34B160D6B654540c56A1F81688 \ - --batcher.eigenda-service-manager 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 \ - --chain.rpc http://localhost:8545 \ - --chain.private-key ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - --batcher.aws.region us-east-1 \ - --batcher.aws.access-key-id xyz \ - --batcher.aws.secret-access-key hello \ - --batcher.aws.endpoint-url http://0.0.0.0:4566 \ - --batcher.s3-bucket-name test-eigenda-blobstore \ - --batcher.dynamodb-table-name test-BlobMetadata \ - --encoder-socket 34000 \ - --batcher.enable-metrics \ - --batcher.graph-url false \ - --batcher.batch-size-limit 10000 \ - --batcher.use-graph false \ - --batcher.srs-order 3000 \ - --encoding-timeout 10s \ - --attestation-timeout 11s \ - --chain-read-timeout 12s \ - --chain-write-timeout 13s - run_server: build ./bin/server \ --grpc-port 51001 \ diff --git a/disperser/batcher/batcher.go b/disperser/batcher/batcher.go deleted file mode 100644 index d6f117bed5..0000000000 --- a/disperser/batcher/batcher.go +++ /dev/null @@ -1,774 +0,0 @@ -package batcher - -import ( - "bytes" - "context" - "errors" - "fmt" - "math" - "math/big" - "time" - - "github.com/Layr-Labs/eigenda/common" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/core/types" - "github.com/gammazero/workerpool" - "github.com/google/uuid" - "github.com/hashicorp/go-multierror" - "github.com/prometheus/client_golang/prometheus" - "github.com/wealdtech/go-merkletree/v2" -) - -const ( - QuantizationFactor = uint(1) - indexerWarmupDelay = 2 * time.Second -) - -type BatchPlan struct { - IncludedBlobs []*disperser.BlobMetadata - Quorums map[core.QuorumID]QuorumInfo - State *core.IndexedOperatorState -} - -type QuorumInfo struct { - Assignments map[core.OperatorID]core.Assignment - Info core.AssignmentInfo - QuantizationFactor uint -} - -type TimeoutConfig struct { - EncodingTimeout time.Duration - // The time allowed for a particular validator to provide a signature for a batch. - AttestationTimeout time.Duration - // The time allowed for collecting any and all signatures for a batch. - BatchAttestationTimeout time.Duration - ChainReadTimeout time.Duration - ChainWriteTimeout time.Duration - ChainStateTimeout time.Duration - TxnBroadcastTimeout time.Duration -} - -type Config struct { - PullInterval time.Duration - FinalizerInterval time.Duration - FinalizerPoolSize int - EncoderSocket string - SRSOrder int - NumConnections int - EncodingRequestQueueSize int - // BatchSizeMBLimit is the maximum size of a batch in MB - BatchSizeMBLimit uint - MaxNumRetriesPerBlob uint - - FinalizationBlockDelay uint - - TargetNumChunks uint64 - MaxBlobsToFetchFromStore int -} - -type Batcher struct { - Config - TimeoutConfig - - Queue disperser.BlobStore - Dispatcher disperser.Dispatcher - EncoderClient disperser.EncoderClient - - ChainState core.IndexedChainState - AssignmentCoordinator core.AssignmentCoordinator - Aggregator core.SignatureAggregator - EncodingStreamer *EncodingStreamer - Transactor core.Writer - TransactionManager TxnManager - Metrics *Metrics - HeartbeatChan chan time.Time - - ethClient common.EthClient - finalizer Finalizer - logger logging.Logger -} - -func NewBatcher( - config Config, - timeoutConfig TimeoutConfig, - queue disperser.BlobStore, - dispatcher disperser.Dispatcher, - chainState core.IndexedChainState, - assignmentCoordinator core.AssignmentCoordinator, - encoderClient disperser.EncoderClient, - aggregator core.SignatureAggregator, - ethClient common.EthClient, - finalizer Finalizer, - transactor core.Writer, - txnManager TxnManager, - logger logging.Logger, - metrics *Metrics, - heartbeatChan chan time.Time, -) (*Batcher, error) { - batchTrigger := NewEncodedSizeNotifier( - make(chan struct{}, 1), - uint64(config.BatchSizeMBLimit)*1024*1024, // convert to bytes - ) - streamerConfig := StreamerConfig{ - SRSOrder: config.SRSOrder, - EncodingRequestTimeout: config.PullInterval, - EncodingQueueLimit: config.EncodingRequestQueueSize, - TargetNumChunks: config.TargetNumChunks, - MaxBlobsToFetchFromStore: config.MaxBlobsToFetchFromStore, - FinalizationBlockDelay: config.FinalizationBlockDelay, - ChainStateTimeout: timeoutConfig.ChainStateTimeout, - } - encodingWorkerPool := workerpool.New(config.NumConnections) - encodingStreamer, err := NewEncodingStreamer( - streamerConfig, - queue, - chainState, - encoderClient, - assignmentCoordinator, - batchTrigger, - encodingWorkerPool, - metrics.EncodingStreamerMetrics, - metrics, - logger, - ) - if err != nil { - return nil, err - } - - return &Batcher{ - Config: config, - TimeoutConfig: timeoutConfig, - - Queue: queue, - Dispatcher: dispatcher, - EncoderClient: encoderClient, - - ChainState: chainState, - AssignmentCoordinator: assignmentCoordinator, - Aggregator: aggregator, - EncodingStreamer: encodingStreamer, - Transactor: transactor, - TransactionManager: txnManager, - Metrics: metrics, - - ethClient: ethClient, - finalizer: finalizer, - logger: logger.With("component", "Batcher"), - HeartbeatChan: heartbeatChan, - }, nil -} - -func (b *Batcher) RecoverState(ctx context.Context) error { - b.logger.Info("Recovering state...") - start := time.Now() - metas, err := b.Queue.GetBlobMetadataByStatus(ctx, disperser.Dispersing) - if err != nil { - return fmt.Errorf("failed to get blobs in dispersing state: %w", err) - } - expired := 0 - processing := 0 - for _, meta := range metas { - if meta.Expiry == 0 || meta.Expiry < uint64(time.Now().Unix()) { - err = b.Queue.MarkBlobFailed(ctx, meta.GetBlobKey()) - if err != nil { - return fmt.Errorf("failed to mark blob (%s) as failed: %w", meta.GetBlobKey(), err) - } - expired += 1 - } else { - err = b.Queue.MarkBlobProcessing(ctx, meta.GetBlobKey()) - if err != nil { - return fmt.Errorf("failed to mark blob (%s) as processing: %w", meta.GetBlobKey(), err) - } - processing += 1 - } - } - b.logger.Info("Recovering state took", - "duration", time.Since(start), - "numBlobs", len(metas), - "expired", expired, - "processing", processing) - return nil -} - -func (b *Batcher) Start(ctx context.Context) error { - err := b.RecoverState(ctx) - if err != nil { - return fmt.Errorf("failed to recover state: %w", err) - } - err = b.ChainState.Start(ctx) - if err != nil { - return err - } - // Wait for few seconds for indexer to index blockchain - // This won't be needed when we switch to using Graph node - time.Sleep(indexerWarmupDelay) - err = b.EncodingStreamer.Start(ctx) - if err != nil { - return err - } - batchTrigger := b.EncodingStreamer.EncodedSizeNotifier - - go func() { - receiptChan := b.TransactionManager.ReceiptChan() - for { - select { - case <-ctx.Done(): - return - case receiptOrErr := <-receiptChan: - b.logger.Info("received response from transaction manager", - "receipt", receiptOrErr.Receipt, - "err", receiptOrErr.Err) - err := b.ProcessConfirmedBatch(ctx, receiptOrErr) - if err != nil { - b.logger.Error("failed to process confirmed batch", - "err", err) - } - } - } - }() - b.TransactionManager.Start(ctx) - - b.finalizer.Start(ctx) - - go func() { - ticker := time.NewTicker(b.PullInterval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if err := b.HandleSingleBatch(ctx); err != nil { - if errors.Is(err, errNoEncodedResults) { - b.logger.Warn("no encoded results to make a batch with") - } else { - b.logger.Error("failed to process a batch", "err", err) - } - } - case <-batchTrigger.Notify: - ticker.Stop() - - if err := b.HandleSingleBatch(ctx); err != nil { - if errors.Is(err, errNoEncodedResults) { - b.logger.Warn("no encoded results to make a batch with") - } else { - b.logger.Error("failed to process a batch", "err", err) - } - } - ticker.Reset(b.PullInterval) - } - } - }() - - return nil -} - -// updateConfirmationInfo updates the confirmation info for each blob in the batch and returns failed blobs to retry. -func (b *Batcher) updateConfirmationInfo( - ctx context.Context, - batchData confirmationMetadata, - txnReceipt *types.Receipt, -) ([]*disperser.BlobMetadata, error) { - if txnReceipt.BlockNumber == nil { - return nil, errors.New( - "HandleSingleBatch: error getting transaction receipt block number") - } - if len(batchData.blobs) == 0 { - return nil, errors.New( - "failed to process confirmed batch: no blobs from transaction manager metadata") - } - if batchData.batchHeader == nil { - return nil, errors.New( - "failed to process confirmed batch: batch header from transaction manager metadata is nil") - } - if len(batchData.blobHeaders) == 0 { - return nil, errors.New( - "failed to process confirmed batch: no blob headers from transaction manager metadata") - } - if batchData.merkleTree == nil { - return nil, errors.New( - "failed to process confirmed batch: merkle tree from transaction manager metadata is nil") - } - if batchData.aggSig == nil { - return nil, errors.New( - "failed to process confirmed batch: aggSig from transaction manager metadata is nil") - } - headerHash, err := batchData.batchHeader.GetBatchHeaderHash() - if err != nil { - return nil, fmt.Errorf("HandleSingleBatch: error getting batch header hash: %w", err) - } - batchID, err := b.getBatchID(ctx, txnReceipt) - if err != nil { - return nil, fmt.Errorf("HandleSingleBatch: error fetching batch ID: %w", err) - } - - blobsToRetry := make([]*disperser.BlobMetadata, 0) - var updateConfirmationInfoErr error - - for blobIndex, metadata := range batchData.blobs { - // Mark the blob failed if it didn't get enough signatures. - status := disperser.InsufficientSignatures - - var proof []byte - if isBlobAttested(batchData.aggSig.QuorumResults, batchData.blobHeaders[blobIndex]) { - status = disperser.Confirmed - // generate inclusion proof - if blobIndex >= len(batchData.blobHeaders) { - b.logger.Error("HandleSingleBatch: error confirming blobs: blob header not found in batch", - "index", blobIndex) - blobsToRetry = append(blobsToRetry, batchData.blobs[blobIndex]) - continue - } - - merkleProof, err := batchData.merkleTree.GenerateProofWithIndex(uint64(blobIndex), 0) - if err != nil { - b.logger.Error("HandleSingleBatch: failed to generate blob header inclusion proof", - "err", err) - blobsToRetry = append(blobsToRetry, batchData.blobs[blobIndex]) - continue - } - proof = core.SerializeMerkleProof(merkleProof) - } - - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: headerHash, - BlobIndex: uint32(blobIndex), - SignatoryRecordHash: core.ComputeSignatoryRecordHash( - uint32(batchData.batchHeader.ReferenceBlockNumber), - batchData.aggSig.NonSigners), - ReferenceBlockNumber: uint32(batchData.batchHeader.ReferenceBlockNumber), - BatchRoot: batchData.batchHeader.BatchRoot[:], - BlobInclusionProof: proof, - BlobCommitment: &batchData.blobHeaders[blobIndex].BlobCommitments, - BatchID: uint32(batchID), - ConfirmationTxnHash: txnReceipt.TxHash, - ConfirmationBlockNumber: uint32(txnReceipt.BlockNumber.Uint64()), - Fee: []byte{0}, // No fee - QuorumResults: batchData.aggSig.QuorumResults, - BlobQuorumInfos: batchData.blobHeaders[blobIndex].QuorumInfos, - } - - if status == disperser.Confirmed { - if _, updateConfirmationInfoErr = b.Queue.MarkBlobConfirmed( - ctx, metadata, confirmationInfo); updateConfirmationInfoErr == nil { - b.Metrics.UpdateCompletedBlob(int(metadata.RequestMetadata.BlobSize), disperser.Confirmed) - } - } else if status == disperser.InsufficientSignatures { - if _, updateConfirmationInfoErr = b.Queue.MarkBlobInsufficientSignatures( - ctx, metadata, confirmationInfo); updateConfirmationInfoErr == nil { - b.Metrics.UpdateCompletedBlob(int(metadata.RequestMetadata.BlobSize), disperser.InsufficientSignatures) - } - } else { - updateConfirmationInfoErr = fmt.Errorf( - "HandleSingleBatch: trying to update confirmation info for blob in status "+ - "other than confirmed or insufficient signatures: %s", - status.String()) - } - if updateConfirmationInfoErr != nil { - b.logger.Error("HandleSingleBatch: error updating blob confirmed metadata", - "err", updateConfirmationInfoErr) - blobsToRetry = append(blobsToRetry, batchData.blobs[blobIndex]) - } - requestTime := time.Unix(0, int64(metadata.RequestMetadata.RequestedAt)) - b.Metrics.ObserveLatency("E2E", float64(time.Since(requestTime).Milliseconds())) - b.Metrics.ObserveBlobAge("confirmed", float64(time.Since(requestTime).Milliseconds())) - for _, quorumInfo := range batchData.blobHeaders[blobIndex].QuorumInfos { - b.Metrics.IncrementBlobSize("confirmed", quorumInfo.QuorumID, int(metadata.RequestMetadata.BlobSize)) - } - } - - return blobsToRetry, nil -} - -func (b *Batcher) ProcessConfirmedBatch(ctx context.Context, receiptOrErr *ReceiptOrErr) error { - if receiptOrErr.Metadata == nil { - return errors.New( - "failed to process confirmed batch: no metadata from transaction manager response") - } - confirmationMetadata := receiptOrErr.Metadata.(confirmationMetadata) - blobs := confirmationMetadata.blobs - if len(blobs) == 0 { - return errors.New("failed to process confirmed batch: no blobs from transaction manager metadata") - } - if receiptOrErr.Err != nil { - _ = b.handleFailure(ctx, blobs, FailConfirmBatch) - return fmt.Errorf("failed to confirm batch onchain: %w", receiptOrErr.Err) - } - if confirmationMetadata.aggSig == nil { - _ = b.handleFailure(ctx, blobs, FailNoAggregatedSignature) - return errors.New("failed to process confirmed batch: aggSig from transaction manager metadata is nil") - } - b.logger.Info("received ConfirmBatch transaction receipt", - "blockNumber", receiptOrErr.Receipt.BlockNumber, - "txnHash", receiptOrErr.Receipt.TxHash.Hex()) - - // Mark the blobs as complete - stageTimer := time.Now() - blobsToRetry, err := b.updateConfirmationInfo(ctx, confirmationMetadata, receiptOrErr.Receipt) - if err != nil { - _ = b.handleFailure(ctx, blobs, FailUpdateConfirmationInfo) - return fmt.Errorf("failed to update confirmation info: %w", err) - } - if len(blobsToRetry) > 0 { - b.logger.Error("failed to update confirmation info", - "failed", len(blobsToRetry), - "total", len(blobs)) - _ = b.handleFailure(ctx, blobsToRetry, FailUpdateConfirmationInfo) - } - b.logger.Debug("Update confirmation info took", - "duration", time.Since(stageTimer).String()) - b.Metrics.ObserveLatency("UpdateConfirmationInfo", float64(time.Since(stageTimer).Milliseconds())) - batchSize := int64(0) - for _, blobMeta := range blobs { - batchSize += int64(blobMeta.RequestMetadata.BlobSize) - } - b.Metrics.IncrementBatchCount(batchSize) - - return nil -} - -func (b *Batcher) handleFailure( - ctx context.Context, - blobMetadatas []*disperser.BlobMetadata, - reason FailReason, -) error { - var result *multierror.Error - numPermanentFailures := 0 - for _, metadata := range blobMetadatas { - b.EncodingStreamer.RemoveEncodedBlob(metadata) - retry, err := b.Queue.HandleBlobFailure(ctx, metadata, b.MaxNumRetriesPerBlob) - if err != nil { - b.logger.Error("HandleSingleBatch: error handling blob failure", - "err", err) - // Append the error - result = multierror.Append(result, err) - } - - if retry { - continue - } - - if reason == FailNoSignatures { - b.Metrics.UpdateCompletedBlob(int(metadata.RequestMetadata.BlobSize), disperser.InsufficientSignatures) - } else { - b.Metrics.UpdateCompletedBlob(int(metadata.RequestMetadata.BlobSize), disperser.Failed) - } - numPermanentFailures++ - } - b.Metrics.UpdateBatchError(reason, numPermanentFailures) - - // Return the error(s) - return result.ErrorOrNil() -} - -type confirmationMetadata struct { - batchID uuid.UUID - batchHeader *core.BatchHeader - blobs []*disperser.BlobMetadata - blobHeaders []*core.BlobHeader - merkleTree *merkletree.MerkleTree - aggSig *core.SignatureAggregation -} - -func (b *Batcher) observeBlobAge(stage string, batch *batch) { - for _, m := range batch.BlobMetadata { - requestTime := time.Unix(0, int64(m.RequestMetadata.RequestedAt)) - b.Metrics.ObserveBlobAge(stage, float64(time.Since(requestTime).Milliseconds())) - } -} - -func (b *Batcher) observeBlobAgeAndSize(stage string, batch *batch) { - for i, m := range batch.BlobMetadata { - requestTime := time.Unix(0, int64(m.RequestMetadata.RequestedAt)) - b.Metrics.ObserveBlobAge(stage, float64(time.Since(requestTime).Milliseconds())) - for _, quorumInfo := range batch.BlobHeaders[i].QuorumInfos { - b.Metrics.IncrementBlobSize(stage, quorumInfo.QuorumID, int(m.RequestMetadata.BlobSize)) - } - } -} - -func (b *Batcher) HandleSingleBatch(ctx context.Context) error { - log := b.logger - - // Signal Liveness to indicate no stall - b.signalLiveness() - - // start a timer - timer := prometheus.NewTimer(prometheus.ObserverFunc(func(f float64) { - b.Metrics.ObserveLatency("total", f*1000) // make milliseconds - })) - defer timer.ObserveDuration() - - stageTimer := time.Now() - batch, err := b.EncodingStreamer.CreateBatch(ctx) - if err != nil { - return err - } - log.Debug("CreateBatch took", - "duration", time.Since(stageTimer)) - b.observeBlobAge("batched", batch) - - // Dispatch encoded batch - log.Debug("Dispatching encoded batch...") - stageTimer = time.Now() - - attestationCtx, cancel := context.WithTimeout(ctx, b.BatchAttestationTimeout) - defer cancel() - - update := b.Dispatcher.DisperseBatch(attestationCtx, batch.State, batch.EncodedBlobs, batch.BatchHeader) - log.Debug("DisperseBatch took", - "duration", time.Since(stageTimer)) - b.observeBlobAge("attestation_requested", batch) - h, err := batch.State.OperatorState.Hash() - if err != nil { - log.Error("HandleSingleBatch: error getting operator state hash", - "err", err) - } - hStr := make([]string, 0, len(h)) - for q, hash := range h { - hStr = append(hStr, fmt.Sprintf("%d: %x", q, hash)) - } - log.Info("Dispatched encoded batch", - "operatorStateHash", hStr) - - // Get the batch header hash - log.Debug("Getting batch header hash...") - headerHash, err := batch.BatchHeader.GetBatchHeaderHash() - if err != nil { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailBatchHeaderHash) - return fmt.Errorf("HandleSingleBatch: error getting batch header hash: %w", err) - } - - // Aggregate the signatures - log.Debug("Aggregating signatures...") - - stageTimer = time.Now() - quorumAttestation, err := b.Aggregator.ReceiveSignatures(ctx, attestationCtx, batch.State, headerHash, update) - if err != nil { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailAggregateSignatures) - return fmt.Errorf("HandleSingleBatch: error receiving and validating signatures: %w", err) - } - operatorCount := make(map[core.QuorumID]int) - signerCount := make(map[core.QuorumID]int) - for quorumID, opState := range batch.State.Operators { - operatorCount[quorumID] = len(opState) - if _, ok := signerCount[quorumID]; !ok { - signerCount[quorumID] = 0 - } - for opID := range opState { - if _, ok := quorumAttestation.SignerMap[opID]; ok { - signerCount[quorumID]++ - } - } - } - b.Metrics.UpdateAttestation(operatorCount, signerCount, quorumAttestation.QuorumResults) - for _, quorumResult := range quorumAttestation.QuorumResults { - log.Info("Aggregated quorum result", - "quorumID", quorumResult.QuorumID, - "percentSigned", quorumResult.PercentSigned) - } - - b.observeBlobAgeAndSize("attested", batch) - - numPassed, passedQuorums := numBlobsAttestedByQuorum(quorumAttestation.QuorumResults, batch.BlobHeaders) - // TODO(mooselumph): Determine whether to confirm the batch based on the number of successes - if numPassed == 0 { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailNoSignatures) - return errors.New("HandleSingleBatch: no blobs received sufficient signatures") - } - - nonEmptyQuorums := []core.QuorumID{} - for quorumID := range passedQuorums { - log.Info("Quorums successfully attested", - "quorumID", quorumID) - nonEmptyQuorums = append(nonEmptyQuorums, quorumID) - } - - indexedOperatorState, err := b.ChainState.GetIndexedOperatorState( - ctx, - batch.BatchHeader.ReferenceBlockNumber, - nonEmptyQuorums) - if err != nil { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailAggregateSignatures) - return fmt.Errorf("HandleSingleBatch: error getting indexed operator state: %w", err) - } - - // Aggregate the signatures across only the non-empty quorums. Excluding empty quorums reduces the gas cost. - aggSig, err := b.Aggregator.AggregateSignatures( - indexedOperatorState, - quorumAttestation, - nonEmptyQuorums) - if err != nil { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailAggregateSignatures) - return fmt.Errorf("HandleSingleBatch: error aggregating signatures: %w", err) - } - - log.Debug("AggregateSignatures took", - "duration", time.Since(stageTimer)) - b.Metrics.ObserveLatency("AggregateSignatures", float64(time.Since(stageTimer).Milliseconds())) - - // Confirm the batch - log.Debug("Confirming batch...") - - txn, err := b.Transactor.BuildConfirmBatchTxn(ctx, batch.BatchHeader, aggSig.QuorumResults, aggSig) - if err != nil { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailConfirmBatch) - return fmt.Errorf("HandleSingleBatch: error building confirmBatch transaction: %w", err) - } - err = b.TransactionManager.ProcessTransaction( - ctx, - NewTxnRequest( - txn, - "confirmBatch", - big.NewInt(0), - confirmationMetadata{ - batchID: uuid.Nil, - batchHeader: batch.BatchHeader, - blobs: batch.BlobMetadata, - blobHeaders: batch.BlobHeaders, - merkleTree: batch.MerkleTree, - aggSig: aggSig, - })) - if err != nil { - _ = b.handleFailure(ctx, batch.BlobMetadata, FailConfirmBatch) - return fmt.Errorf("HandleSingleBatch: error sending confirmBatch transaction: %w", err) - } - - return nil -} - -func (b *Batcher) parseBatchIDFromReceipt(txReceipt *types.Receipt) (uint32, error) { - if len(txReceipt.Logs) == 0 { - return 0, errors.New("failed to get transaction receipt with logs") - } - for _, log := range txReceipt.Logs { - if len(log.Topics) == 0 { - b.logger.Debug("transaction receipt has no topics") - continue - } - b.logger.Debug("[getBatchIDFromReceipt] ", - "sigHash", log.Topics[0].Hex()) - - if log.Topics[0] == common.BatchConfirmedEventSigHash { - smAbi, err := abi.JSON(bytes.NewReader(common.ServiceManagerAbi)) - if err != nil { - return 0, fmt.Errorf("failed to parse ServiceManager ABI: %w", err) - } - eventAbi, err := smAbi.EventByID(common.BatchConfirmedEventSigHash) - if err != nil { - return 0, fmt.Errorf("failed to parse BatchConfirmed event ABI: %w", err) - } - unpackedData, err := eventAbi.Inputs.Unpack(log.Data) - if err != nil { - return 0, fmt.Errorf("failed to unpack BatchConfirmed log data: %w", err) - } - - // There should be exactly one input in the data field, batchId. - // Labs/eigenda/blob/master/contracts/src/interfaces/IEigenDAServiceManager.sol#L17 - if len(unpackedData) != 1 { - return 0, fmt.Errorf( - "BatchConfirmed log should contain exactly 1 inputs. Found %d", len(unpackedData)) - } - return unpackedData[0].(uint32), nil - } - } - return 0, errors.New("failed to find BatchConfirmed log from the transaction") -} - -func (b *Batcher) getBatchID(ctx context.Context, txReceipt *types.Receipt) (uint32, error) { - const ( - maxRetries = 4 - baseDelay = 1 * time.Second - ) - var ( - batchID uint32 - err error - ) - - batchID, err = b.parseBatchIDFromReceipt(txReceipt) - if err == nil { - return batchID, nil - } - - txHash := txReceipt.TxHash - for i := 0; i < maxRetries; i++ { - retrySec := math.Pow(2, float64(i)) - b.logger.Warn("failed to get transaction receipt, retrying...", - "retryIn", retrySec, - "err", err) - time.Sleep(time.Duration(retrySec) * baseDelay) - - txReceipt, err = b.ethClient.TransactionReceipt(ctx, txHash) - if err != nil { - continue - } - - batchID, err = b.parseBatchIDFromReceipt(txReceipt) - if err == nil { - return batchID, nil - } - } - - if err != nil { - b.logger.Warn("failed to get transaction receipt after retries", - "numRetries", maxRetries, - "err", err) - return 0, err - } - - return batchID, nil -} - -// numBlobsAttestedByQuorum returns two values: -// 1. the number of blobs that have been successfully attested by all quorums -// 2. map[QuorumID]struct{} contains quorums that have been successfully attested by the quorum -// (has at least one blob attested in the quorum) -func numBlobsAttestedByQuorum( - signedQuorums map[core.QuorumID]*core.QuorumResult, - headers []*core.BlobHeader, -) (int, map[core.QuorumID]struct{}) { - numPassed := 0 - quorums := make(map[core.QuorumID]struct{}) - for _, blob := range headers { - thisPassed := true - for _, quorum := range blob.QuorumInfos { - if signedQuorums[quorum.QuorumID].PercentSigned < quorum.ConfirmationThreshold { - thisPassed = false - } else { - quorums[quorum.QuorumID] = struct{}{} - } - } - if thisPassed { - numPassed++ - } - } - - return numPassed, quorums -} - -func isBlobAttested(signedQuorums map[core.QuorumID]*core.QuorumResult, header *core.BlobHeader) bool { - for _, quorum := range header.QuorumInfos { - if _, ok := signedQuorums[quorum.QuorumID]; !ok { - return false - } - if signedQuorums[quorum.QuorumID].PercentSigned < quorum.ConfirmationThreshold { - return false - } - } - return true -} - -func (b *Batcher) signalLiveness() { - select { - case b.HeartbeatChan <- time.Now(): - b.logger.Info("Heartbeat signal sent") - default: - // This case happens if there's no receiver ready to consume the heartbeat signal. - // It prevents the goroutine from blocking if the channel is full or not being listened to. - b.logger.Warn("Heartbeat signal skipped, no receiver on the channel") - } -} diff --git a/disperser/batcher/batcher_test.go b/disperser/batcher/batcher_test.go deleted file mode 100644 index dd66ac4192..0000000000 --- a/disperser/batcher/batcher_test.go +++ /dev/null @@ -1,835 +0,0 @@ -package batcher_test - -import ( - "context" - "encoding/hex" - "errors" - "math/big" - "runtime" - "sync" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/common" - cmock "github.com/Layr-Labs/eigenda/common/mock" - "github.com/Layr-Labs/eigenda/core" - coremock "github.com/Layr-Labs/eigenda/core/mock" - "github.com/Layr-Labs/eigenda/disperser" - bat "github.com/Layr-Labs/eigenda/disperser/batcher" - batchermock "github.com/Layr-Labs/eigenda/disperser/batcher/mock" - "github.com/Layr-Labs/eigenda/disperser/common/inmem" - dmock "github.com/Layr-Labs/eigenda/disperser/mock" - "github.com/Layr-Labs/eigenda/encoding/codec" - "github.com/Layr-Labs/eigenda/encoding/v1/kzg" - "github.com/Layr-Labs/eigenda/encoding/v1/kzg/prover" - "github.com/Layr-Labs/eigenda/test" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - gettysburgAddressBytes = codec.ConvertByPaddingEmptyByte([]byte("Fourscore and seven years ago our fathers brought forth, on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal. Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived, and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting-place for those who here gave their lives, that that nation might live. It is altogether fitting and proper that we should do this. But, in a larger sense, we cannot dedicate, we cannot consecrate—we cannot hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they here gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.")) - handleBatchLivenessChan = make(chan time.Time, 1) -) - -type batcherComponents struct { - transactor *coremock.MockWriter - txnManager *batchermock.MockTxnManager - blobStore *inmem.BlobStore - encoderClient *disperser.LocalEncoderClient - encodingStreamer *bat.EncodingStreamer - ethClient *cmock.MockEthClient - dispatcher *dmock.Dispatcher - chainData *coremock.ChainDataMock -} - -// makeTestEncoder makes an encoder currently using the only supported backend. -func makeTestProver() (*prover.Prover, error) { - config := &kzg.KzgConfig{ - G1Path: "../../resources/srs/g1.point", - G2Path: "../../resources/srs/g2.point", - CacheDir: "../../resources/srs/SRSTables", - SRSOrder: 3000, - SRSNumberToLoad: 3000, - NumWorker: uint64(runtime.GOMAXPROCS(0)), - LoadG2Points: true, - } - - return prover.NewProver(config, nil) -} - -func makeTestBlob(securityParams []*core.SecurityParam) core.Blob { - blob := core.Blob{ - RequestHeader: core.BlobRequestHeader{ - SecurityParams: securityParams, - }, - Data: gettysburgAddressBytes, - } - return blob -} - -func makeBatcher(t *testing.T) (*batcherComponents, *bat.Batcher, func() []time.Time) { - t.Helper() - ctx := t.Context() - logger := test.GetLogger() - - finalizationBlockDelay := uint(75) - - // Core Components - cst, err := coremock.MakeChainDataMock(map[uint8]int{ - 0: 4, - 1: 4, - 2: 6, - }) - assert.NoError(t, err) - cst.On("GetCurrentBlockNumber").Return(uint(10)+finalizationBlockDelay, nil) - asgn := &core.StdAssignmentCoordinator{} - transactor := &coremock.MockWriter{} - transactor.On("OperatorIDToAddress").Return(gethcommon.Address{}, nil) - agg, err := core.NewStdSignatureAggregator(logger, transactor) - assert.NoError(t, err) - p, err := makeTestProver() - assert.NoError(t, err) - - state := cst.GetTotalOperatorState(ctx, 0) - - // Disperser Components - dispatcher := dmock.NewDispatcher(state) - blobStore := &inmem.BlobStore{ - Blobs: make(map[disperser.BlobHash]*inmem.BlobHolder), - Metadata: make(map[disperser.BlobKey]*disperser.BlobMetadata), - } - - pullInterval := 100 * time.Millisecond - config := bat.Config{ - PullInterval: pullInterval, - NumConnections: 1, - EncodingRequestQueueSize: 100, - BatchSizeMBLimit: 100, - SRSOrder: 3000, - MaxNumRetriesPerBlob: 2, - FinalizationBlockDelay: finalizationBlockDelay, - } - timeoutConfig := bat.TimeoutConfig{ - EncodingTimeout: 10 * time.Second, - AttestationTimeout: 10 * time.Second, - BatchAttestationTimeout: 12 * time.Second, - ChainReadTimeout: 10 * time.Second, - ChainWriteTimeout: 10 * time.Second, - TxnBroadcastTimeout: 10 * time.Second, - } - - metrics := bat.NewMetrics("9100", logger) - - encoderClient := disperser.NewLocalEncoderClient(p) - finalizer := batchermock.NewFinalizer() - ethClient := &cmock.MockEthClient{} - txnManager := batchermock.NewTxnManager() - - b, err := bat.NewBatcher(config, timeoutConfig, blobStore, dispatcher, cst, asgn, encoderClient, agg, ethClient, finalizer, transactor, txnManager, logger, metrics, handleBatchLivenessChan) - assert.NoError(t, err) - - var mu sync.Mutex - var heartbeatsReceived []time.Time - doneListening := make(chan bool) - - go func() { - for { - select { - case hb := <-b.HeartbeatChan: - mu.Lock() // Lock before modifying the slice - heartbeatsReceived = append(heartbeatsReceived, hb) - mu.Unlock() - case <-doneListening: - return - } - } - }() - - // Make the batcher - return &batcherComponents{ - transactor: transactor, - txnManager: txnManager, - blobStore: blobStore, - encoderClient: encoderClient, - encodingStreamer: b.EncodingStreamer, - ethClient: ethClient, - dispatcher: dispatcher, - chainData: cst, - }, b, func() []time.Time { - close(doneListening) // Stop the goroutine listening to heartbeats - - mu.Lock() // Lock before reading the slice - defer mu.Unlock() - return heartbeatsReceived - } -} - -func queueBlob(t *testing.T, ctx context.Context, blob *core.Blob, blobStore disperser.BlobStore) (uint64, disperser.BlobKey) { - requestedAt := uint64(time.Now().UnixNano()) - blobKey, err := blobStore.StoreBlob(ctx, blob, requestedAt) - assert.NoError(t, err) - - return requestedAt, blobKey -} - -func TestBatcherIterations(t *testing.T) { - ctx := t.Context() - - blob1 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }}) - blob2 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 1, - AdversaryThreshold: 70, - ConfirmationThreshold: 100, - }}) - components, batcher, getHeartbeats := makeBatcher(t) - components.dispatcher.On("DisperseBatch").Return(map[core.OperatorID]struct{}{}) - - defer func() { - heartbeats := getHeartbeats() - assert.NotEmpty(t, heartbeats, "Expected heartbeats, but none were received") - - // Further assertions can be made here, such as checking the number of heartbeats - // or validating the time intervals between them if needed. - }() - // should be encoding 3 and 0 - logData, err := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000") - assert.NoError(t, err) - - txHash := gethcommon.HexToHash("0x1234") - blockNumber := big.NewInt(123) - receipt := &types.Receipt{ - Logs: []*types.Log{ - { - Topics: []gethcommon.Hash{common.BatchConfirmedEventSigHash, gethcommon.HexToHash("1234")}, - Data: logData, - }, - }, - BlockNumber: blockNumber, - TxHash: txHash, - } - blobStore := components.blobStore - requestedAt1, blobKey1 := queueBlob(t, ctx, &blob1, blobStore) - _, blobKey2 := queueBlob(t, ctx, &blob2, blobStore) - - // Start the batcher - out := make(chan bat.EncodingResultOrStatus) - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - count, size := components.encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, 2, count) - assert.Equal(t, uint64(27631), size) - - txn := types.NewTransaction(0, gethcommon.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) - components.transactor.On("BuildConfirmBatchTxn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - quorumResults := args[2].(map[core.QuorumID]*core.QuorumResult) - assert.Len(t, quorumResults, 2) - assert.Contains(t, quorumResults, core.QuorumID(0)) - assert.Contains(t, quorumResults, core.QuorumID(1)) - - aggSig := args[3].(*core.SignatureAggregation) - assert.Empty(t, aggSig.NonSigners) - assert.Len(t, aggSig.QuorumAggPubKeys, 2) - assert.Contains(t, aggSig.QuorumAggPubKeys, core.QuorumID(0)) - assert.Contains(t, aggSig.QuorumAggPubKeys, core.QuorumID(1)) - assert.Equal(t, aggSig.QuorumResults, map[core.QuorumID]*core.QuorumResult{ - core.QuorumID(0): { - QuorumID: core.QuorumID(0), - PercentSigned: uint8(100), - }, - core.QuorumID(1): { - QuorumID: core.QuorumID(1), - PercentSigned: uint8(100), - }, - }) - }).Return(txn, nil) - components.txnManager.On("ProcessTransaction").Return(nil) - - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) - assert.Greater(t, len(components.txnManager.Requests), 0) - err = batcher.ProcessConfirmedBatch(ctx, &bat.ReceiptOrErr{ - Receipt: receipt, - Err: nil, - Metadata: components.txnManager.Requests[len(components.txnManager.Requests)-1].Metadata, - }) - assert.NoError(t, err) - // Check that the blob was processed - meta1, err := blobStore.GetBlobMetadata(ctx, blobKey1) - assert.NoError(t, err) - assert.Equal(t, blobKey1, meta1.GetBlobKey()) - assert.Equal(t, requestedAt1, meta1.RequestMetadata.RequestedAt) - assert.Equal(t, disperser.Confirmed, meta1.BlobStatus) - assert.Equal(t, meta1.ConfirmationInfo.BatchID, uint32(3)) - assert.Equal(t, meta1.ConfirmationInfo.ConfirmationTxnHash, txHash) - assert.Equal(t, meta1.ConfirmationInfo.ConfirmationBlockNumber, uint32(blockNumber.Int64())) - - meta2, err := blobStore.GetBlobMetadata(ctx, blobKey2) - assert.NoError(t, err) - assert.Equal(t, blobKey2, meta2.GetBlobKey()) - assert.Equal(t, disperser.Confirmed, meta2.BlobStatus) - - res, err := components.encodingStreamer.EncodedBlobstore.GetEncodingResult(meta1.GetBlobKey(), 0) - assert.ErrorContains(t, err, "no such key") - assert.Nil(t, res) - res, err = components.encodingStreamer.EncodedBlobstore.GetEncodingResult(meta2.GetBlobKey(), 1) - assert.ErrorContains(t, err, "no such key") - assert.Nil(t, res) - count, size = components.encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, 0, count) - assert.Equal(t, uint64(0), size) - - // confirmed metadata should be immutable and not be updated - existingBlobIndex := meta1.ConfirmationInfo.BlobIndex - meta1, err = blobStore.MarkBlobConfirmed(ctx, meta1, &disperser.ConfirmationInfo{ - BlobIndex: existingBlobIndex + 1, - }) - assert.NoError(t, err) - // check confirmation info isn't updated - assert.Equal(t, existingBlobIndex, meta1.ConfirmationInfo.BlobIndex) - assert.Equal(t, disperser.Confirmed, meta1.BlobStatus) -} - -func TestBlobFailures(t *testing.T) { - ctx := t.Context() - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }}) - - components, batcher, getHeartbeats := makeBatcher(t) - components.dispatcher.On("DisperseBatch").Return(map[core.OperatorID]struct{}{}) - - defer func() { - heartbeats := getHeartbeats() - assert.Equal(t, 3, len(heartbeats), "Expected heartbeats, but none were received") - }() - - confirmationErr := errors.New("error") - blobStore := components.blobStore - requestedAt, blobKey := queueBlob(t, ctx, &blob, blobStore) - - // Start the batcher - out := make(chan bat.EncodingResultOrStatus) - err := components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - - txn := types.NewTransaction(0, gethcommon.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) - components.transactor.On("BuildConfirmBatchTxn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(txn, nil) - components.txnManager.On("ProcessTransaction").Return(nil) - - // Test with receipt response with error - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) - assert.Greater(t, len(components.txnManager.Requests), 0) - err = batcher.ProcessConfirmedBatch(ctx, &bat.ReceiptOrErr{ - Receipt: nil, - Err: confirmationErr, - Metadata: components.txnManager.Requests[len(components.txnManager.Requests)-1].Metadata, - }) - assert.ErrorIs(t, err, confirmationErr) - - meta, err := blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - assert.Equal(t, blobKey, meta.GetBlobKey()) - assert.Equal(t, requestedAt, meta.RequestMetadata.RequestedAt) - // should be retried - assert.Equal(t, disperser.Processing, meta.BlobStatus) - assert.Equal(t, uint(1), meta.NumRetries) - metadatas, err := blobStore.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.NoError(t, err) - assert.Len(t, metadatas, 1) - encodedResult, err := components.encodingStreamer.EncodedBlobstore.GetEncodingResult(blobKey, 0) - assert.Error(t, err) - assert.Nil(t, encodedResult) - - // Test with receipt response with no block number - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - components.encodingStreamer.ReferenceBlockNumber = 10 - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) - err = batcher.ProcessConfirmedBatch(ctx, &bat.ReceiptOrErr{ - Receipt: &types.Receipt{ - TxHash: gethcommon.HexToHash("0x1234"), - }, - Err: nil, - Metadata: components.txnManager.Requests[len(components.txnManager.Requests)-1].Metadata, - }) - assert.ErrorContains(t, err, "error getting transaction receipt block number") - - meta, err = blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - - // should be retried again - assert.Equal(t, disperser.Processing, meta.BlobStatus) - assert.Equal(t, uint(2), meta.NumRetries) - - // Try again - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - components.encodingStreamer.ReferenceBlockNumber = 10 - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) - - err = batcher.ProcessConfirmedBatch(ctx, &bat.ReceiptOrErr{ - Receipt: &types.Receipt{ - TxHash: gethcommon.HexToHash("0x1234"), - }, - Err: nil, - Metadata: components.txnManager.Requests[len(components.txnManager.Requests)-1].Metadata, - }) - assert.ErrorContains(t, err, "error getting transaction receipt block number") - - meta, err = blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - - // should not be retried again - assert.Equal(t, disperser.Failed, meta.BlobStatus) - assert.Equal(t, uint(2), meta.NumRetries) -} - -// TestBlobRetry tests that the blob that has been dispersed to DA nodes but is pending onchain confirmation isn't re-dispersed. -func TestBlobRetry(t *testing.T) { - ctx := t.Context() - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }}) - - components, batcher, getHeartbeats := makeBatcher(t) - components.dispatcher.On("DisperseBatch").Return(map[core.OperatorID]struct{}{}) - - defer func() { - heartbeats := getHeartbeats() - assert.Equal(t, 1, len(heartbeats), "Expected heartbeats, but none were received") - }() - - blobStore := components.blobStore - _, blobKey := queueBlob(t, ctx, &blob, blobStore) - - // Start the batcher - out := make(chan bat.EncodingResultOrStatus) - err := components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - - encodedResult, err := components.encodingStreamer.EncodedBlobstore.GetEncodingResult(blobKey, 0) - assert.NoError(t, err) - assert.NotNil(t, encodedResult) - - txn := types.NewTransaction(0, gethcommon.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) - components.transactor.On("BuildConfirmBatchTxn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(txn, nil) - components.txnManager.On("ProcessTransaction").Return(nil) - - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) - - // ConfirmBatch transaction has been sent. Waiting for transaction to be confirmed onchain - meta, err := blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - assert.Equal(t, disperser.Dispersing, meta.BlobStatus) - encodedResult, err = components.encodingStreamer.EncodedBlobstore.GetEncodingResult(blobKey, 0) - assert.ErrorContains(t, err, "no such key") - assert.Nil(t, encodedResult) - - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - timer := time.NewTimer(1 * time.Second) - select { - case <-out: - t.Fatal("shouldn't have picked up any blobs to encode") - case <-timer.C: - } - batch, err := components.encodingStreamer.CreateBatch(ctx) - assert.ErrorContains(t, err, "no encoded results") - assert.Nil(t, batch) - - // Shouldn't pick up any blobs to encode - components.encodingStreamer.ReferenceBlockNumber = 12 - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - timer = time.NewTimer(1 * time.Second) - select { - case <-out: - t.Fatal("shouldn't have picked up any blobs to encode") - case <-timer.C: - } - - batch, err = components.encodingStreamer.CreateBatch(ctx) - assert.ErrorContains(t, err, "no encoded results") - assert.Nil(t, batch) - - meta, err = blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - assert.Equal(t, disperser.Dispersing, meta.BlobStatus) - - // Trigger a retry - confirmationErr := errors.New("error") - err = batcher.ProcessConfirmedBatch(ctx, &bat.ReceiptOrErr{ - Receipt: nil, - Err: confirmationErr, - Metadata: components.txnManager.Requests[len(components.txnManager.Requests)-1].Metadata, - }) - assert.ErrorIs(t, err, confirmationErr) - meta, err = blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - assert.Equal(t, disperser.Processing, meta.BlobStatus) - assert.Equal(t, uint(1), meta.NumRetries) - - components.encodingStreamer.ReferenceBlockNumber = 14 - // Should pick up the blob to encode - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - timer = time.NewTimer(1 * time.Second) - var res bat.EncodingResultOrStatus - select { - case res = <-out: - case <-timer.C: - t.Fatal("should have picked up the blob to encode") - } - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, res) - assert.NoError(t, err) - encodedResult, err = components.encodingStreamer.EncodedBlobstore.GetEncodingResult(blobKey, 0) - assert.NoError(t, err) - assert.NotNil(t, encodedResult) -} - -func TestRetryTxnReceipt(t *testing.T) { - ctx := t.Context() - var err error - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }}) - components, batcher, getHeartbeats := makeBatcher(t) - components.dispatcher.On("DisperseBatch").Return(map[core.OperatorID]struct{}{}) - - defer func() { - heartbeats := getHeartbeats() - assert.NotEmpty(t, heartbeats, "Expected heartbeats, but none were received") - - // Further assertions can be made here, such as checking the number of heartbeats - // or validating the time intervals between them if needed. - }() - invalidReceipt := &types.Receipt{ - Logs: []*types.Log{ - { - Topics: []gethcommon.Hash{common.BatchConfirmedEventSigHash, gethcommon.HexToHash("1234")}, - Data: []byte{}, // empty data - }, - }, - BlockNumber: big.NewInt(123), - } - // should be encoding 3 and 0 - validLogData, err := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000") - assert.NoError(t, err) - validReceipt := &types.Receipt{ - Logs: []*types.Log{ - { - Topics: []gethcommon.Hash{common.BatchConfirmedEventSigHash, gethcommon.HexToHash("1234")}, - Data: validLogData, - }, - }, - BlockNumber: big.NewInt(123), - } - - components.ethClient.On("TransactionReceipt").Return(invalidReceipt, nil).Twice() - components.ethClient.On("TransactionReceipt").Return(validReceipt, nil).Once() - blobStore := components.blobStore - requestedAt, blobKey := queueBlob(t, ctx, &blob, blobStore) - - // Start the batcher - out := make(chan bat.EncodingResultOrStatus) - err = components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - - txn := types.NewTransaction(0, gethcommon.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) - components.transactor.On("BuildConfirmBatchTxn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(txn, nil) - components.txnManager.On("ProcessTransaction").Return(nil) - - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) - err = batcher.ProcessConfirmedBatch(ctx, &bat.ReceiptOrErr{ - Receipt: invalidReceipt, - Err: nil, - Metadata: components.txnManager.Requests[len(components.txnManager.Requests)-1].Metadata, - }) - assert.NoError(t, err) - // Check that the blob was processed - meta, err := blobStore.GetBlobMetadata(ctx, blobKey) - assert.NoError(t, err) - assert.Equal(t, blobKey, meta.GetBlobKey()) - assert.Equal(t, requestedAt, meta.RequestMetadata.RequestedAt) - assert.Equal(t, disperser.Confirmed, meta.BlobStatus) - assert.Equal(t, meta.ConfirmationInfo.BatchID, uint32(3)) - components.ethClient.AssertNumberOfCalls(t, "TransactionReceipt", 3) -} - -// TestBlobAttestationFailures tests a case where the attestation fails for all blobs in one quorum, -// in which case the quorum should be omitted from the confirmation transaction. -func TestBlobAttestationFailures(t *testing.T) { - ctx := t.Context() - blob0 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 1, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - }) - - blob1 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 1, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 2, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - }) - - components, batcher, _ := makeBatcher(t) - - blobStore := components.blobStore - _, _ = queueBlob(t, ctx, &blob0, blobStore) - _, _ = queueBlob(t, ctx, &blob1, blobStore) - - // Start the batcher - out := make(chan bat.EncodingResultOrStatus) - err := components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - - components.dispatcher.On("DisperseBatch").Return(map[core.OperatorID]struct{}{ - // operator 5 is only in quorum 2 - coremock.MakeOperatorId(5): {}, - }) - - txn := types.NewTransaction(0, gethcommon.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) - components.transactor.On("BuildConfirmBatchTxn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - quorumResults := args[2].(map[core.QuorumID]*core.QuorumResult) - assert.Len(t, quorumResults, 2) - assert.Contains(t, quorumResults, core.QuorumID(0)) - assert.Contains(t, quorumResults, core.QuorumID(1)) - // should not contain quorum 2 - assert.NotContains(t, quorumResults, core.QuorumID(2)) - - aggSig := args[3].(*core.SignatureAggregation) - assert.Empty(t, aggSig.NonSigners) - assert.NotContains(t, aggSig.QuorumAggPubKeys, core.QuorumID(2)) - assert.NotContains(t, aggSig.QuorumResults, core.QuorumID(2)) - }).Return(txn, nil) - components.txnManager.On("ProcessTransaction").Return(nil) - - // Test with receipt response with error - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) -} - -// TestBlobAttestationFailures2 tests a case where the attestation fails for some blobs in one quorum, -// in which case the quorum should not be omitted from the confirmation transaction. -func TestBlobAttestationFailures2(t *testing.T) { - ctx := t.Context() - blob0 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 2, - AdversaryThreshold: 80, - ConfirmationThreshold: 50, - }, - }) - - blob1 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 2, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - }) - - components, batcher, _ := makeBatcher(t) - - blobStore := components.blobStore - _, _ = queueBlob(t, ctx, &blob0, blobStore) - _, _ = queueBlob(t, ctx, &blob1, blobStore) - - // Start the batcher - out := make(chan bat.EncodingResultOrStatus) - err := components.encodingStreamer.RequestEncoding(ctx, out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - err = components.encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NoError(t, err) - - components.dispatcher.On("DisperseBatch").Return(map[core.OperatorID]struct{}{ - // this operator is only in quorum 2 - coremock.MakeOperatorId(5): {}, - }) - - txn := types.NewTransaction(0, gethcommon.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) - components.transactor.On("BuildConfirmBatchTxn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - quorumResults := args[2].(map[core.QuorumID]*core.QuorumResult) - assert.Len(t, quorumResults, 2) - assert.Contains(t, quorumResults, core.QuorumID(0)) - assert.Contains(t, quorumResults, core.QuorumID(2)) - - aggSig := args[3].(*core.SignatureAggregation) - assert.Len(t, aggSig.NonSigners, 1) - assert.Contains(t, aggSig.QuorumAggPubKeys, core.QuorumID(0)) - assert.Contains(t, aggSig.QuorumAggPubKeys, core.QuorumID(2)) - assert.Equal(t, aggSig.QuorumResults, map[core.QuorumID]*core.QuorumResult{ - core.QuorumID(0): { - QuorumID: core.QuorumID(0), - PercentSigned: uint8(100), - }, - core.QuorumID(2): { - QuorumID: core.QuorumID(2), - PercentSigned: uint8(71), - }, - }) - }).Return(txn, nil) - components.txnManager.On("ProcessTransaction").Return(nil) - - // Test with receipt response with error - err = batcher.HandleSingleBatch(ctx) - assert.NoError(t, err) -} - -func TestBatcherRecoverState(t *testing.T) { - ctx := t.Context() - blob0 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 2, - AdversaryThreshold: 80, - ConfirmationThreshold: 50, - }, - }) - - blob1 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 2, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - }) - - blob2 := makeTestBlob([]*core.SecurityParam{ - { - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - { - QuorumID: 2, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - }) - - components, batcher, _ := makeBatcher(t) - - blobStore := components.blobStore - _, key0 := queueBlob(t, ctx, &blob0, blobStore) - _, key1 := queueBlob(t, ctx, &blob1, blobStore) - _, key2 := queueBlob(t, ctx, &blob2, blobStore) - components.blobStore.Metadata[key2].Expiry = uint64(time.Now().Add(time.Hour * (-24)).Unix()) - - err := blobStore.MarkBlobDispersing(ctx, key0) - assert.NoError(t, err) - err = blobStore.MarkBlobDispersing(ctx, key2) - assert.NoError(t, err) - - b0, err := blobStore.GetBlobMetadata(ctx, key0) - assert.NoError(t, err) - assert.Equal(t, b0.BlobStatus, disperser.Dispersing) - - b1, err := blobStore.GetBlobMetadata(ctx, key1) - assert.NoError(t, err) - assert.Equal(t, b1.BlobStatus, disperser.Processing) - - b2, err := blobStore.GetBlobMetadata(ctx, key2) - assert.NoError(t, err) - assert.Equal(t, b2.BlobStatus, disperser.Dispersing) - err = batcher.RecoverState(ctx) - assert.NoError(t, err) - - b0, err = blobStore.GetBlobMetadata(ctx, key0) - assert.NoError(t, err) - assert.Equal(t, b0.BlobStatus, disperser.Processing) - - b1, err = blobStore.GetBlobMetadata(ctx, key1) - assert.NoError(t, err) - assert.Equal(t, b1.BlobStatus, disperser.Processing) - - b2, err = blobStore.GetBlobMetadata(ctx, key2) - assert.NoError(t, err) - assert.Equal(t, b2.BlobStatus, disperser.Failed) -} diff --git a/disperser/batcher/encoded_blob_store.go b/disperser/batcher/encoded_blob_store.go deleted file mode 100644 index 6deccee54c..0000000000 --- a/disperser/batcher/encoded_blob_store.go +++ /dev/null @@ -1,204 +0,0 @@ -package batcher - -import ( - "fmt" - "sync" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/Layr-Labs/eigensdk-go/logging" -) - -type requestID string - -type encodedBlobStore struct { - mu sync.RWMutex - - requested map[requestID]struct{} - encoded map[requestID]*EncodingResult - // encodedResultSize is the total size of all the chunks in the encoded results in bytes - encodedResultSize uint64 - - logger logging.Logger -} - -// EncodingResult contains information about the encoding of a blob -type EncodingResult struct { - BlobMetadata *disperser.BlobMetadata - ReferenceBlockNumber uint - BlobQuorumInfo *core.BlobQuorumInfo - Commitment *encoding.BlobCommitments - ChunksData *core.ChunksData - Assignments map[core.OperatorID]core.Assignment -} - -// EncodingResultOrStatus is a wrapper for EncodingResult that also contains an error -type EncodingResultOrStatus struct { - EncodingResult - // Err is set if there was an error during encoding - Err error -} - -func newEncodedBlobStore(logger logging.Logger) *encodedBlobStore { - return &encodedBlobStore{ - requested: make(map[requestID]struct{}), - encoded: make(map[requestID]*EncodingResult), - encodedResultSize: 0, - logger: logger, - } -} - -func (e *encodedBlobStore) PutEncodingRequest(blobKey disperser.BlobKey, quorumID core.QuorumID) { - e.mu.Lock() - defer e.mu.Unlock() - - requestID := getRequestID(blobKey, quorumID) - e.requested[requestID] = struct{}{} -} - -func (e *encodedBlobStore) HasEncodingRequested(blobKey disperser.BlobKey, quorumID core.QuorumID, referenceBlockNumber uint) bool { - e.mu.RLock() - defer e.mu.RUnlock() - - requestID := getRequestID(blobKey, quorumID) - if _, ok := e.requested[requestID]; ok { - return true - } - - res, ok := e.encoded[requestID] - if ok && res.ReferenceBlockNumber == referenceBlockNumber { - return true - } - return false -} - -func (e *encodedBlobStore) DeleteEncodingRequest(blobKey disperser.BlobKey, quorumID core.QuorumID) { - e.mu.Lock() - defer e.mu.Unlock() - - requestID := getRequestID(blobKey, quorumID) - if _, ok := e.requested[requestID]; !ok { - return - } - - delete(e.requested, requestID) -} - -func (e *encodedBlobStore) PutEncodingResult(result *EncodingResult) error { - e.mu.Lock() - defer e.mu.Unlock() - - blobKey := disperser.BlobKey{ - BlobHash: result.BlobMetadata.BlobHash, - MetadataHash: result.BlobMetadata.MetadataHash, - } - requestID := getRequestID(blobKey, result.BlobQuorumInfo.QuorumID) - if _, ok := e.requested[requestID]; !ok { - return fmt.Errorf("PutEncodedBlob: no such key (%s) in requested set", requestID) - } - - if _, ok := e.encoded[requestID]; !ok { - e.encodedResultSize += getChunksSize(result) - } - e.encoded[requestID] = result - delete(e.requested, requestID) - - return nil -} - -func (e *encodedBlobStore) GetEncodingResult(blobKey disperser.BlobKey, quorumID core.QuorumID) (*EncodingResult, error) { - e.mu.RLock() - defer e.mu.RUnlock() - - requestID := getRequestID(blobKey, quorumID) - if _, ok := e.encoded[requestID]; !ok { - return nil, fmt.Errorf("GetEncodedBlob: no such key (%s) in encoded set", requestID) - } - - return e.encoded[requestID], nil -} - -func (e *encodedBlobStore) DeleteEncodingResult(blobKey disperser.BlobKey, quorumID core.QuorumID) { - e.mu.Lock() - defer e.mu.Unlock() - - requestID := getRequestID(blobKey, quorumID) - encodedResult, ok := e.encoded[requestID] - if !ok { - return - } - - delete(e.encoded, requestID) - e.encodedResultSize -= getChunksSize(encodedResult) -} - -// PopLatestEncodingResults returns all the encoded results that are pending dispersal and deletes them along with stale results that are older than the given reference block -func (e *encodedBlobStore) PopLatestEncodingResults(refBlockNumber uint) []*EncodingResult { - e.mu.Lock() - defer e.mu.Unlock() - - fetched := make([]*EncodingResult, 0) - staleCount := 0 - for k, encodedResult := range e.encoded { - if encodedResult.ReferenceBlockNumber == refBlockNumber { - fetched = append(fetched, encodedResult) - // this is safe: https://go.dev/doc/effective_go#for - delete(e.encoded, k) - e.encodedResultSize -= getChunksSize(encodedResult) - } else if encodedResult.ReferenceBlockNumber < refBlockNumber { - delete(e.encoded, k) - staleCount++ - e.encodedResultSize -= getChunksSize(encodedResult) - } else { - e.logger.Error("unexpected case", "refBlockNumber", encodedResult.ReferenceBlockNumber, "refBlockNumber", refBlockNumber) - } - } - e.logger.Debug("consumed encoded results", "fetched", len(fetched), "stale", staleCount, "refBlockNumber", refBlockNumber, "encodedSize", e.encodedResultSize) - - return fetched -} - -// GetNewAndDeleteStaleEncodingResults returns all the fresh encoded results that are pending dispersal, and deletes all the stale results that are older than the given block number -func (e *encodedBlobStore) GetNewAndDeleteStaleEncodingResults(blockNumber uint) []*EncodingResult { - e.mu.Lock() - defer e.mu.Unlock() - fetched := make([]*EncodingResult, 0) - staleCount := 0 - pendingConfirmation := 0 - for k, encodedResult := range e.encoded { - if encodedResult.ReferenceBlockNumber == blockNumber { - fetched = append(fetched, encodedResult) - } else if encodedResult.ReferenceBlockNumber < blockNumber { - // this is safe: https://go.dev/doc/effective_go#for - delete(e.encoded, k) - staleCount++ - e.encodedResultSize -= getChunksSize(encodedResult) - } else { - e.logger.Error("unexpected case", "refBlockNumber", encodedResult.ReferenceBlockNumber, "blockNumber", blockNumber) - } - } - e.logger.Debug("consumed encoded results", "fetched", len(fetched), "stale", staleCount, "pendingConfirmation", pendingConfirmation, "blockNumber", blockNumber, "encodedSize", e.encodedResultSize) - - return fetched -} - -// GetEncodedResultSize returns the total size of all the chunks in the encoded results in bytes -func (e *encodedBlobStore) GetEncodedResultSize() (int, uint64) { - e.mu.RLock() - defer e.mu.RUnlock() - - return len(e.encoded), e.encodedResultSize -} - -func getRequestID(key disperser.BlobKey, quorumID core.QuorumID) requestID { - return requestID(fmt.Sprintf("%s-%d", key.String(), quorumID)) -} - -// getChunksSize returns the total size of all the chunks in the encoded result in bytes -func getChunksSize(result *EncodingResult) uint64 { - if result == nil || result.ChunksData == nil { - return 0 - } - return result.ChunksData.Size() -} diff --git a/disperser/batcher/encoding_streamer.go b/disperser/batcher/encoding_streamer.go deleted file mode 100644 index 30ee9a574b..0000000000 --- a/disperser/batcher/encoding_streamer.go +++ /dev/null @@ -1,726 +0,0 @@ -package batcher - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "strings" - "sync" - "time" - - "github.com/Layr-Labs/eigenda/common" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/Layr-Labs/eigensdk-go/logging" - lru "github.com/hashicorp/golang-lru/v2" - "github.com/wealdtech/go-merkletree/v2" - grpc_metadata "google.golang.org/grpc/metadata" -) - -const encodingInterval = 2 * time.Second - -const operatorStateCacheSize = 32 - -var errNoEncodedResults = errors.New("no encoded results") - -type EncodedSizeNotifier struct { - mu sync.Mutex - - Notify chan struct{} - // threshold is the size of the total encoded blob results in bytes that triggers the notifier - threshold uint64 - // active is set to false after the notifier is triggered to prevent it from triggering again for the same batch - // This is reset when CreateBatch is called and the encoded results have been consumed - active bool -} - -type StreamerConfig struct { - - // SRSOrder is the order of the SRS used for encoding - SRSOrder int - // EncodingRequestTimeout is the timeout for each encoding request - EncodingRequestTimeout time.Duration - - // ChainStateTimeout is the timeout used for getting the chainstate - ChainStateTimeout time.Duration - - // EncodingQueueLimit is the maximum number of encoding requests that can be queued - EncodingQueueLimit int - - // TargetNumChunks is the target number of chunks per encoded blob - TargetNumChunks uint64 - - // Maximum number of Blobs to fetch from store - MaxBlobsToFetchFromStore int - - FinalizationBlockDelay uint -} - -type EncodingStreamer struct { - StreamerConfig - - mu sync.RWMutex - - EncodedBlobstore *encodedBlobStore - ReferenceBlockNumber uint - Pool common.WorkerPool - EncodedSizeNotifier *EncodedSizeNotifier - - blobStore disperser.BlobStore - chainState core.IndexedChainState - encoderClient disperser.EncoderClient - assignmentCoordinator core.AssignmentCoordinator - - encodingCtxCancelFuncs []context.CancelFunc - - metrics *EncodingStreamerMetrics - batcherMetrics *Metrics - logger logging.Logger - - // Used to keep track of the last evaluated key for fetching metadatas - exclusiveStartKey *disperser.BlobStoreExclusiveStartKey - - operatorStateCache *lru.Cache[string, *core.IndexedOperatorState] -} - -type batch struct { - EncodedBlobs []core.EncodedBlob - BlobMetadata []*disperser.BlobMetadata - BlobHeaders []*core.BlobHeader - BatchHeader *core.BatchHeader - State *core.IndexedOperatorState - MerkleTree *merkletree.MerkleTree -} - -func NewEncodedSizeNotifier(notify chan struct{}, threshold uint64) *EncodedSizeNotifier { - return &EncodedSizeNotifier{ - Notify: notify, - threshold: threshold, - active: true, - } -} - -func NewEncodingStreamer( - config StreamerConfig, - blobStore disperser.BlobStore, - chainState core.IndexedChainState, - encoderClient disperser.EncoderClient, - assignmentCoordinator core.AssignmentCoordinator, - encodedSizeNotifier *EncodedSizeNotifier, - workerPool common.WorkerPool, - metrics *EncodingStreamerMetrics, - batcherMetrics *Metrics, - logger logging.Logger) (*EncodingStreamer, error) { - if config.EncodingQueueLimit <= 0 { - return nil, errors.New("EncodingQueueLimit should be greater than 0") - } - operatorStateCache, err := lru.New[string, *core.IndexedOperatorState](operatorStateCacheSize) - if err != nil { - return nil, err - } - return &EncodingStreamer{ - StreamerConfig: config, - EncodedBlobstore: newEncodedBlobStore(logger), - ReferenceBlockNumber: uint(0), - Pool: workerPool, - EncodedSizeNotifier: encodedSizeNotifier, - blobStore: blobStore, - chainState: chainState, - encoderClient: encoderClient, - assignmentCoordinator: assignmentCoordinator, - encodingCtxCancelFuncs: make([]context.CancelFunc, 0), - metrics: metrics, - batcherMetrics: batcherMetrics, - logger: logger.With("component", "EncodingStreamer"), - exclusiveStartKey: nil, - operatorStateCache: operatorStateCache, - }, nil -} - -func (e *EncodingStreamer) Start(ctx context.Context) error { - encoderChan := make(chan EncodingResultOrStatus) - - // goroutine for handling blob encoding responses - go func() { - for { - select { - case <-ctx.Done(): - return - case response := <-encoderChan: - err := e.ProcessEncodedBlobs(ctx, response) - if err != nil { - if strings.Contains(err.Error(), context.Canceled.Error()) { - // ignore canceled errors because canceled encoding requests are normal - continue - } - if strings.Contains(err.Error(), "too many requests") { - e.logger.Warn("encoding request ratelimited", "err", err) - } else if strings.Contains(err.Error(), "connection reset by peer") { - e.logger.Warn("encoder connection reset by peer", "err", err) - } else if strings.Contains(err.Error(), "error reading from server: EOF") { - e.logger.Warn("encoder request dropped", "err", err) - } else if strings.Contains(err.Error(), "connection refused") { - e.logger.Warn("encoder connection refused", "err", err) - } else { - e.logger.Error("error processing encoded blobs", "err", err) - } - } - } - } - }() - - // goroutine for making blob encoding requests - go func() { - ticker := time.NewTicker(encodingInterval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - err := e.RequestEncoding(ctx, encoderChan) - if err != nil { - e.logger.Warn("error requesting encoding", "err", err) - } - } - } - }() - - return nil -} - -func (e *EncodingStreamer) dedupRequests(metadatas []*disperser.BlobMetadata, referenceBlockNumber uint) []*disperser.BlobMetadata { - res := make([]*disperser.BlobMetadata, 0) - for _, meta := range metadatas { - allQuorumsRequested := true - // check if the blob has been requested for all quorums - for _, quorum := range meta.RequestMetadata.SecurityParams { - if !e.EncodedBlobstore.HasEncodingRequested(meta.GetBlobKey(), quorum.QuorumID, referenceBlockNumber) { - allQuorumsRequested = false - break - } - } - if !allQuorumsRequested { - res = append(res, meta) - } - } - - return res -} - -func (e *EncodingStreamer) RequestEncoding(ctx context.Context, encoderChan chan EncodingResultOrStatus) error { - stageTimer := time.Now() - // pull new blobs and send to encoder - e.mu.Lock() - metadatas, newExclusiveStartKey, err := e.blobStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Processing, int32(e.StreamerConfig.MaxBlobsToFetchFromStore), e.exclusiveStartKey) - e.exclusiveStartKey = newExclusiveStartKey - e.mu.Unlock() - - if err != nil { - return fmt.Errorf("error getting blob metadatas: %w", err) - } - if len(metadatas) == 0 { - e.logger.Info("no new metadatas to encode") - return nil - } - - // read lock to access e.ReferenceBlockNumber - e.mu.RLock() - referenceBlockNumber := e.ReferenceBlockNumber - e.mu.RUnlock() - - if referenceBlockNumber == 0 { - // Update the reference block number for the next iteration - blockNumber, err := e.chainState.GetCurrentBlockNumber(ctx) - if err != nil { - return fmt.Errorf("failed to get current block number, won't request encoding: %w", err) - } else { - if blockNumber > e.FinalizationBlockDelay { - blockNumber -= e.FinalizationBlockDelay - } - - e.mu.Lock() - e.ReferenceBlockNumber = blockNumber - e.mu.Unlock() - referenceBlockNumber = blockNumber - } - } - - e.logger.Debug("metadata in processing status", "numMetadata", len(metadatas)) - metadatas = e.dedupRequests(metadatas, referenceBlockNumber) - if len(metadatas) == 0 { - e.logger.Info("no new metadatas to encode") - return nil - } - - waitingQueueSize := e.Pool.WaitingQueueSize() - numMetadatastoProcess := e.EncodingQueueLimit - waitingQueueSize - if numMetadatastoProcess > len(metadatas) { - numMetadatastoProcess = len(metadatas) - } - if numMetadatastoProcess <= 0 { - // encoding queue is full - e.logger.Warn("worker pool queue is full. skipping this round of encoding requests", "waitingQueueSize", waitingQueueSize, "encodingQueueLimit", e.EncodingQueueLimit) - return nil - } - // only process subset of blobs so it doesn't exceed the EncodingQueueLimit - // TODO: this should be done at the request time and keep the cursor so that we don't fetch the same metadata every time - metadatas = metadatas[:numMetadatastoProcess] - - e.logger.Debug("new metadatas to encode", "numMetadata", len(metadatas), "duration", time.Since(stageTimer)) - - // Get the operator state - - timeoutCtx, cancel := context.WithTimeout(ctx, e.ChainStateTimeout) - defer cancel() - state, err := e.getOperatorState(timeoutCtx, metadatas, referenceBlockNumber) - if err != nil { - return fmt.Errorf("error getting operator state: %w", err) - } - metadatas = e.validateMetadataQuorums(metadatas, state) - - metadataByKey := make(map[disperser.BlobKey]*disperser.BlobMetadata, 0) - for _, metadata := range metadatas { - metadataByKey[metadata.GetBlobKey()] = metadata - } - - stageTimer = time.Now() - blobs, err := e.blobStore.GetBlobsByMetadata(ctx, metadatas) - if err != nil { - return fmt.Errorf("error getting blobs from blob store: %w", err) - } - e.logger.Debug("retrieved blobs to encode", "numBlobs", len(blobs), "duration", time.Since(stageTimer)) - - e.logger.Debug("encoding blobs...", "numBlobs", len(blobs), "blockNumber", referenceBlockNumber) - - for i := range metadatas { - metadata := metadatas[i] - - e.RequestEncodingForBlob(ctx, metadata, blobs[metadata.GetBlobKey()], state, referenceBlockNumber, encoderChan) - } - - return nil -} - -type pendingRequestInfo struct { - BlobQuorumInfo *core.BlobQuorumInfo - EncodingParams encoding.EncodingParams - Assignments map[core.OperatorID]core.Assignment -} - -func (e *EncodingStreamer) RequestEncodingForBlob(ctx context.Context, metadata *disperser.BlobMetadata, blob *core.Blob, state *core.IndexedOperatorState, referenceBlockNumber uint, encoderChan chan EncodingResultOrStatus) { - - // Validate the encoding parameters for each quorum - - blobKey := metadata.GetBlobKey() - - pending := make([]pendingRequestInfo, 0, len(metadata.RequestMetadata.SecurityParams)) - - for ind := range metadata.RequestMetadata.SecurityParams { - - quorum := metadata.RequestMetadata.SecurityParams[ind] - - // Check if the blob has already been encoded for this quorum - if e.EncodedBlobstore.HasEncodingRequested(blobKey, quorum.QuorumID, referenceBlockNumber) { - continue - } - - blobLength := encoding.GetBlobLength(uint32(metadata.RequestMetadata.BlobSize)) - - chunkLength, err := e.assignmentCoordinator.CalculateChunkLength( - state.OperatorState, uint(blobLength), e.TargetNumChunks, quorum) - if err != nil { - e.logger.Error("error calculating chunk length", "err", err) - continue - } - - blobQuorumInfo := &core.BlobQuorumInfo{ - SecurityParam: core.SecurityParam{ - QuorumID: quorum.QuorumID, - AdversaryThreshold: quorum.AdversaryThreshold, - ConfirmationThreshold: quorum.ConfirmationThreshold, - QuorumRate: quorum.QuorumRate, - }, - ChunkLength: chunkLength, - } - assignments, info, err := e.assignmentCoordinator.GetAssignments( - state.OperatorState, uint(blobLength), blobQuorumInfo) - if err != nil { - e.logger.Error("error getting assignments", "err", err) - continue - } - - params := encoding.ParamsFromMins(uint64(chunkLength), info.TotalChunks) - - err = encoding.ValidateEncodingParamsAndBlobLength(params, uint64(blobLength), uint64(e.SRSOrder)) - if err != nil { - e.logger.Error("invalid encoding params", "err", err) - // Cancel the blob - err := e.blobStore.MarkBlobFailed(ctx, blobKey) - if err != nil { - e.logger.Error("error marking blob failed", "err", err) - } - return - } - - pending = append(pending, pendingRequestInfo{ - BlobQuorumInfo: blobQuorumInfo, - EncodingParams: params, - Assignments: assignments, - }) - } - - if len(pending) > 0 { - requestTime := time.Unix(0, int64(metadata.RequestMetadata.RequestedAt)) - e.batcherMetrics.ObserveBlobAge("encoding_requested", float64(time.Since(requestTime).Milliseconds())) - } - - // Execute the encoding requests - for ind := range pending { - res := pending[ind] - - // Create a new context for each encoding request - // This allows us to cancel all outstanding encoding requests when we create a new batch - // This is necessary because an encoding request is dependent on the reference block number - // If the reference block number changes, we need to cancel all outstanding encoding requests - // and re-request them with the new reference block number - encodingCtx, cancel := context.WithTimeout(ctx, e.EncodingRequestTimeout) - e.mu.Lock() - e.encodingCtxCancelFuncs = append(e.encodingCtxCancelFuncs, cancel) - e.mu.Unlock() - - // Add headers for routing - md := grpc_metadata.New(map[string]string{ - "content-type": "application/grpc", - "x-payload-size": fmt.Sprintf("%d", len(blob.Data)), - }) - encodingCtx = grpc_metadata.NewOutgoingContext(encodingCtx, md) - - e.Pool.Submit(func() { - defer cancel() - start := time.Now() - commits, chunks, err := e.encoderClient.EncodeBlob(encodingCtx, blob.Data, res.EncodingParams) - if err != nil { - encoderChan <- EncodingResultOrStatus{Err: fmt.Errorf("encoderClient.EncodeBlob: %w", err), EncodingResult: EncodingResult{ - BlobMetadata: metadata, - BlobQuorumInfo: res.BlobQuorumInfo, - }} - e.metrics.ObserveEncodingLatency("failed", res.BlobQuorumInfo.QuorumID, len(blob.Data), float64(time.Since(start).Milliseconds())) - return - } - - encoderChan <- EncodingResultOrStatus{ - EncodingResult: EncodingResult{ - BlobMetadata: metadata, - ReferenceBlockNumber: referenceBlockNumber, - BlobQuorumInfo: res.BlobQuorumInfo, - Commitment: commits, - ChunksData: chunks, - Assignments: res.Assignments, - }, - Err: nil, - } - e.metrics.ObserveEncodingLatency("success", res.BlobQuorumInfo.QuorumID, len(blob.Data), float64(time.Since(start).Milliseconds())) - }) - e.EncodedBlobstore.PutEncodingRequest(blobKey, res.BlobQuorumInfo.QuorumID) - } -} - -func (e *EncodingStreamer) ProcessEncodedBlobs(ctx context.Context, result EncodingResultOrStatus) error { - if result.Err != nil { - e.EncodedBlobstore.DeleteEncodingRequest(result.BlobMetadata.GetBlobKey(), result.BlobQuorumInfo.QuorumID) - return fmt.Errorf("error encoding blob: %w", result.Err) - } - - err := e.EncodedBlobstore.PutEncodingResult(&result.EncodingResult) - if err != nil { - return fmt.Errorf("failed to putEncodedBlob: %w", err) - } - - requestTime := time.Unix(0, int64(result.BlobMetadata.RequestMetadata.RequestedAt)) - e.batcherMetrics.ObserveBlobAge("encoded", float64(time.Since(requestTime).Milliseconds())) - e.batcherMetrics.IncrementBlobSize("encoded", result.BlobQuorumInfo.QuorumID, int(result.BlobMetadata.RequestMetadata.BlobSize)) - - count, encodedSize := e.EncodedBlobstore.GetEncodedResultSize() - e.metrics.UpdateEncodedBlobs(count, encodedSize) - if e.EncodedSizeNotifier.threshold > 0 && encodedSize >= e.EncodedSizeNotifier.threshold { - e.EncodedSizeNotifier.mu.Lock() - - if e.EncodedSizeNotifier.active { - e.logger.Info("encoded size threshold reached", "size", encodedSize) - e.EncodedSizeNotifier.Notify <- struct{}{} - // make sure this doesn't keep triggering before encoded blob store is reset - e.EncodedSizeNotifier.active = false - } - e.EncodedSizeNotifier.mu.Unlock() - } - - return nil -} - -func (e *EncodingStreamer) UpdateReferenceBlock(currentBlockNumber uint) error { - blockNumber := currentBlockNumber - if blockNumber > e.FinalizationBlockDelay { - blockNumber -= e.FinalizationBlockDelay - } - if e.ReferenceBlockNumber > blockNumber { - return fmt.Errorf("reference block number is being updated to a lower value: from %d to %d", e.ReferenceBlockNumber, blockNumber) - } - e.mu.Lock() - defer e.mu.Unlock() - if e.ReferenceBlockNumber < blockNumber { - // Wipe out the encoding results based on previous reference block number - _ = e.EncodedBlobstore.PopLatestEncodingResults(e.ReferenceBlockNumber) - } - e.ReferenceBlockNumber = blockNumber - return nil -} - -// CreateBatch makes a batch from all blobs in the encoded blob store. -// If successful, it returns a batch, and updates the reference block number for next batch to use. -// Otherwise, it returns an error and keeps the blobs in the encoded blob store. -// This function is meant to be called periodically in a single goroutine as it resets the state of the encoded blob store. -func (e *EncodingStreamer) CreateBatch(ctx context.Context) (*batch, error) { - // lock to update e.ReferenceBlockNumber - e.mu.Lock() - defer e.mu.Unlock() - // Cancel outstanding encoding requests - // Assumption: `CreateBatch` will be called at an interval longer than time it takes to encode a single blob - if len(e.encodingCtxCancelFuncs) > 0 { - e.logger.Info("canceling outstanding encoding requests", "count", len(e.encodingCtxCancelFuncs)) - for _, cancel := range e.encodingCtxCancelFuncs { - cancel() - } - e.encodingCtxCancelFuncs = make([]context.CancelFunc, 0) - } - - // If there were no requested blobs between the last batch and now, there is no need to create a new batch - if e.ReferenceBlockNumber == 0 { - blockNumber, err := e.chainState.GetCurrentBlockNumber(ctx) - if err != nil { - e.logger.Error("failed to get current block number. will not clean up the encoded blob store.", "err", err) - } else { - _ = e.EncodedBlobstore.GetNewAndDeleteStaleEncodingResults(blockNumber) - } - return nil, errNoEncodedResults - } - - // Delete any encoded results that are not from the current batching iteration (i.e. that has different reference block number) - // If any pending encoded results are discarded here, it will be re-requested in the next iteration - encodedResults := e.EncodedBlobstore.GetNewAndDeleteStaleEncodingResults(e.ReferenceBlockNumber) - - // Reset the notifier - e.EncodedSizeNotifier.mu.Lock() - e.EncodedSizeNotifier.active = true - e.EncodedSizeNotifier.mu.Unlock() - - e.logger.Info("creating a batch...", "numBlobs", len(encodedResults), "refblockNumber", e.ReferenceBlockNumber) - if len(encodedResults) == 0 { - return nil, errNoEncodedResults - } - - encodedBlobByKey := make(map[disperser.BlobKey]core.EncodedBlob) - blobQuorums := make(map[disperser.BlobKey][]*core.BlobQuorumInfo) - blobHeaderByKey := make(map[disperser.BlobKey]*core.BlobHeader) - metadataByKey := make(map[disperser.BlobKey]*disperser.BlobMetadata) - for i := range encodedResults { - // each result represent an encoded result per (blob, quorum param) - // if the same blob has been dispersed multiple time with different security params, - // there will be multiple encoded results for that (blob, quorum) - result := encodedResults[i] - blobKey := result.BlobMetadata.GetBlobKey() - if _, ok := encodedBlobByKey[blobKey]; !ok { - metadataByKey[blobKey] = result.BlobMetadata - blobQuorums[blobKey] = make([]*core.BlobQuorumInfo, 0) - blobHeader := &core.BlobHeader{ - BlobCommitments: *result.Commitment, - } - blobHeaderByKey[blobKey] = blobHeader - encodedBlobByKey[blobKey] = core.EncodedBlob{ - BlobHeader: blobHeader, - EncodedBundlesByOperator: make(map[core.OperatorID]core.EncodedBundles), - } - } - - // Populate the assigned bundles - for opID, assignment := range result.Assignments { - bundles, ok := encodedBlobByKey[blobKey].EncodedBundlesByOperator[opID] - if !ok { - encodedBlobByKey[blobKey].EncodedBundlesByOperator[opID] = make(core.EncodedBundles) - bundles = encodedBlobByKey[blobKey].EncodedBundlesByOperator[opID] - } - bundles[result.BlobQuorumInfo.QuorumID] = new(core.ChunksData) - bundles[result.BlobQuorumInfo.QuorumID].Format = result.ChunksData.Format - bundles[result.BlobQuorumInfo.QuorumID].Chunks = append(bundles[result.BlobQuorumInfo.QuorumID].Chunks, result.ChunksData.Chunks[assignment.StartIndex:assignment.StartIndex+assignment.NumChunks]...) - bundles[result.BlobQuorumInfo.QuorumID].ChunkLen = result.ChunksData.ChunkLen - } - - blobQuorums[blobKey] = append(blobQuorums[blobKey], result.BlobQuorumInfo) - } - - // Populate the blob quorum infos - for blobKey, encodedBlob := range encodedBlobByKey { - encodedBlob.BlobHeader.QuorumInfos = blobQuorums[blobKey] - } - - for blobKey, metadata := range metadataByKey { - quorumPresent := make(map[core.QuorumID]bool) - for _, quorum := range blobQuorums[blobKey] { - quorumPresent[quorum.QuorumID] = true - } - // Check if the blob has valid quorums. If any of the quorums are not valid, delete the blobKey - for _, quorum := range metadata.RequestMetadata.SecurityParams { - _, ok := quorumPresent[quorum.QuorumID] - if !ok { - // Delete the blobKey. These encoded blobs will be automatically removed by the next run of - // RequestEncoding - delete(metadataByKey, blobKey) - break - } - } - } - - if len(metadataByKey) == 0 { - return nil, errNoEncodedResults - } - - // Transform maps to slices so orders in different slices match - encodedBlobs := make([]core.EncodedBlob, 0, len(metadataByKey)) - blobHeaders := make([]*core.BlobHeader, 0, len(metadataByKey)) - metadatas := make([]*disperser.BlobMetadata, 0, len(metadataByKey)) - for key := range metadataByKey { - err := e.transitionBlobToDispersing(ctx, metadataByKey[key]) - if err != nil { - continue - } - encodedBlobs = append(encodedBlobs, encodedBlobByKey[key]) - blobHeaders = append(blobHeaders, blobHeaderByKey[key]) - metadatas = append(metadatas, metadataByKey[key]) - } - - timeoutCtx, cancel := context.WithTimeout(context.Background(), e.ChainStateTimeout) - defer cancel() - - state, err := e.getOperatorState(timeoutCtx, metadatas, e.ReferenceBlockNumber) - if err != nil { - for _, metadata := range metadatas { - _ = e.handleFailedMetadata(ctx, metadata) - } - return nil, err - } - - // Populate the batch header - batchHeader := &core.BatchHeader{ - ReferenceBlockNumber: e.ReferenceBlockNumber, - BatchRoot: [32]byte{}, - } - - tree, err := batchHeader.SetBatchRoot(blobHeaders) - if err != nil { - for _, metadata := range metadatas { - _ = e.handleFailedMetadata(ctx, metadata) - } - return nil, err - } - - e.ReferenceBlockNumber = 0 - - return &batch{ - EncodedBlobs: encodedBlobs, - BatchHeader: batchHeader, - BlobHeaders: blobHeaders, - BlobMetadata: metadatas, - State: state, - MerkleTree: tree, - }, nil -} - -func (e *EncodingStreamer) handleFailedMetadata(ctx context.Context, metadata *disperser.BlobMetadata) error { - err := e.blobStore.MarkBlobProcessing(ctx, metadata.GetBlobKey()) - if err != nil { - e.logger.Error("error marking blob as processing", "err", err) - } - - return err -} - -func (e *EncodingStreamer) transitionBlobToDispersing(ctx context.Context, metadata *disperser.BlobMetadata) error { - blobKey := metadata.GetBlobKey() - err := e.blobStore.MarkBlobDispersing(ctx, blobKey) - if err != nil { - e.logger.Error("error marking blob as dispersing", "err", err, "blobKey", blobKey.String()) - return err - } - // remove encoded blob from storage so we don't disperse it again - e.RemoveEncodedBlob(metadata) - return nil -} - -func (e *EncodingStreamer) RemoveEncodedBlob(metadata *disperser.BlobMetadata) { - for _, sp := range metadata.RequestMetadata.SecurityParams { - e.EncodedBlobstore.DeleteEncodingResult(metadata.GetBlobKey(), sp.QuorumID) - } -} - -// getOperatorState returns the operator state for the blobs that have valid quorums -func (e *EncodingStreamer) getOperatorState(ctx context.Context, metadatas []*disperser.BlobMetadata, blockNumber uint) (*core.IndexedOperatorState, error) { - - quorums := make(map[core.QuorumID]QuorumInfo, 0) - for _, metadata := range metadatas { - for _, quorum := range metadata.RequestMetadata.SecurityParams { - quorums[quorum.QuorumID] = QuorumInfo{} - } - } - - quorumIds := make([]core.QuorumID, len(quorums)) - i := 0 - for id := range quorums { - quorumIds[i] = id - i++ - } - - cacheKey := computeCacheKey(blockNumber, quorumIds) - if val, ok := e.operatorStateCache.Get(cacheKey); ok { - return val, nil - } - // GetIndexedOperatorState should return state for valid quorums only - state, err := e.chainState.GetIndexedOperatorState(ctx, blockNumber, quorumIds) - if err != nil { - return nil, fmt.Errorf("error getting operator state at block number %d: %w", blockNumber, err) - } - e.operatorStateCache.Add(cacheKey, state) - return state, nil -} - -// It also returns the list of valid blob metadatas (i.e. blobs that have valid quorums) -func (e *EncodingStreamer) validateMetadataQuorums(metadatas []*disperser.BlobMetadata, state *core.IndexedOperatorState) []*disperser.BlobMetadata { - validMetadata := make([]*disperser.BlobMetadata, 0) - for _, metadata := range metadatas { - valid := true - for _, quorum := range metadata.RequestMetadata.SecurityParams { - if aggKey, ok := state.AggKeys[quorum.QuorumID]; !ok || aggKey == nil { - e.logger.Warn("got blob with a quorum without APK. Will skip.", "blobKey", metadata.GetBlobKey(), "quorum", quorum.QuorumID) - valid = false - } - } - if valid { - validMetadata = append(validMetadata, metadata) - } else { - _, err := e.blobStore.HandleBlobFailure(context.Background(), metadata, 0) - if err != nil { - e.logger.Error("error handling blob failure", "err", err) - } - } - } - return validMetadata -} - -func computeCacheKey(blockNumber uint, quorumIDs []uint8) string { - bytes := make([]byte, 8+len(quorumIDs)) - binary.LittleEndian.PutUint64(bytes, uint64(blockNumber)) - copy(bytes[8:], quorumIDs) - return string(bytes) -} diff --git a/disperser/batcher/encoding_streamer_test.go b/disperser/batcher/encoding_streamer_test.go deleted file mode 100644 index 33d9796003..0000000000 --- a/disperser/batcher/encoding_streamer_test.go +++ /dev/null @@ -1,748 +0,0 @@ -package batcher_test - -import ( - "crypto/rand" - "errors" - "testing" - "time" - - cmock "github.com/Layr-Labs/eigenda/common/mock" - "github.com/Layr-Labs/eigenda/core" - coremock "github.com/Layr-Labs/eigenda/core/mock" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/batcher" - "github.com/Layr-Labs/eigenda/disperser/common/inmem" - "github.com/Layr-Labs/eigenda/disperser/mock" - "github.com/Layr-Labs/eigenda/test" - "github.com/gammazero/workerpool" - "github.com/stretchr/testify/assert" - tmock "github.com/stretchr/testify/mock" -) - -var ( - streamerConfig = batcher.StreamerConfig{ - SRSOrder: 300000, - EncodingRequestTimeout: 5 * time.Second, - EncodingQueueLimit: 100, - MaxBlobsToFetchFromStore: 10, - FinalizationBlockDelay: 75, - } -) - -const numOperators = 10 - -type components struct { - blobStore disperser.BlobStore - chainDataMock *coremock.ChainDataMock - encoderClient *disperser.LocalEncoderClient -} - -func createEncodingStreamer(t *testing.T, initialBlockNumber uint, batchThreshold uint64, streamerConfig batcher.StreamerConfig) (*batcher.EncodingStreamer, *components) { - t.Helper() - - logger := test.GetLogger() - blobStore := inmem.NewBlobStore() - cst, err := coremock.MakeChainDataMock(map[uint8]int{ - 0: numOperators, - 1: numOperators, - 2: numOperators, - }) - assert.Nil(t, err) - p, err := makeTestProver() - assert.Nil(t, err) - encoderClient := disperser.NewLocalEncoderClient(p) - asgn := &core.StdAssignmentCoordinator{} - sizeNotifier := batcher.NewEncodedSizeNotifier(make(chan struct{}, 1), batchThreshold) - workerpool := workerpool.New(5) - metrics := batcher.NewMetrics("9100", logger) - encodingStreamer, err := batcher.NewEncodingStreamer(streamerConfig, blobStore, cst, encoderClient, asgn, sizeNotifier, workerpool, metrics.EncodingStreamerMetrics, metrics, logger) - assert.Nil(t, err) - encodingStreamer.ReferenceBlockNumber = initialBlockNumber - - return encodingStreamer, &components{ - blobStore: blobStore, - chainDataMock: cst, - encoderClient: encoderClient, - } -} - -func TestEncodingQueueLimit(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - blobStore := inmem.NewBlobStore() - cst, err := coremock.MakeChainDataMock(map[uint8]int{ - 0: numOperators, - 1: numOperators, - 2: numOperators, - }) - assert.Nil(t, err) - encoderClient := mock.NewMockEncoderClient() - encoderClient.On("EncodeBlob", tmock.Anything, tmock.Anything, tmock.Anything).Return(nil, nil, nil) - asgn := &core.StdAssignmentCoordinator{} - sizeNotifier := batcher.NewEncodedSizeNotifier(make(chan struct{}, 1), 100000) - pool := &cmock.MockWorkerpool{} - metrics := batcher.NewMetrics("9100", logger) - encodingStreamer, err := batcher.NewEncodingStreamer(streamerConfig, blobStore, cst, encoderClient, asgn, sizeNotifier, pool, metrics.EncodingStreamerMetrics, metrics, logger) - assert.Nil(t, err) - encodingStreamer.ReferenceBlockNumber = 10 - - securityParams := []*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }} - blobData := []byte{1, 2, 3, 4, 5} - blob := core.Blob{ - RequestHeader: core.BlobRequestHeader{ - SecurityParams: securityParams, - }, - Data: blobData, - } - - pool.On("Submit", tmock.Anything).Run(func(args tmock.Arguments) { - args.Get(0).(func())() - }) - - // assume that encoding queue is already full - pool.On("WaitingQueueSize").Return(streamerConfig.EncodingQueueLimit).Once() - - key, err := blobStore.StoreBlob(ctx, &blob, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - out := make(chan batcher.EncodingResultOrStatus, 1) - // This should return without making a request since encoding queue was already full - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - - encoderClient.AssertNotCalled(t, "EncodeBlob") - select { - case <-out: - t.Fatal("did not expect any encoding results") - default: - } - // assume that encoding queue opens up - pool.On("WaitingQueueSize").Return(0).Once() - - // retry - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - - encoderClient.AssertNumberOfCalls(t, "EncodeBlob", 1) - encoderClient.AssertCalled(t, "EncodeBlob", tmock.Anything, blobData, tmock.Anything) - var encodingResult batcher.EncodingResultOrStatus - select { - case encodingResult = <-out: - default: - t.Fatal("did not expect any encoding results") - } - err = encodingStreamer.ProcessEncodedBlobs(ctx, encodingResult) - assert.Nil(t, err) - res, err := encodingStreamer.EncodedBlobstore.GetEncodingResult(key, 0) - assert.Nil(t, err) - assert.NotNil(t, res) -} - -func TestBatchTrigger(t *testing.T) { - ctx := t.Context() - encodingStreamer, c := createEncodingStreamer(t, 10, 30_000, streamerConfig) - - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }}) - _, err := c.blobStore.StoreBlob(ctx, &blob, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - out := make(chan batcher.EncodingResultOrStatus) - // Request encoding - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - count, size := encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 1) - assert.Equal(t, size, uint64(26630)) - - // try encode the same blobs again at different block (this happens when the blob is retried) - encodingStreamer.ReferenceBlockNumber = 11 - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - - count, size = encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 1) - assert.Equal(t, size, uint64(26630)) - - // don't notify yet - select { - case <-encodingStreamer.EncodedSizeNotifier.Notify: - t.Fatal("expected not to be notified") - default: - } - - // Request encoding once more - _, err = c.blobStore.StoreBlob(ctx, &blob, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - - count, size = encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 2) - assert.Equal(t, size, uint64(26630)*2) - - // notify - select { - case <-encodingStreamer.EncodedSizeNotifier.Notify: - default: - t.Fatal("expected to be notified") - } -} - -func TestStreamingEncoding(t *testing.T) { - ctx := t.Context() - - encodingStreamer, c := createEncodingStreamer(t, 0, 1e12, streamerConfig) - - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }}) - metadataKey, err := c.blobStore.StoreBlob(ctx, &blob, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - metadata, err := c.blobStore.GetBlobMetadata(ctx, metadataKey) - assert.Nil(t, err) - assert.Equal(t, disperser.Processing, metadata.BlobStatus) - - c.chainDataMock.On("GetCurrentBlockNumber").Return(uint(10)+encodingStreamer.FinalizationBlockDelay, nil) - - out := make(chan batcher.EncodingResultOrStatus) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - isRequested := encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 10) - assert.True(t, isRequested) - count, size := encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 0) - assert.Equal(t, size, uint64(0)) - - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - encodedResult, err := encodingStreamer.EncodedBlobstore.GetEncodingResult(metadataKey, core.QuorumID(0)) - assert.Nil(t, err) - assert.NotNil(t, encodedResult) - assert.Equal(t, metadata, encodedResult.BlobMetadata) - assert.Equal(t, uint(10), encodedResult.ReferenceBlockNumber) - assert.Equal(t, &core.BlobQuorumInfo{ - SecurityParam: core.SecurityParam{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - ChunkLength: 16, - }, encodedResult.BlobQuorumInfo) - assert.NotNil(t, encodedResult.Commitment) - assert.NotNil(t, encodedResult.Commitment.Commitment) - assert.NotNil(t, encodedResult.Commitment.LengthProof) - assert.Greater(t, encodedResult.Commitment.Length, uint32(0)) - assert.Len(t, encodedResult.Assignments, numOperators) - assert.Len(t, encodedResult.ChunksData.Chunks, 32) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 10) - assert.True(t, isRequested) - count, size = encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 1) - assert.Equal(t, size, uint64(26630)) - - // Cancel previous blob so it doesn't get reencoded. - err = c.blobStore.MarkBlobFailed(ctx, metadataKey) - assert.Nil(t, err) - - encodingStreamer.ReferenceBlockNumber = 11 - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 11) - assert.False(t, isRequested) - // Request another blob again - requestedAt := uint64(time.Now().UnixNano()) - metadataKey, err = c.blobStore.StoreBlob(ctx, &blob, requestedAt) - assert.Nil(t, err) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - encodedResult, err = encodingStreamer.EncodedBlobstore.GetEncodingResult(metadataKey, core.QuorumID(0)) - assert.Nil(t, err) - assert.NotNil(t, encodedResult) - // This should delete the stale results but keep the new encoded results - results := encodingStreamer.EncodedBlobstore.GetNewAndDeleteStaleEncodingResults(uint(11)) - assert.Len(t, results, 1) - encodedResult, err = encodingStreamer.EncodedBlobstore.GetEncodingResult(metadataKey, core.QuorumID(0)) - assert.Nil(t, err) - assert.NotNil(t, encodedResult) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 11) - assert.True(t, isRequested) - count, size = encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 1) - assert.Equal(t, size, uint64(26630)) - - // Request the same blob, which should be dedupped - _, err = c.blobStore.StoreBlob(ctx, &blob, requestedAt) - assert.Nil(t, err) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - assert.Equal(t, len(out), 0) - // It should not have been added to the encoded blob store - count, size = encodingStreamer.EncodedBlobstore.GetEncodedResultSize() - assert.Equal(t, count, 1) - assert.Equal(t, size, uint64(26630)) -} - -func TestEncodingFailure(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - blobStore := inmem.NewBlobStore() - cst, err := coremock.MakeChainDataMock(map[uint8]int{ - 0: numOperators, - 1: numOperators, - 2: numOperators, - }) - assert.Nil(t, err) - encoderClient := mock.NewMockEncoderClient() - asgn := &core.StdAssignmentCoordinator{} - sizeNotifier := batcher.NewEncodedSizeNotifier(make(chan struct{}, 1), 1e12) - workerpool := workerpool.New(5) - streamerConfig := batcher.StreamerConfig{ - SRSOrder: 300000, - EncodingRequestTimeout: 5 * time.Second, - EncodingQueueLimit: 100, - MaxBlobsToFetchFromStore: 10, - } - metrics := batcher.NewMetrics("9100", logger) - encodingStreamer, err := batcher.NewEncodingStreamer(streamerConfig, blobStore, cst, encoderClient, asgn, sizeNotifier, workerpool, metrics.EncodingStreamerMetrics, metrics, logger) - assert.Nil(t, err) - encodingStreamer.ReferenceBlockNumber = 10 - - // put a blob in the blobstore - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, { - QuorumID: 1, - AdversaryThreshold: 70, - ConfirmationThreshold: 100, - }}) - - metadataKey, err := blobStore.StoreBlob(ctx, &blob, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - - cst.On("GetCurrentBlockNumber").Return(uint(10)+encodingStreamer.FinalizationBlockDelay, nil) - encoderClient.On("EncodeBlob", tmock.Anything, tmock.Anything, tmock.Anything).Return(nil, nil, errors.New("errrrr")) - // request encoding - out := make(chan batcher.EncodingResultOrStatus) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - isRequested := encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(1), 10) - assert.True(t, isRequested) - - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NotNil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.NotNil(t, err) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 9) - assert.False(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 10) - assert.False(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 11) - assert.False(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(1), 10) - assert.False(t, isRequested) -} - -func TestPartialBlob(t *testing.T) { - ctx := t.Context() - encodingStreamer, c := createEncodingStreamer(t, 10, 1e12, streamerConfig) - - c.chainDataMock.On("GetCurrentBlockNumber").Return(uint(10)+encodingStreamer.FinalizationBlockDelay, nil) - - out := make(chan batcher.EncodingResultOrStatus) - - // put in first blob and request encoding - blob1 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }}) - - metadataKey1, err := c.blobStore.StoreBlob(ctx, &blob1, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - metadata1, err := c.blobStore.GetBlobMetadata(ctx, metadataKey1) - assert.Nil(t, err) - assert.Equal(t, disperser.Processing, metadata1.BlobStatus) - - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - - isRequested := encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(0), 10) - assert.True(t, isRequested) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(0), 10) - assert.True(t, isRequested) - - // Put in second blob and request encoding - blob2 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 1, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, { - QuorumID: 2, - AdversaryThreshold: 70, - ConfirmationThreshold: 95, - }}) - metadataKey2, err := c.blobStore.StoreBlob(ctx, &blob2, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - metadata2, err := c.blobStore.GetBlobMetadata(ctx, metadataKey2) - assert.Nil(t, err) - assert.Equal(t, disperser.Processing, metadata2.BlobStatus) - - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(1), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(2), 10) - assert.True(t, isRequested) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - - // The second quorum doesn't complete - <-out - encodingStreamer.Pool.StopWait() - - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(1), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(2), 10) - assert.True(t, isRequested) - - // get batch - assert.Equal(t, encodingStreamer.ReferenceBlockNumber, uint(10)) - batch, err := encodingStreamer.CreateBatch(ctx) - assert.Nil(t, err) - assert.NotNil(t, batch) - assert.Equal(t, encodingStreamer.ReferenceBlockNumber, uint(0)) - - // Check BatchHeader - assert.NotNil(t, batch.BatchHeader) - assert.Greater(t, len(batch.BatchHeader.BatchRoot), 0) - assert.Equal(t, batch.BatchHeader.ReferenceBlockNumber, uint(10)) - - // Check BatchMetadata - assert.NotNil(t, batch.State) - assert.ElementsMatch(t, batch.BlobMetadata[0].RequestMetadata.SecurityParams, blob1.RequestHeader.SecurityParams) - - // Check EncodedBlobs - assert.Len(t, batch.EncodedBlobs, 1) - assert.Len(t, batch.EncodedBlobs[0].EncodedBundlesByOperator, numOperators) - - encodedBlob1 := batch.EncodedBlobs[0] - assert.NotNil(t, encodedBlob1) - assert.NotNil(t, encodedBlob1.BlobHeader) - assert.NotNil(t, encodedBlob1.BlobHeader.BlobCommitments) - assert.NotNil(t, encodedBlob1.BlobHeader.BlobCommitments.Commitment) - assert.NotNil(t, encodedBlob1.BlobHeader.BlobCommitments.LengthProof) - assert.Equal(t, encodedBlob1.BlobHeader.BlobCommitments.Length, uint32(48)) //nolint: staticcheck - assert.Len(t, encodedBlob1.BlobHeader.QuorumInfos, 1) - assert.ElementsMatch(t, encodedBlob1.BlobHeader.QuorumInfos, []*core.BlobQuorumInfo{{ - SecurityParam: core.SecurityParam{ - QuorumID: 0, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }, - ChunkLength: 8, - }}) - - assert.Contains(t, batch.BlobHeaders, encodedBlob1.BlobHeader) - assert.Len(t, encodedBlob1.EncodedBundlesByOperator, numOperators) - for _, bundles := range encodedBlob1.EncodedBundlesByOperator { - assert.Len(t, bundles, 1) - assert.Greater(t, len(bundles[0].Chunks), 0) - break - } - - assert.Len(t, batch.BlobHeaders, 1) - assert.Len(t, batch.BlobMetadata, 1) - assert.Contains(t, batch.BlobMetadata, metadata1) -} - -func TestIncorrectParameters(t *testing.T) { - ctx := t.Context() - - streamerConfig := batcher.StreamerConfig{ - SRSOrder: 3000, - EncodingRequestTimeout: 5 * time.Second, - EncodingQueueLimit: 100, - MaxBlobsToFetchFromStore: 10, - } - - encodingStreamer, c := createEncodingStreamer(t, 0, 1e12, streamerConfig) - - // put a blob in the blobstore - - // The blob size is acceptable with the first security parameter but too large with the second - // security parameter. Thus, the entire blob should be rejected. - - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 50, - ConfirmationThreshold: 100, - }, { - QuorumID: 1, - AdversaryThreshold: 90, - ConfirmationThreshold: 100, - }}) - blob.Data = make([]byte, 10000) - _, err := rand.Read(blob.Data) - assert.NoError(t, err) - - metadataKey, err := c.blobStore.StoreBlob(ctx, &blob, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - - c.chainDataMock.On("GetCurrentBlockNumber").Return(uint(10)+encodingStreamer.FinalizationBlockDelay, nil) - - // request encoding - out := make(chan batcher.EncodingResultOrStatus) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - - isRequested := encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(0), 10) - assert.False(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey, core.QuorumID(1), 10) - assert.False(t, isRequested) - - stats, err := c.blobStore.GetBlobMetadata(ctx, metadataKey) - assert.NoError(t, err) - assert.Equal(t, disperser.Failed, stats.BlobStatus) - -} - -func TestInvalidQuorum(t *testing.T) { - ctx := t.Context() - encodingStreamer, c := createEncodingStreamer(t, 10, 1e12, streamerConfig) - - c.chainDataMock.On("GetCurrentBlockNumber").Return(uint(10)+encodingStreamer.FinalizationBlockDelay, nil) - - out := make(chan batcher.EncodingResultOrStatus) - - // this blob should not be encoded because the quorum does not exist - blob1 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }, { - QuorumID: 99, // this quorum does not exist - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }}) - - // this blob should be encoded - blob2 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }, { - QuorumID: 1, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }}) - - metadataKey1, err := c.blobStore.StoreBlob(ctx, &blob1, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - metadataKey2, err := c.blobStore.StoreBlob(ctx, &blob2, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - - // request encoding - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - - isRequested := encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(0), 10) - assert.False(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(99), 10) - assert.False(t, isRequested) - - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(0), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(1), 10) - assert.True(t, isRequested) - - stats, err := c.blobStore.GetBlobMetadata(ctx, metadataKey1) - assert.NoError(t, err) - assert.Equal(t, disperser.Failed, stats.BlobStatus) -} - -func TestGetBatch(t *testing.T) { - encodingStreamer, c := createEncodingStreamer(t, 10, 1e12, streamerConfig) - ctx := t.Context() - - // put 2 blobs in the blobstore - blob1 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, { - QuorumID: 1, - AdversaryThreshold: 70, - ConfirmationThreshold: 95, - }}) - blob2 := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 2, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }}) - metadataKey1, err := c.blobStore.StoreBlob(ctx, &blob1, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - metadata1, err := c.blobStore.GetBlobMetadata(ctx, metadataKey1) - assert.Nil(t, err) - assert.Equal(t, disperser.Processing, metadata1.BlobStatus) - metadataKey2, err := c.blobStore.StoreBlob(ctx, &blob2, uint64(time.Now().UnixNano())) - assert.Nil(t, err) - metadata2, err := c.blobStore.GetBlobMetadata(ctx, metadataKey2) - assert.Nil(t, err) - assert.Equal(t, disperser.Processing, metadata2.BlobStatus) - - c.chainDataMock.On("GetCurrentBlockNumber").Return(uint(10)+encodingStreamer.FinalizationBlockDelay, nil) - - // request encoding - out := make(chan batcher.EncodingResultOrStatus) - err = encodingStreamer.RequestEncoding(ctx, out) - assert.Nil(t, err) - isRequested := encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(0), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(1), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(2), 10) - assert.True(t, isRequested) - - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - err = encodingStreamer.ProcessEncodedBlobs(ctx, <-out) - assert.Nil(t, err) - encodingStreamer.Pool.StopWait() - - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(0), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey1, core.QuorumID(1), 10) - assert.True(t, isRequested) - isRequested = encodingStreamer.EncodedBlobstore.HasEncodingRequested(metadataKey2, core.QuorumID(2), 10) - assert.True(t, isRequested) - - // get batch - assert.Equal(t, encodingStreamer.ReferenceBlockNumber, uint(10)) - batch, err := encodingStreamer.CreateBatch(ctx) - assert.Nil(t, err) - assert.NotNil(t, batch) - assert.Equal(t, encodingStreamer.ReferenceBlockNumber, uint(0)) - metadata1, err = c.blobStore.GetBlobMetadata(ctx, metadataKey1) - assert.Nil(t, err) - assert.Equal(t, disperser.Dispersing, metadata1.BlobStatus) - metadata2, err = c.blobStore.GetBlobMetadata(ctx, metadataKey2) - assert.Equal(t, disperser.Dispersing, metadata2.BlobStatus) - assert.Nil(t, err) - res, err := encodingStreamer.EncodedBlobstore.GetEncodingResult(metadataKey1, core.QuorumID(0)) - assert.Nil(t, res) - assert.ErrorContains(t, err, "GetEncodedBlob: no such key") - res, err = encodingStreamer.EncodedBlobstore.GetEncodingResult(metadataKey1, core.QuorumID(1)) - assert.Nil(t, res) - assert.ErrorContains(t, err, "GetEncodedBlob: no such key") - res, err = encodingStreamer.EncodedBlobstore.GetEncodingResult(metadataKey2, core.QuorumID(0)) - assert.Nil(t, res) - assert.ErrorContains(t, err, "GetEncodedBlob: no such key") - - // Check BatchHeader - assert.NotNil(t, batch.BatchHeader) - assert.Greater(t, len(batch.BatchHeader.BatchRoot), 0) - assert.Equal(t, batch.BatchHeader.ReferenceBlockNumber, uint(10)) - - // Check State - assert.NotNil(t, batch.State) - - // Check EncodedBlobs - assert.Len(t, batch.EncodedBlobs, 2) - assert.Len(t, batch.EncodedBlobs[0].EncodedBundlesByOperator, numOperators) - - var encodedBlob1 core.EncodedBlob - var encodedBlob2 core.EncodedBlob - for i := range batch.BlobHeaders { - blobHeader := batch.BlobHeaders[i] - if len(blobHeader.QuorumInfos) > 1 { - encodedBlob1 = batch.EncodedBlobs[i] - // batch.EncodedBlobs and batch.BlobMetadata should be in the same order - assert.ElementsMatch(t, batch.BlobMetadata[i].RequestMetadata.SecurityParams, blob1.RequestHeader.SecurityParams) - } else { - encodedBlob2 = batch.EncodedBlobs[i] - assert.ElementsMatch(t, batch.BlobMetadata[i].RequestMetadata.SecurityParams, blob2.RequestHeader.SecurityParams) - } - } - assert.NotNil(t, encodedBlob1) - assert.NotNil(t, encodedBlob2) - - assert.NotNil(t, encodedBlob1.BlobHeader) - assert.NotNil(t, encodedBlob1.BlobHeader.BlobCommitments) - assert.NotNil(t, encodedBlob1.BlobHeader.BlobCommitments.Commitment) - assert.NotNil(t, encodedBlob1.BlobHeader.BlobCommitments.LengthProof) - assert.Equal(t, encodedBlob1.BlobHeader.BlobCommitments.Length, uint32(48)) //nolint: staticcheck - assert.Len(t, encodedBlob1.BlobHeader.QuorumInfos, 2) - assert.ElementsMatch(t, encodedBlob1.BlobHeader.QuorumInfos, []*core.BlobQuorumInfo{ - { - SecurityParam: core.SecurityParam{ - QuorumID: 0, - AdversaryThreshold: 80, - ConfirmationThreshold: 100, - }, - ChunkLength: 16, - }, - { - SecurityParam: core.SecurityParam{ - QuorumID: 1, - AdversaryThreshold: 70, - ConfirmationThreshold: 95, - }, - ChunkLength: 8, - }, - }) - - assert.Contains(t, batch.BlobHeaders, encodedBlob1.BlobHeader) - for _, bundles := range encodedBlob1.EncodedBundlesByOperator { - assert.Len(t, bundles, 2) - assert.Greater(t, len(bundles[0].Chunks), 0) - assert.Greater(t, len(bundles[1].Chunks), 0) - break - } - - assert.NotNil(t, encodedBlob2.BlobHeader) - assert.NotNil(t, encodedBlob2.BlobHeader.BlobCommitments) - assert.NotNil(t, encodedBlob2.BlobHeader.BlobCommitments.Commitment) - assert.NotNil(t, encodedBlob2.BlobHeader.BlobCommitments.LengthProof) - assert.Equal(t, encodedBlob2.BlobHeader.BlobCommitments.Length, uint32(48)) //nolint: staticcheck - assert.Len(t, encodedBlob2.BlobHeader.QuorumInfos, 1) - assert.ElementsMatch(t, encodedBlob2.BlobHeader.QuorumInfos, []*core.BlobQuorumInfo{{ - SecurityParam: core.SecurityParam{ - QuorumID: 2, - AdversaryThreshold: 75, - ConfirmationThreshold: 100, - }, - ChunkLength: 8, - }}) - for _, bundles := range encodedBlob2.EncodedBundlesByOperator { - assert.Len(t, bundles, 1) - assert.Greater(t, len(bundles[core.QuorumID(2)].Chunks), 0) - break - } - assert.Len(t, batch.BlobHeaders, 2) - assert.Len(t, batch.BlobMetadata, 2) - assert.Contains(t, batch.BlobMetadata, metadata1) - assert.Contains(t, batch.BlobMetadata, metadata2) -} diff --git a/disperser/batcher/finalizer.go b/disperser/batcher/finalizer.go deleted file mode 100644 index 6fd21f06a6..0000000000 --- a/disperser/batcher/finalizer.go +++ /dev/null @@ -1,287 +0,0 @@ -package batcher - -import ( - "context" - "errors" - "fmt" - "math" - "time" - - "github.com/Layr-Labs/eigenda/common" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/core/types" - "github.com/gammazero/workerpool" - - gcommon "github.com/ethereum/go-ethereum/common" -) - -const maxRetries = 3 -const baseDelay = 1 * time.Second - -// Finalizer runs periodically to finalize blobs that have been confirmed -type Finalizer interface { - Start(ctx context.Context) - FinalizeBlobs(ctx context.Context) error -} - -type finalizer struct { - timeout time.Duration - loopInterval time.Duration - blobStore disperser.BlobStore - ethClient common.EthClient - rpcClient common.RPCEthClient - maxNumRetriesPerBlob uint - numBlobsPerFetch int32 - numWorkers int - logger logging.Logger - metrics *FinalizerMetrics -} - -func NewFinalizer( - timeout time.Duration, - loopInterval time.Duration, - blobStore disperser.BlobStore, - ethClient common.EthClient, - rpcClient common.RPCEthClient, - maxNumRetriesPerBlob uint, - numBlobsPerFetch int32, - numWorkers int, - logger logging.Logger, - metrics *FinalizerMetrics, -) Finalizer { - return &finalizer{ - timeout: timeout, - loopInterval: loopInterval, - blobStore: blobStore, - ethClient: ethClient, - rpcClient: rpcClient, - maxNumRetriesPerBlob: maxNumRetriesPerBlob, - numBlobsPerFetch: numBlobsPerFetch, - numWorkers: numWorkers, - logger: logger.With("component", "Finalizer"), - metrics: metrics, - } -} - -func (f *finalizer) Start(ctx context.Context) { - go func() { - ticker := time.NewTicker(f.loopInterval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if err := f.FinalizeBlobs(ctx); err != nil { - f.logger.Error("failed to finalize blobs", "err", err) - } - } - } - }() -} - -// FinalizeBlobs checks the latest finalized block and marks blobs in `confirmed` state as `finalized` if their confirmation -// block number is less than or equal to the latest finalized block number. -// If it failes to process some blobs, it will log the error, skip the failed blobs, and will not return an error. The function should be invoked again to retry. -func (f *finalizer) FinalizeBlobs(ctx context.Context) error { - startTime := time.Now() - pool := workerpool.New(f.numWorkers) - finalizedHeader, err := f.getLatestFinalizedBlock(ctx) - if err != nil { - return fmt.Errorf("FinalizeBlobs: error getting latest finalized block: %w", err) - } - lastFinalBlock := finalizedHeader.Number.Uint64() - - totalProcessed := 0 - metadatas, exclusiveStartKey, err := f.blobStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Confirmed, f.numBlobsPerFetch, nil) - if err != nil { - return fmt.Errorf("FinalizeBlobs: error getting blob headers: %w", err) - } - - for len(metadatas) > 0 { - metas := metadatas - f.logger.Info("finalizing blobs", "numBlobs", len(metas), "finalizedBlockNumber", lastFinalBlock) - pool.Submit(func() { - f.updateBlobs(ctx, metas, lastFinalBlock) - }) - totalProcessed += len(metadatas) - - if exclusiveStartKey == nil { - break - } - metadatas, exclusiveStartKey, err = f.blobStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Confirmed, f.numBlobsPerFetch, exclusiveStartKey) - if err != nil { - f.logger.Error("error getting blob headers on subsequent call", "err", err) - break - } - } - pool.StopWait() - - f.logger.Info("FinalizeBlobs: successfully processed all finalized blobs", "finalizedBlockNumber", lastFinalBlock, "totalProcessed", totalProcessed, "elapsedTime", time.Since(startTime)) - f.metrics.UpdateLastSeenFinalizedBlock(lastFinalBlock) - f.metrics.UpdateNumBlobs("processed", totalProcessed) - f.metrics.ObserveLatency("total", float64(time.Since(startTime).Milliseconds())) - return nil -} - -func (f *finalizer) updateBlobs(ctx context.Context, metadatas []*disperser.BlobMetadata, lastFinalBlock uint64) { - // Panic recovery - defer func() { - if r := recover(); r != nil { - // Log panic - f.logger.Error("encountered panic", "recovered", r) - } - }() - - for _, m := range metadatas { - // Check if metadata is nil before proceeding - if m == nil { - f.logger.Error("encountered nil metadata in loop") - continue - } - - stageTimer := time.Now() - blobKey := m.GetBlobKey() - - if m.BlobStatus != disperser.Confirmed { - f.logger.Error("the blob retrieved by status Confirmed is actually", m.BlobStatus.String(), "blobKey", blobKey.String()) - continue - } - - confirmationMetadata, err := f.blobStore.GetBlobMetadata(ctx, blobKey) - if err != nil { - f.logger.Error("error getting confirmed metadata", "blobKey", blobKey.String(), "err", err) - continue - } - - // Noticed minor issue where ProcessConfirmedBatch goroutine probably set this to failed status after updateBlobs was called to finalize the blobs. - // For Failed blobs, it is expected that ConfirmationInfo will be null. - if confirmationMetadata != nil && confirmationMetadata.BlobStatus != disperser.Confirmed { - f.logger.Error("the blob retrieved is actually", confirmationMetadata.BlobStatus.String(), "blobKey", blobKey.String()) - continue - } - - // Additional checks for confirmationMetadata and its nested fields - if confirmationMetadata == nil || confirmationMetadata.ConfirmationInfo == nil { - f.logger.Error("received nil confirmationMetadata or ConfirmationInfo", "blobKey", blobKey.String()) - continue - } - - // Leave as confirmed if the confirmation block is after the latest finalized block (not yet finalized) - if uint64(confirmationMetadata.ConfirmationInfo.ConfirmationBlockNumber) > lastFinalBlock { - continue - } - - // confirmation block number may have changed due to reorg - confirmationBlockNumber, err := f.getTransactionBlockNumber(ctx, confirmationMetadata.ConfirmationInfo.ConfirmationTxnHash) - if errors.Is(err, ethereum.NotFound) { - // The confirmed block is finalized, but the transaction is not found. It means the transaction should be considered forked/invalid and the blob should be considered as failed. - f.logger.Warn("confirmed transaction not found", "blobKey", blobKey.String(), "confirmationTxnHash", confirmationMetadata.ConfirmationInfo.ConfirmationTxnHash.Hex(), "confirmationBlockNumber", confirmationMetadata.ConfirmationInfo.ConfirmationBlockNumber) - err := f.blobStore.MarkBlobFailed(ctx, m.GetBlobKey()) - if err != nil { - f.logger.Error("error marking blob as failed", "blobKey", blobKey.String(), "err", err) - } - f.metrics.IncrementNumBlobs("failed") - continue - } - if err != nil { - f.logger.Error("error getting transaction block number", "err", err) - f.metrics.IncrementNumBlobs("failed") - continue - } - - if confirmationBlockNumber != uint64(confirmationMetadata.ConfirmationInfo.ConfirmationBlockNumber) { - // Confirmation block number has changed due to reorg. Update the confirmation block number in the metadata - err := f.blobStore.UpdateConfirmationBlockNumber(ctx, m, uint32(confirmationBlockNumber)) - if err != nil { - f.logger.Error("error updating confirmation block number", "blobKey", blobKey.String(), "err", err) - f.metrics.IncrementNumBlobs("failed") - continue - } - } - - // Leave as confirmed if the reorged confirmation block is after the latest finalized block (not yet finalized) - if uint64(confirmationBlockNumber) > lastFinalBlock { - continue - } - - err = f.blobStore.MarkBlobFinalized(ctx, blobKey) - if err != nil { - f.logger.Error("error marking blob as finalized", "blobKey", blobKey.String(), "err", err) - f.metrics.IncrementNumBlobs("failed") - continue - } - f.metrics.IncrementNumBlobs("finalized") - f.metrics.ObserveLatency("round", float64(time.Since(stageTimer).Milliseconds())) - } -} - -func (f *finalizer) getTransactionBlockNumber(ctx context.Context, hash gcommon.Hash) (uint64, error) { - var ctxWithTimeout context.Context - var cancel context.CancelFunc - var txReceipt *types.Receipt - var err error - - rpcCallAttempt := func() error { - ctxWithTimeout, cancel = context.WithTimeout(ctx, f.timeout) - defer cancel() - txReceipt, err = f.ethClient.TransactionReceipt(ctxWithTimeout, hash) - return err - } - - for i := 0; i < maxRetries; i++ { - - err = rpcCallAttempt() - if err == nil { - break - } - - if errors.Is(err, ethereum.NotFound) { - // If the transaction is not found, it means the transaction has been reorged out of the chain. - return 0, err - } - - retrySec := math.Pow(2, float64(i)) - f.logger.Error("error getting transaction", "err", err, "retrySec", retrySec, "hash", hash.Hex()) - time.Sleep(time.Duration(retrySec) * baseDelay) - } - - if err != nil { - return 0, fmt.Errorf("Finalizer: error getting transaction receipt after retries: %w", err) - } - - return txReceipt.BlockNumber.Uint64(), nil -} - -func (f *finalizer) getLatestFinalizedBlock(ctx context.Context) (*types.Header, error) { - var ctxWithTimeout context.Context - var cancel context.CancelFunc - var header = types.Header{} - var err error - - rpcCallAttempt := func() error { - ctxWithTimeout, cancel = context.WithTimeout(ctx, f.timeout) - defer cancel() - err = f.rpcClient.CallContext(ctxWithTimeout, &header, "eth_getBlockByNumber", "finalized", false) - return err - } - - for i := 0; i < maxRetries; i++ { - err = rpcCallAttempt() - if err == nil { - break - } - retrySec := math.Pow(2, float64(i)) - f.logger.Error("error getting latest finalized block", "err", err, "retrySec", retrySec) - time.Sleep(time.Duration(retrySec) * baseDelay) - } - - if err != nil { - return nil, fmt.Errorf("Finalizer: error getting latest finalized block after retries: %w", err) - } - - return &header, nil -} diff --git a/disperser/batcher/finalizer_test.go b/disperser/batcher/finalizer_test.go deleted file mode 100644 index b6258343e1..0000000000 --- a/disperser/batcher/finalizer_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package batcher_test - -import ( - "math/big" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/common/mock" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/batcher" - "github.com/Layr-Labs/eigenda/disperser/common/inmem" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/Layr-Labs/eigenda/test" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/assert" - m "github.com/stretchr/testify/mock" -) - -const timeout = 5 * time.Second -const loopInterval = 6 * time.Minute - -func TestFinalizedBlob(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - queue := inmem.NewBlobStore() - ethClient := &mock.MockEthClient{} - rpcClient := &mock.MockRPCEthClient{} - - latestFinalBlock := int64(1_000_010) - rpcClient.On("CallContext", m.Anything, m.Anything, "eth_getBlockByNumber", "finalized", false). - Run(func(args m.Arguments) { - args[1].(*types.Header).Number = big.NewInt(latestFinalBlock) - }).Return(nil).Once() - ethClient.On("TransactionReceipt", m.Anything, m.Anything).Return(&types.Receipt{ - BlockNumber: new(big.Int).SetUint64(1_000_000), - }, nil) - - metrics := batcher.NewMetrics("9100", logger) - finalizer := batcher.NewFinalizer(timeout, loopInterval, queue, ethClient, rpcClient, 1, 1, 1, logger, metrics.FinalizerMetrics) - - requestedAt := uint64(time.Now().UnixNano()) - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - }}) - metadataKey1, err := queue.StoreBlob(ctx, &blob, requestedAt) - assert.NoError(t, err) - metadataKey2, err := queue.StoreBlob(ctx, &blob, requestedAt+1) - assert.NoError(t, err) - batchHeaderHash := [32]byte{1, 2, 3} - blobIndex := uint32(10) - sigRecordHash := [32]byte{0} - inclusionProof := []byte{1, 2, 3, 4, 5} - expiry := uint64(time.Now().Add(time.Hour).Unix()) - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - SignatoryRecordHash: sigRecordHash, - ReferenceBlockNumber: 132, - BatchRoot: []byte("hello"), - BlobInclusionProof: inclusionProof, - BlobCommitment: &encoding.BlobCommitments{}, - BatchID: 99, - ConfirmationTxnHash: common.HexToHash("0x123"), - ConfirmationBlockNumber: uint32(150), - Fee: []byte{0}, - } - metadata1 := &disperser.BlobMetadata{ - BlobHash: metadataKey1.BlobHash, - MetadataHash: metadataKey1.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: blob.RequestHeader.SecurityParams, - }, - RequestedAt: requestedAt, - }, - } - metadata2 := &disperser.BlobMetadata{ - BlobHash: metadataKey2.BlobHash, - MetadataHash: metadataKey2.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: expiry + 1, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: blob.RequestHeader.SecurityParams, - }, - RequestedAt: requestedAt + 1, - }, - } - m, err := queue.MarkBlobConfirmed(ctx, metadata1, confirmationInfo) - assert.Equal(t, disperser.Confirmed, m.BlobStatus) - assert.NoError(t, err) - m, err = queue.MarkBlobConfirmed(ctx, metadata2, confirmationInfo) - assert.Equal(t, disperser.Confirmed, m.BlobStatus) - assert.NoError(t, err) - - err = finalizer.FinalizeBlobs(ctx) - assert.NoError(t, err) - - metadatas, err := queue.GetBlobMetadataByStatus(ctx, disperser.Confirmed) - assert.NoError(t, err) - assert.Len(t, metadatas, 0) - - metadatas, err = queue.GetBlobMetadataByStatus(ctx, disperser.Finalized) - assert.NoError(t, err) - assert.Len(t, metadatas, 2) - - assert.ElementsMatch(t, []string{metadatas[0].BlobHash, metadatas[1].BlobHash}, []string{metadataKey1.BlobHash, metadataKey2.BlobHash}) - assert.Equal(t, metadatas[0].BlobStatus, disperser.Finalized) - assert.Equal(t, metadatas[1].BlobStatus, disperser.Finalized) - assert.ElementsMatch(t, []uint64{metadatas[0].RequestMetadata.RequestedAt, metadatas[1].RequestMetadata.RequestedAt}, []uint64{requestedAt, requestedAt + 1}) - assert.Equal(t, metadatas[0].RequestMetadata.SecurityParams, blob.RequestHeader.SecurityParams) - assert.Equal(t, metadatas[1].RequestMetadata.SecurityParams, blob.RequestHeader.SecurityParams) -} - -func TestUnfinalizedBlob(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - queue := inmem.NewBlobStore() - ethClient := &mock.MockEthClient{} - rpcClient := &mock.MockRPCEthClient{} - - latestFinalBlock := int64(1_000_010) - rpcClient.On("CallContext", m.Anything, m.Anything, "eth_getBlockByNumber", "finalized", false). - Run(func(args m.Arguments) { - args[1].(*types.Header).Number = big.NewInt(latestFinalBlock) - }).Return(nil).Once() - ethClient.On("TransactionReceipt", m.Anything, m.Anything).Return(&types.Receipt{ - BlockNumber: new(big.Int).SetUint64(1_000_100), - }, nil) - - metrics := batcher.NewMetrics("9100", logger) - finalizer := batcher.NewFinalizer(timeout, loopInterval, queue, ethClient, rpcClient, 1, 1, 1, logger, metrics.FinalizerMetrics) - - requestedAt := uint64(time.Now().UnixNano()) - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - }}) - metadataKey, err := queue.StoreBlob(ctx, &blob, requestedAt) - assert.NoError(t, err) - batchHeaderHash := [32]byte{1, 2, 3} - blobIndex := uint32(10) - sigRecordHash := [32]byte{0} - inclusionProof := []byte{1, 2, 3, 4, 5} - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - SignatoryRecordHash: sigRecordHash, - ReferenceBlockNumber: 132, - BatchRoot: []byte("hello"), - BlobInclusionProof: inclusionProof, - BlobCommitment: &encoding.BlobCommitments{}, - BatchID: 99, - ConfirmationTxnHash: common.HexToHash("0x123"), - ConfirmationBlockNumber: uint32(150), - Fee: []byte{0}, - } - expiry := uint64(time.Now().Add(100000).Unix()) - metadata := &disperser.BlobMetadata{ - BlobHash: metadataKey.BlobHash, - MetadataHash: metadataKey.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: blob.RequestHeader.SecurityParams, - }, - BlobSize: uint(len(blob.Data)), - RequestedAt: requestedAt, - }, - } - m, err := queue.MarkBlobConfirmed(ctx, metadata, confirmationInfo) - assert.NoError(t, err) - assert.Equal(t, disperser.Confirmed, m.BlobStatus) - err = finalizer.FinalizeBlobs(ctx) - assert.NoError(t, err) - - metadatas, err := queue.GetBlobMetadataByStatus(ctx, disperser.Confirmed) - assert.NoError(t, err) - assert.Len(t, metadatas, 1) - - metadatas, err = queue.GetBlobMetadataByStatus(ctx, disperser.Finalized) - assert.NoError(t, err) - assert.Len(t, metadatas, 0) -} - -func TestNoReceipt(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - queue := inmem.NewBlobStore() - ethClient := &mock.MockEthClient{} - rpcClient := &mock.MockRPCEthClient{} - - latestFinalBlock := int64(1_000_010) - rpcClient.On("CallContext", m.Anything, m.Anything, "eth_getBlockByNumber", "finalized", false). - Run(func(args m.Arguments) { - args[1].(*types.Header).Number = big.NewInt(latestFinalBlock) - }).Return(nil) - ethClient.On("TransactionReceipt", m.Anything, m.Anything).Return(nil, ethereum.NotFound) - - metrics := batcher.NewMetrics("9100", logger) - finalizer := batcher.NewFinalizer(timeout, loopInterval, queue, ethClient, rpcClient, 1, 1, 1, logger, metrics.FinalizerMetrics) - - requestedAt := uint64(time.Now().UnixNano()) - blob := makeTestBlob([]*core.SecurityParam{{ - QuorumID: 0, - AdversaryThreshold: 80, - }}) - metadataKey, err := queue.StoreBlob(ctx, &blob, requestedAt) - assert.NoError(t, err) - batchHeaderHash := [32]byte{1, 2, 3} - blobIndex := uint32(10) - sigRecordHash := [32]byte{0} - inclusionProof := []byte{1, 2, 3, 4, 5} - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - SignatoryRecordHash: sigRecordHash, - ReferenceBlockNumber: 132, - BatchRoot: []byte("hello"), - BlobInclusionProof: inclusionProof, - BlobCommitment: &encoding.BlobCommitments{}, - BatchID: 99, - ConfirmationTxnHash: common.HexToHash("0x123"), - ConfirmationBlockNumber: uint32(150), - Fee: []byte{0}, - } - expiry := uint64(time.Now().Add(100000).Unix()) - metadata := &disperser.BlobMetadata{ - BlobHash: metadataKey.BlobHash, - MetadataHash: metadataKey.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: blob.RequestHeader.SecurityParams, - }, - BlobSize: uint(len(blob.Data)), - RequestedAt: requestedAt, - }, - } - m, err := queue.MarkBlobConfirmed(ctx, metadata, confirmationInfo) - assert.NoError(t, err) - assert.Equal(t, disperser.Confirmed, m.BlobStatus) - err = finalizer.FinalizeBlobs(ctx) - assert.NoError(t, err) - - // status should be kept at confirmed - metadatas, err := queue.GetBlobMetadataByStatus(ctx, disperser.Finalized) - assert.NoError(t, err) - assert.Len(t, metadatas, 0) - metadatas, err = queue.GetBlobMetadataByStatus(ctx, disperser.Failed) - assert.NoError(t, err) - assert.Len(t, metadatas, 1) - metadatas, err = queue.GetBlobMetadataByStatus(ctx, disperser.Confirmed) - assert.NoError(t, err) - assert.Len(t, metadatas, 0) - metadatas, err = queue.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.NoError(t, err) - assert.Len(t, metadatas, 0) -} diff --git a/disperser/batcher/grpc/dispatcher.go b/disperser/batcher/grpc/dispatcher.go deleted file mode 100644 index 657b5980cf..0000000000 --- a/disperser/batcher/grpc/dispatcher.go +++ /dev/null @@ -1,347 +0,0 @@ -package dispatcher - -import ( - "context" - "errors" - "fmt" - "time" - - commonpb "github.com/Layr-Labs/eigenda/api/grpc/common" - "github.com/Layr-Labs/eigenda/api/grpc/node" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/batcher" - "github.com/Layr-Labs/eigensdk-go/logging" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/protobuf/proto" -) - -type Config struct { - Timeout time.Duration - EnableGnarkBundleEncoding bool -} - -type dispatcher struct { - *Config - - logger logging.Logger - metrics *batcher.DispatcherMetrics -} - -func NewDispatcher( - cfg *Config, - logger logging.Logger, - metrics *batcher.DispatcherMetrics, -) *dispatcher { - return &dispatcher{ - Config: cfg, - logger: logger.With("component", "Dispatcher"), - metrics: metrics, - } -} - -var _ disperser.Dispatcher = (*dispatcher)(nil) - -func (c *dispatcher) DisperseBatch( - ctx context.Context, - state *core.IndexedOperatorState, - blobs []core.EncodedBlob, - batchHeader *core.BatchHeader, -) chan core.SigningMessage { - update := make(chan core.SigningMessage, len(state.IndexedOperators)) - - // Disperse - c.sendAllChunks(ctx, state, blobs, batchHeader, update) - - return update -} - -func (c *dispatcher) sendAllChunks( - ctx context.Context, - state *core.IndexedOperatorState, - blobs []core.EncodedBlob, - batchHeader *core.BatchHeader, - update chan core.SigningMessage, -) { - for id, op := range state.IndexedOperators { - go func(op core.IndexedOperatorInfo, id core.OperatorID) { - blobMessages := make([]*core.EncodedBlobMessage, 0) - hasAnyBundles := false - batchHeaderHash, err := batchHeader.GetBatchHeaderHash() - if err != nil { - update <- core.SigningMessage{ - Err: fmt.Errorf("failed to get batch header hash: %w", err), - Signature: nil, - ValidatorId: id, - BatchHeaderHash: [32]byte{}, - Latency: -1, - } - return - } - for _, blob := range blobs { - if _, ok := blob.EncodedBundlesByOperator[id]; ok { - hasAnyBundles = true - } - blobMessages = append(blobMessages, &core.EncodedBlobMessage{ - BlobHeader: blob.BlobHeader, - // Bundles will be empty if the operator is not in the quorums blob is dispersed on - EncodedBundles: blob.EncodedBundlesByOperator[id], - }) - } - if !hasAnyBundles { - // Operator is not part of any quorum, no need to send chunks - update <- core.SigningMessage{ - Err: errors.New("operator is not part of any quorum"), - Signature: nil, - ValidatorId: id, - BatchHeaderHash: batchHeaderHash, - Latency: -1, - } - return - } - - requestedAt := time.Now() - sig, err := c.sendChunks(ctx, blobMessages, batchHeader, &op) - latency := time.Since(requestedAt) - if err != nil { - update <- core.SigningMessage{ - Err: err, - Signature: nil, - ValidatorId: id, - BatchHeaderHash: batchHeaderHash, - Latency: latency, - } - c.metrics.ObserveLatency(id.Hex(), false, float64(latency.Milliseconds())) - } else { - update <- core.SigningMessage{ - Signature: sig, - ValidatorId: id, - BatchHeaderHash: batchHeaderHash, - Latency: latency, - Err: nil, - } - c.metrics.ObserveLatency(id.Hex(), true, float64(latency.Milliseconds())) - } - - }(core.IndexedOperatorInfo{ - PubkeyG1: op.PubkeyG1, - PubkeyG2: op.PubkeyG2, - Socket: op.Socket, - }, id) - } -} - -func (c *dispatcher) sendChunks( - ctx context.Context, - blobs []*core.EncodedBlobMessage, - batchHeader *core.BatchHeader, - op *core.IndexedOperatorInfo, -) (*core.Signature, error) { - // TODO Add secure Grpc - - conn, err := grpc.NewClient( - core.OperatorSocket(op.Socket).GetV1DispersalSocket(), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - c.logger.Warn("Disperser cannot connect to operator dispersal socket", - "dispersal_socket", core.OperatorSocket(op.Socket).GetV1DispersalSocket(), - "err", err) - return nil, err - } - defer core.CloseLogOnError(conn, "operator connection", c.logger) - - gc := node.NewDispersalClient(conn) - ctx, cancel := context.WithTimeout(ctx, c.Timeout) - defer cancel() - start := time.Now() - request, totalSize, err := GetStoreChunksRequest(blobs, batchHeader, c.EnableGnarkBundleEncoding) - if err != nil { - return nil, err - } - c.logger.Debug("sending chunks to operator", - "operator", op.Socket, - "num blobs", len(blobs), - "size", totalSize, - "request message size", proto.Size(request), - "request serialization time", time.Since(start), - "use Gnark chunk encoding", c.EnableGnarkBundleEncoding) - opt := grpc.MaxCallSendMsgSize(60 * 1024 * 1024 * 1024) - reply, err := gc.StoreChunks(ctx, request, opt) - - if err != nil { - return nil, err - } - - sigBytes := reply.GetSignature() - point, err := new(core.Signature).Deserialize(sigBytes) - if err != nil { - return nil, err - } - sig := &core.Signature{G1Point: point} - return sig, nil -} -func GetStoreChunksRequest( - blobMessages []*core.EncodedBlobMessage, - batchHeader *core.BatchHeader, - useGnarkBundleEncoding bool, -) (*node.StoreChunksRequest, int64, error) { - blobs := make([]*node.Blob, len(blobMessages)) - totalSize := int64(0) - for i, blob := range blobMessages { - var err error - blobs[i], err = getBlobMessage(blob, useGnarkBundleEncoding) - if err != nil { - return nil, 0, err - } - totalSize += getBundlesSize(blob) - } - - request := &node.StoreChunksRequest{ - BatchHeader: getBatchHeaderMessage(batchHeader), - Blobs: blobs, - } - - return request, totalSize, nil -} - -func GetStoreBlobsRequest( - blobMessages []*core.EncodedBlobMessage, - batchHeader *core.BatchHeader, - useGnarkBundleEncoding bool, -) (*node.StoreBlobsRequest, int64, error) { - blobs := make([]*node.Blob, len(blobMessages)) - totalSize := int64(0) - for i, blob := range blobMessages { - var err error - blobs[i], err = getBlobMessage(blob, useGnarkBundleEncoding) - if err != nil { - return nil, 0, err - } - totalSize += getBundlesSize(blob) - } - - request := &node.StoreBlobsRequest{ - Blobs: blobs, - ReferenceBlockNumber: uint32(batchHeader.ReferenceBlockNumber), - } - - return request, totalSize, nil -} - -func getBlobMessage(blob *core.EncodedBlobMessage, useGnarkBundleEncoding bool) (*node.Blob, error) { - if blob.BlobHeader == nil { - return nil, errors.New("blob header is nil") - } - if blob.BlobHeader.Commitment == nil { - return nil, errors.New("blob header commitment is nil") - } - commitData := &commonpb.G1Commitment{ - X: blob.BlobHeader.Commitment.X.Marshal(), - Y: blob.BlobHeader.Commitment.Y.Marshal(), - } - var lengthCommitData, lengthProofData node.G2Commitment - if blob.BlobHeader.LengthCommitment != nil { - lengthCommitData.XA0 = blob.BlobHeader.LengthCommitment.X.A0.Marshal() - lengthCommitData.XA1 = blob.BlobHeader.LengthCommitment.X.A1.Marshal() - lengthCommitData.YA0 = blob.BlobHeader.LengthCommitment.Y.A0.Marshal() - lengthCommitData.YA1 = blob.BlobHeader.LengthCommitment.Y.A1.Marshal() - } - if blob.BlobHeader.LengthProof != nil { - lengthProofData.XA0 = blob.BlobHeader.LengthProof.X.A0.Marshal() - lengthProofData.XA1 = blob.BlobHeader.LengthProof.X.A1.Marshal() - lengthProofData.YA0 = blob.BlobHeader.LengthProof.Y.A0.Marshal() - lengthProofData.YA1 = blob.BlobHeader.LengthProof.Y.A1.Marshal() - } - - quorumHeaders := make([]*node.BlobQuorumInfo, len(blob.BlobHeader.QuorumInfos)) - - for i, header := range blob.BlobHeader.QuorumInfos { - quorumHeaders[i] = &node.BlobQuorumInfo{ - QuorumId: uint32(header.QuorumID), - AdversaryThreshold: uint32(header.AdversaryThreshold), - ChunkLength: uint32(header.ChunkLength), - ConfirmationThreshold: uint32(header.ConfirmationThreshold), - Ratelimit: header.QuorumRate, - } - } - - var err error - bundles := make([]*node.Bundle, len(quorumHeaders)) - if useGnarkBundleEncoding { - // the ordering of quorums in bundles must be same as in quorumHeaders - for i, quorumHeader := range quorumHeaders { - quorum := quorumHeader.GetQuorumId() - if chunksData, ok := blob.EncodedBundles[uint8(quorum)]; ok { - if chunksData.Format != core.GnarkChunkEncodingFormat { - chunksData, err = chunksData.ToGnarkFormat() - if err != nil { - return nil, err - } - } - bundleBytes, err := chunksData.FlattenToBundle() - if err != nil { - return nil, err - } - bundles[i] = &node.Bundle{ - Bundle: bundleBytes, - } - } else { - bundles[i] = &node.Bundle{ - // empty bundle for quorums operators are not part of - Bundle: make([]byte, 0), - } - } - } - } else { - // the ordering of quorums in bundles must be same as in quorumHeaders - for i, quorumHeader := range quorumHeaders { - quorum := quorumHeader.GetQuorumId() - if chunksData, ok := blob.EncodedBundles[uint8(quorum)]; ok { - if chunksData.Format != core.GobChunkEncodingFormat { - chunksData, err = chunksData.ToGobFormat() - if err != nil { - return nil, err - } - } - bundles[i] = &node.Bundle{ - Chunks: chunksData.Chunks, - } - } else { - bundles[i] = &node.Bundle{ - // empty bundle for quorums operators are not part of - Chunks: make([][]byte, 0), - } - } - } - } - - return &node.Blob{ - Header: &node.BlobHeader{ - Commitment: commitData, - LengthCommitment: &lengthCommitData, - LengthProof: &lengthProofData, - Length: uint32(blob.BlobHeader.Length), - QuorumHeaders: quorumHeaders, - }, - Bundles: bundles, - }, nil -} - -func getBatchHeaderMessage(header *core.BatchHeader) *node.BatchHeader { - - return &node.BatchHeader{ - BatchRoot: header.BatchRoot[:], - ReferenceBlockNumber: uint32(header.ReferenceBlockNumber), - } -} - -func getBundlesSize(blob *core.EncodedBlobMessage) int64 { - size := int64(0) - for _, bundle := range blob.EncodedBundles { - size += int64(bundle.Size()) - } - return size -} diff --git a/disperser/batcher/metrics.go b/disperser/batcher/metrics.go deleted file mode 100644 index b0762a1f96..0000000000 --- a/disperser/batcher/metrics.go +++ /dev/null @@ -1,411 +0,0 @@ -package batcher - -import ( - "context" - "fmt" - "net/http" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/common" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -type FailReason string - -const ( - FailBatchHeaderHash FailReason = "batch_header_hash" - FailAggregateSignatures FailReason = "aggregate_signatures" - FailNoSignatures FailReason = "no_signatures" - FailConfirmBatch FailReason = "confirm_batch" - FailGetBatchID FailReason = "get_batch_id" - FailUpdateConfirmationInfo FailReason = "update_confirmation_info" - FailNoAggregatedSignature FailReason = "no_aggregated_signature" -) - -type MetricsConfig struct { - HTTPPort string - EnableMetrics bool -} - -type EncodingStreamerMetrics struct { - EncodedBlobs *prometheus.GaugeVec - BlobEncodingLatency *prometheus.SummaryVec -} - -type TxnManagerMetrics struct { - Latency *prometheus.SummaryVec - GasUsed prometheus.Gauge - SpeedUps prometheus.Gauge - TxQueue prometheus.Gauge - NumTx *prometheus.CounterVec -} - -type FinalizerMetrics struct { - NumBlobs *prometheus.CounterVec - LastSeenFinalizedBlock prometheus.Gauge - Latency *prometheus.SummaryVec -} - -type DispatcherMetrics struct { - Latency *prometheus.SummaryVec - OperatorLatency *prometheus.GaugeVec -} - -type Metrics struct { - *EncodingStreamerMetrics - *TxnManagerMetrics - *FinalizerMetrics - *DispatcherMetrics - - registry *prometheus.Registry - - Blob *prometheus.CounterVec - Batch *prometheus.CounterVec - BatchProcLatency *prometheus.SummaryVec - BatchProcLatencyHistogram *prometheus.HistogramVec - BlobAge *prometheus.SummaryVec - BlobSizeTotal *prometheus.CounterVec - Attestation *prometheus.GaugeVec - BatchError *prometheus.CounterVec - - httpPort string - logger logging.Logger -} - -func NewMetrics(httpPort string, logger logging.Logger) *Metrics { - namespace := "eigenda_batcher" - reg := prometheus.NewRegistry() - reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - reg.MustRegister(collectors.NewGoCollector()) - encodingStreamerMetrics := EncodingStreamerMetrics{ - EncodedBlobs: promauto.With(reg).NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "encoded_blobs", - Help: "number and size of all encoded blobs", - }, - []string{"type"}, - ), - BlobEncodingLatency: promauto.With(reg).NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: namespace, - Name: "blob_encoding_latency_ms", - Help: "blob encoding latency summary in milliseconds", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, - }, - []string{"state", "quorum", "size_bucket"}, - ), - } - - txnManagerMetrics := TxnManagerMetrics{ - Latency: promauto.With(reg).NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: namespace, - Name: "txn_manager_latency_ms", - Help: "transaction confirmation latency summary in milliseconds", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, - }, - []string{"stage"}, - ), - GasUsed: promauto.With(reg).NewGauge( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "gas_used", - Help: "gas used for onchain batch confirmation", - }, - ), - SpeedUps: promauto.With(reg).NewGauge( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "speed_ups", - Help: "number of times the gas price was increased", - }, - ), - TxQueue: promauto.With(reg).NewGauge( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "tx_queue", - Help: "number of transactions in transaction queue", - }, - ), - NumTx: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "tx_total", - Help: "number of transactions processed", - }, - []string{"state"}, - ), - } - - finalizerMetrics := FinalizerMetrics{ - NumBlobs: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "finalizer_num_blobs", - Help: "number of blobs in each state", - }, - []string{"state"}, // possible values are "processed", "failed", "finalized" - ), - LastSeenFinalizedBlock: promauto.With(reg).NewGauge( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "last_finalized_block", - Help: "last finalized block number", - }, - ), - Latency: promauto.With(reg).NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: namespace, - Name: "finalizer_process_latency_ms", - Help: "finalizer process latency summary in milliseconds", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, - }, - []string{"stage"}, // possible values are "round" and "total" - ), - } - - dispatcherMatrics := DispatcherMetrics{ - Latency: promauto.With(reg).NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: namespace, - Name: "attestation_latency_ms", - Help: "attestation latency summary in milliseconds", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, - }, - []string{"operator_id", "status"}, - ), - OperatorLatency: promauto.With(reg).NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "operator_attestation_latency_ms", - Help: "attestation latency in ms observed for operators", - }, - []string{"operator_id"}, - ), - } - - metrics := &Metrics{ - EncodingStreamerMetrics: &encodingStreamerMetrics, - TxnManagerMetrics: &txnManagerMetrics, - FinalizerMetrics: &finalizerMetrics, - DispatcherMetrics: &dispatcherMatrics, - Blob: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "blobs_total", - Help: "the number and unencoded size of total dispersal blobs, if a blob is in multiple quorums, it'll only be counted once", - }, - []string{"state", "data"}, // state is either success or failure - ), - Batch: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "batches_total", - Help: "the number and unencoded size of total dispersal batches", - }, - []string{"data"}, - ), - BatchProcLatency: promauto.With(reg).NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: namespace, - Name: "batch_process_latency_ms", - Help: "batch process latency summary in milliseconds", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, - }, - []string{"stage"}, - ), - BatchProcLatencyHistogram: promauto.With(reg).NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: namespace, - Name: "batch_process_latency_histogram_ms", - Help: "batch process latency histogram in milliseconds", - // In minutes: 1, 2, 3, 5, 8, 10, 13, 15, 21, 34, 55, 89 - Buckets: []float64{60_000, 120_000, 180_000, 300_000, 480_000, 600_000, 780_000, 900_000, 1_260_000, 2_040_000, 3_300_000, 5_340_000}, - }, - []string{"stage"}, - ), - BlobAge: promauto.With(reg).NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: namespace, - Name: "blob_age_ms", - Help: "blob age (in ms) since dispersal request time at different stages of its lifecycle", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, - }, - // The stage would be: - // encoding_requested -> encoded -> batched -> attestation_requested -> attested -> confirmed - []string{"stage"}, - ), - BlobSizeTotal: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "blob_size_total", - Help: "the size in bytes of unencoded blobs, if a blob is in multiple quorums, it'll be acounted multiple times", - }, - []string{"stage", "quorum"}, - ), - Attestation: promauto.With(reg).NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "attestation", - Help: "number of signers and non-signers for the batch", - }, - []string{"type", "quorum"}, - ), - BatchError: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: namespace, - Name: "batch_error", - Help: "number of batch errors", - }, - []string{"type"}, - ), - registry: reg, - httpPort: httpPort, - logger: logger.With("component", "BatcherMetrics"), - } - return metrics -} - -func (g *Metrics) UpdateAttestation(operatorCount map[core.QuorumID]int, signerCount map[core.QuorumID]int, quorumResults map[core.QuorumID]*core.QuorumResult) { - for quorumID, count := range operatorCount { - quorumStr := fmt.Sprintf("%d", quorumID) - signers, ok := signerCount[quorumID] - if !ok { - g.logger.Error("signer count not found for quorum", "quorum", quorumID) - continue - } - nonSigners := count - signers - quorumResult, ok := quorumResults[quorumID] - if !ok { - g.logger.Error("quorum result not found for quorum", "quorum", quorumID) - continue - } - - g.Attestation.WithLabelValues("signers", quorumStr).Set(float64(signers)) - g.Attestation.WithLabelValues("non_signers", quorumStr).Set(float64(nonSigners)) - g.Attestation.WithLabelValues("percent_signed", quorumStr).Set(float64(quorumResult.PercentSigned)) - } -} - -func (t *DispatcherMetrics) ObserveLatency(operatorId string, success bool, latencyMS float64) { - label := "success" - if !success { - label = "failure" - } - // The Latency metric has "operator_id" but we null it out because it's separately - // tracked in OperatorLatency. - t.Latency.WithLabelValues("", label).Observe(latencyMS) - // Only tracks successful requests, so there is one stream per operator. - // This is sufficient to provide insights of operators' performance. - if success { - t.OperatorLatency.WithLabelValues(operatorId).Set(latencyMS) - } -} - -// UpdateCompletedBlob increments the number and updates size of processed blobs. -func (g *Metrics) UpdateCompletedBlob(size int, status disperser.BlobStatus) { - switch status { - case disperser.Confirmed: - g.Blob.WithLabelValues("confirmed", "number").Inc() - g.Blob.WithLabelValues("confirmed", "size").Add(float64(size)) - case disperser.Failed: - g.Blob.WithLabelValues("failed", "number").Inc() - g.Blob.WithLabelValues("failed", "size").Add(float64(size)) - case disperser.InsufficientSignatures: - g.Blob.WithLabelValues("insufficient_signature", "number").Inc() - g.Blob.WithLabelValues("insufficient_signature", "size").Add(float64(size)) - default: - return - } - - g.Blob.WithLabelValues("total", "number").Inc() - g.Blob.WithLabelValues("total", "size").Add(float64(size)) -} - -func (g *Metrics) IncrementBatchCount(size int64) { - g.Batch.WithLabelValues("number").Inc() - g.Batch.WithLabelValues("size").Add(float64(size)) -} - -func (g *Metrics) UpdateBatchError(errType FailReason, numBlobs int) { - g.BatchError.WithLabelValues(string(errType)).Add(float64(numBlobs)) -} - -func (g *Metrics) ObserveLatency(stage string, latencyMs float64) { - g.BatchProcLatency.WithLabelValues(stage).Observe(latencyMs) - g.BatchProcLatencyHistogram.WithLabelValues(stage).Observe(latencyMs) -} - -func (g *Metrics) ObserveBlobAge(stage string, ageMs float64) { - g.BlobAge.WithLabelValues(stage).Observe(ageMs) -} - -func (g *Metrics) IncrementBlobSize(stage string, quorumId core.QuorumID, blobSize int) { - g.BlobSizeTotal.WithLabelValues(stage, fmt.Sprintf("%d", quorumId)).Add(float64(blobSize)) -} - -func (g *Metrics) Start(ctx context.Context) { - g.logger.Info("starting metrics server at ", "port", g.httpPort) - addr := fmt.Sprintf(":%s", g.httpPort) - go func() { - log := g.logger - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.HandlerFor( - g.registry, - promhttp.HandlerOpts{}, - )) - err := http.ListenAndServe(addr, mux) - log.Error("prometheus server failed", "err", err) - }() -} - -func (e *EncodingStreamerMetrics) UpdateEncodedBlobs(count int, size uint64) { - e.EncodedBlobs.WithLabelValues("size").Set(float64(size)) - e.EncodedBlobs.WithLabelValues("number").Set(float64(count)) -} - -func (e *EncodingStreamerMetrics) ObserveEncodingLatency(state string, quorumId core.QuorumID, blobSize int, latencyMs float64) { - e.BlobEncodingLatency.WithLabelValues(state, fmt.Sprintf("%d", quorumId), common.BlobSizeBucket(blobSize)).Observe(latencyMs) -} - -func (t *TxnManagerMetrics) ObserveLatency(stage string, latencyMs float64) { - t.Latency.WithLabelValues(stage).Observe(latencyMs) -} - -func (t *TxnManagerMetrics) UpdateGasUsed(gasUsed uint64) { - t.GasUsed.Set(float64(gasUsed)) -} - -func (t *TxnManagerMetrics) UpdateSpeedUps(speedUps int) { - t.SpeedUps.Set(float64(speedUps)) -} - -func (t *TxnManagerMetrics) UpdateTxQueue(txQueue int) { - t.TxQueue.Set(float64(txQueue)) -} - -func (t *TxnManagerMetrics) IncrementTxnCount(state string) { - t.NumTx.WithLabelValues(state).Inc() -} - -func (f *FinalizerMetrics) IncrementNumBlobs(state string) { - f.NumBlobs.WithLabelValues(state).Inc() -} - -func (f *FinalizerMetrics) UpdateNumBlobs(state string, count int) { - f.NumBlobs.WithLabelValues(state).Add(float64(count)) -} - -func (f *FinalizerMetrics) UpdateLastSeenFinalizedBlock(blockNumber uint64) { - f.LastSeenFinalizedBlock.Set(float64(blockNumber)) -} - -func (f *FinalizerMetrics) ObserveLatency(stage string, latencyMs float64) { - f.Latency.WithLabelValues(stage).Observe(latencyMs) -} diff --git a/disperser/batcher/mock/finalizer.go b/disperser/batcher/mock/finalizer.go deleted file mode 100644 index ee2fbee617..0000000000 --- a/disperser/batcher/mock/finalizer.go +++ /dev/null @@ -1,22 +0,0 @@ -package mock - -import ( - "context" - - "github.com/stretchr/testify/mock" -) - -type MockFinalizer struct { - mock.Mock -} - -func NewFinalizer() *MockFinalizer { - return &MockFinalizer{} -} - -func (b *MockFinalizer) Start(ctx context.Context) {} - -func (b *MockFinalizer) FinalizeBlobs(ctx context.Context) error { - args := b.Called() - return args.Error(0) -} diff --git a/disperser/batcher/mock/txn_manager.go b/disperser/batcher/mock/txn_manager.go deleted file mode 100644 index cec8cab703..0000000000 --- a/disperser/batcher/mock/txn_manager.go +++ /dev/null @@ -1,33 +0,0 @@ -package mock - -import ( - "context" - - "github.com/Layr-Labs/eigenda/disperser/batcher" - "github.com/stretchr/testify/mock" -) - -type MockTxnManager struct { - mock.Mock - - Requests []*batcher.TxnRequest -} - -var _ batcher.TxnManager = (*MockTxnManager)(nil) - -func NewTxnManager() *MockTxnManager { - return &MockTxnManager{} -} - -func (b *MockTxnManager) Start(ctx context.Context) {} - -func (b *MockTxnManager) ProcessTransaction(ctx context.Context, req *batcher.TxnRequest) error { - args := b.Called() - b.Requests = append(b.Requests, req) - return args.Error(0) -} - -func (b *MockTxnManager) ReceiptChan() chan *batcher.ReceiptOrErr { - args := b.Called() - return args.Get(0).(chan *batcher.ReceiptOrErr) -} diff --git a/disperser/batcher/txn_manager.go b/disperser/batcher/txn_manager.go deleted file mode 100644 index c3d343a3b1..0000000000 --- a/disperser/batcher/txn_manager.go +++ /dev/null @@ -1,428 +0,0 @@ -package batcher - -import ( - "context" - "errors" - "fmt" - "math/big" - "net/url" - "sync" - "time" - - "github.com/Layr-Labs/eigenda/common" - walletsdk "github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/core/types" -) - -// percentage multiplier for gas price. It needs to be >= 10 to properly replace existing transaction -// e.g. 10 means 10% increase -var ( - gasPricePercentageMultiplier = big.NewInt(10) - hundred = big.NewInt(100) - maxSendTransactionRetry = 3 - queryTickerDuration = 3 * time.Second - ErrTransactionNotBroadcasted = errors.New("transaction not broadcasted") -) - -// TxnManager receives transactions from the caller, sends them to the chain, and monitors their status. -// It also handles the case where a transaction is not mined within a certain time. In this case, it will -// resend the transaction with a higher gas price. It is assumed that all transactions originate from the -// same account. -type TxnManager interface { - Start(ctx context.Context) - ProcessTransaction(ctx context.Context, req *TxnRequest) error - ReceiptChan() chan *ReceiptOrErr -} - -type transaction struct { - *types.Transaction - TxID walletsdk.TxID - requestedAt time.Time -} - -type TxnRequest struct { - Tx *types.Transaction - Tag string - Value *big.Int - Metadata interface{} - - requestedAt time.Time - // txAttempts are the transactions that have been attempted to be mined for this request. - // If a transaction hasn't been confirmed within the timeout and a replacement transaction is sent, - // the original transaction hash will be kept in this slice - txAttempts []*transaction -} - -// ReceiptOrErr is a wrapper for a transaction receipt or an error. -// Receipt should be nil if there is an error, and non-nil if there is no error. -// Metadata is the metadata passed in with the transaction request. -type ReceiptOrErr struct { - Receipt *types.Receipt - Metadata interface{} - Err error -} - -type txnManager struct { - mu sync.Mutex - - ethClient common.EthClient - wallet walletsdk.Wallet - numConfirmations int - requestChan chan *TxnRequest - logger logging.Logger - - receiptChan chan *ReceiptOrErr - queueSize int - txnBroadcastTimeout time.Duration - txnRefreshInterval time.Duration - metrics *TxnManagerMetrics -} - -var _ TxnManager = (*txnManager)(nil) - -func NewTxnManager(ethClient common.EthClient, wallet walletsdk.Wallet, numConfirmations, queueSize int, txnBroadcastTimeout time.Duration, txnRefreshInterval time.Duration, logger logging.Logger, metrics *TxnManagerMetrics) TxnManager { - return &txnManager{ - ethClient: ethClient, - wallet: wallet, - numConfirmations: numConfirmations, - requestChan: make(chan *TxnRequest, queueSize), - logger: logger.With("component", "TxnManager"), - receiptChan: make(chan *ReceiptOrErr, queueSize), - queueSize: queueSize, - txnBroadcastTimeout: txnBroadcastTimeout, - txnRefreshInterval: txnRefreshInterval, - metrics: metrics, - } -} - -func NewTxnRequest(tx *types.Transaction, tag string, value *big.Int, metadata interface{}) *TxnRequest { - return &TxnRequest{ - Tx: tx, - Tag: tag, - Value: value, - Metadata: metadata, - - requestedAt: time.Now(), - txAttempts: make([]*transaction, 0), - } -} - -func (t *txnManager) Start(ctx context.Context) { - go func() { - for { - select { - case <-ctx.Done(): - return - case req := <-t.requestChan: - receipt, err := t.monitorTransaction(ctx, req) - if err != nil { - t.receiptChan <- &ReceiptOrErr{ - Receipt: nil, - Metadata: req.Metadata, - Err: err, - } - } else { - t.receiptChan <- &ReceiptOrErr{ - Receipt: receipt, - Metadata: req.Metadata, - Err: nil, - } - if receipt.GasUsed > 0 { - t.metrics.UpdateGasUsed(receipt.GasUsed) - } - } - t.metrics.ObserveLatency("total", float64(time.Since(req.requestedAt).Milliseconds())) - } - } - }() - t.logger.Info("started TxnManager") -} - -// ProcessTransaction sends the transaction and queues the transaction for monitoring. -// It returns an error if the transaction fails to be confirmed for reasons other than timeouts. -// TxnManager monitors the transaction and resends it with a higher gas price if it is not mined without a timeout until the transaction is confirmed or failed. -func (t *txnManager) ProcessTransaction(ctx context.Context, req *TxnRequest) error { - t.mu.Lock() - defer t.mu.Unlock() - t.logger.Debug("new transaction", "tag", req.Tag, "nonce", req.Tx.Nonce(), "gasFeeCap", req.Tx.GasFeeCap(), "gasTipCap", req.Tx.GasTipCap()) - - var txn *types.Transaction - var txID walletsdk.TxID - var err error - retryFromFailure := 0 - for retryFromFailure < maxSendTransactionRetry { - gasTipCap, gasFeeCap, err := t.ethClient.GetLatestGasCaps(ctx) - if err != nil { - return fmt.Errorf("failed to get latest gas caps: %w", err) - } - - txn, err = t.ethClient.UpdateGas(ctx, req.Tx, req.Value, gasTipCap, gasFeeCap) - if err != nil { - return fmt.Errorf("failed to update gas price: %w", err) - } - txID, err = t.wallet.SendTransaction(ctx, txn) - var urlErr *url.Error - didTimeout := false - if errors.As(err, &urlErr) { - didTimeout = urlErr.Timeout() - } - if didTimeout || errors.Is(err, context.DeadlineExceeded) { - t.logger.Warn("failed to send txn due to timeout", "tag", req.Tag, "hash", txn.Hash().Hex(), "numRetries", retryFromFailure, "maxRetry", maxSendTransactionRetry, "err", err) - retryFromFailure++ - continue - } else if err != nil { - return fmt.Errorf("failed to send txn (%s) %s: %w", req.Tag, txn.Hash().Hex(), err) - } else { - t.logger.Debug("successfully sent txn", "tag", req.Tag, "txID", txID, "txHash", txn.Hash().Hex()) - break - } - } - - if txn == nil || txID == "" { - return fmt.Errorf("failed to send txn (%s) %s: %w", req.Tag, req.Tx.Hash().Hex(), err) - } - - req.Tx = txn - req.txAttempts = append(req.txAttempts, &transaction{ - TxID: txID, - Transaction: txn, - requestedAt: time.Now(), - }) - - t.requestChan <- req - t.metrics.UpdateTxQueue(len(t.requestChan)) - return nil -} - -func (t *txnManager) ReceiptChan() chan *ReceiptOrErr { - return t.receiptChan -} - -// ensureAnyTransactionBroadcasted waits until all given transactions are broadcasted to the network. -func (t *txnManager) ensureAnyTransactionBroadcasted(ctx context.Context, txs []*transaction) error { - queryTicker := time.NewTicker(queryTickerDuration) - defer queryTicker.Stop() - - for { - for _, tx := range txs { - _, err := t.wallet.GetTransactionReceipt(ctx, tx.TxID) - if err == nil || errors.Is(err, ethereum.NotFound) || errors.Is(err, walletsdk.ErrReceiptNotYetAvailable) { - t.metrics.ObserveLatency("broadcasted", float64(time.Since(tx.requestedAt).Milliseconds())) - return nil - } - } - - // Wait for the next round. - select { - case <-ctx.Done(): - return ctx.Err() - case <-queryTicker.C: - } - } -} - -func (t *txnManager) ensureAnyTransactionEvaled(ctx context.Context, txs []*transaction) (*types.Receipt, error) { - queryTicker := time.NewTicker(queryTickerDuration) - defer queryTicker.Stop() - var receipt *types.Receipt - var err error - // transactions that need to be queried. Some transactions will be removed from this map depending on their status. - txnsToQuery := make(map[walletsdk.TxID]*types.Transaction, len(txs)) - for _, tx := range txs { - txnsToQuery[tx.TxID] = tx.Transaction - } - - for { - for txID, tx := range txnsToQuery { - receipt, err = t.wallet.GetTransactionReceipt(ctx, txID) - if err == nil { - chainTip, err := t.ethClient.BlockNumber(ctx) - if err == nil { - if receipt.BlockNumber.Uint64()+uint64(t.numConfirmations) > chainTip { - t.logger.Debug("transaction has been mined but don't have enough confirmations at current chain tip", "nonce", tx.Nonce(), "txnBlockNumber", receipt.BlockNumber.Uint64(), "numConfirmations", t.numConfirmations, "chainTip", chainTip) - break - } else { - t.logger.Info("transaction has been mined and has enough confirmations", "nonce", tx.Nonce(), "txnBlockNumber", receipt.BlockNumber.Uint64(), "numConfirmations", t.numConfirmations, "chainTip", chainTip) - return receipt, nil - } - } else { - t.logger.Debug("failed to get chain tip while waiting for transaction to mine", "err", err) - } - } - - if errors.Is(err, ethereum.NotFound) || errors.Is(err, walletsdk.ErrReceiptNotYetAvailable) { - t.logger.Debug("Transaction not yet mined", "txID", txID, "txHash", tx.Hash().Hex(), "err", err) - } else if errors.Is(err, walletsdk.ErrTransactionFailed) { - t.logger.Debug("Transaction failed", "txID", txID, "txHash", tx.Hash().Hex(), "err", err) - delete(txnsToQuery, txID) - } else if errors.Is(err, walletsdk.ErrNotYetBroadcasted) { - t.logger.Error("Transaction has not been broadcasted to network but attempted to retrieve receipt", "err", err) - } else { - t.logger.Debug("Transaction receipt retrieval failed", "err", err) - } - } - - if len(txnsToQuery) == 0 { - return nil, fmt.Errorf("all transactions failed") - } - - // Wait for the next round. - select { - case <-ctx.Done(): - return receipt, ctx.Err() - case <-queryTicker.C: - } - } -} - -// monitorTransaction waits until the transaction is confirmed (or failed) and resends it with a higher gas price if it is not mined without a timeout. -// It returns the receipt once the transaction has been confirmed. -// It returns an error if the transaction fails to be sent for reasons other than timeouts. -func (t *txnManager) monitorTransaction(ctx context.Context, req *TxnRequest) (*types.Receipt, error) { - numSpeedUps := 0 - retryFromFailure := 0 - - var receipt *types.Receipt - var err error - - rpcCallAttempt := func() error { - t.logger.Debug("monitoring transaction", "txHash", req.Tx.Hash().Hex(), "tag", req.Tag, "nonce", req.Tx.Nonce()) - - ctxWithTimeout, cancelBroadcastTimeout := context.WithTimeout(ctx, t.txnBroadcastTimeout) - defer cancelBroadcastTimeout() - - // Ensure transactions are broadcasted to the network before querying the receipt. - // This is to avoid querying the receipt of a transaction that hasn't been broadcasted yet. - // For example, when Fireblocks wallet is used, there may be delays in broadcasting the transaction due to latency from cosigning and MPC operations. - err = t.ensureAnyTransactionBroadcasted(ctxWithTimeout, req.txAttempts) - if err != nil && errors.Is(err, context.DeadlineExceeded) { - t.logger.Warn("transaction not broadcasted within timeout", "tag", req.Tag, "txHash", req.Tx.Hash().Hex(), "nonce", req.Tx.Nonce()) - fireblocksWallet, ok := t.wallet.(interface { - CancelTransactionBroadcast(ctx context.Context, txID walletsdk.TxID) (bool, error) - }) - if ok { - // Consider these transactions failed as they haven't been broadcasted within timeout. - // Cancel these transactions to avoid blocking the next transactions. - for _, tx := range req.txAttempts { - cancelled, err := fireblocksWallet.CancelTransactionBroadcast(ctx, tx.TxID) - if err != nil { - t.logger.Warn("failed to cancel Fireblocks transaction broadcast", "txID", tx.TxID, "err", err) - } else if cancelled { - t.logger.Info("cancelled Fireblocks transaction broadcast because it didn't get broadcasted within timeout", "txID", tx.TxID, "timeout", t.txnBroadcastTimeout.String()) - } - } - } - return ErrTransactionNotBroadcasted - } else if err != nil { - t.logger.Error("unexpected error while waiting for Fireblocks transaction to broadcast", "txHash", req.Tx.Hash().Hex(), "err", err) - return err - } - - ctxWithTimeout, cancelEvaluationTimeout := context.WithTimeout(ctx, t.txnRefreshInterval) - defer cancelEvaluationTimeout() - receipt, err = t.ensureAnyTransactionEvaled( - ctxWithTimeout, - req.txAttempts, - ) - return err - } - - for { - err = rpcCallAttempt() - if err == nil { - t.metrics.UpdateSpeedUps(numSpeedUps) - t.metrics.IncrementTxnCount("success") - return receipt, nil - } - - if errors.Is(err, context.DeadlineExceeded) { - if receipt != nil { - t.logger.Warn("transaction has been mined, but hasn't accumulated the required number of confirmations", "tag", req.Tag, "txHash", req.Tx.Hash().Hex(), "nonce", req.Tx.Nonce()) - continue - } - t.logger.Warn("transaction not mined within timeout, resending with higher gas price", "tag", req.Tag, "txHash", req.Tx.Hash().Hex(), "nonce", req.Tx.Nonce()) - newTx, err := t.speedUpTxn(ctx, req.Tx, req.Tag) - if err != nil { - t.logger.Error("failed to speed up transaction", "err", err) - t.metrics.IncrementTxnCount("failure") - return nil, err - } - txID, err := t.wallet.SendTransaction(ctx, newTx) - if err != nil { - if retryFromFailure >= maxSendTransactionRetry { - t.logger.Warn("failed to send txn - retries exhausted", "tag", req.Tag, "txn", req.Tx.Hash().Hex(), "attempt", retryFromFailure, "maxRetry", maxSendTransactionRetry, "err", err) - t.metrics.IncrementTxnCount("failure") - return nil, err - } else { - t.logger.Warn("failed to send txn - retrying", "tag", req.Tag, "txn", req.Tx.Hash().Hex(), "attempt", retryFromFailure, "maxRetry", maxSendTransactionRetry, "err", err) - } - retryFromFailure++ - continue - } - - t.logger.Debug("successfully sent txn", "tag", req.Tag, "txID", txID, "txHash", newTx.Hash().Hex()) - req.Tx = newTx - req.txAttempts = append(req.txAttempts, &transaction{ - TxID: txID, - Transaction: newTx, - }) - numSpeedUps++ - } else { - t.logger.Error("transaction failed", "tag", req.Tag, "txHash", req.Tx.Hash().Hex(), "err", err) - t.metrics.IncrementTxnCount("failure") - return nil, err - } - } -} - -// speedUpTxn increases the gas price of the existing transaction by specified percentage. -// It makes sure the new gas price is not lower than the current gas price. -func (t *txnManager) speedUpTxn(ctx context.Context, tx *types.Transaction, tag string) (*types.Transaction, error) { - prevGasTipCap := tx.GasTipCap() - prevGasFeeCap := tx.GasFeeCap() - // get the gas tip cap and gas fee cap based on current network condition - currentGasTipCap, currentGasFeeCap, err := t.ethClient.GetLatestGasCaps(ctx) - if err != nil { - return nil, err - } - increasedGasTipCap := increaseGasPrice(prevGasTipCap) - increasedGasFeeCap := increaseGasPrice(prevGasFeeCap) - // make sure increased gas prices are not lower than current gas prices - var newGasTipCap, newGasFeeCap *big.Int - if currentGasTipCap.Cmp(increasedGasTipCap) > 0 { - newGasTipCap = currentGasTipCap - } else { - newGasTipCap = increasedGasTipCap - } - if currentGasFeeCap.Cmp(increasedGasFeeCap) > 0 { - newGasFeeCap = currentGasFeeCap - } else { - newGasFeeCap = increasedGasFeeCap - } - - t.logger.Info("increasing gas price", "tag", tag, "txHash", tx.Hash().Hex(), "nonce", tx.Nonce(), "prevGasTipCap", prevGasTipCap, "prevGasFeeCap", prevGasFeeCap, "newGasTipCap", newGasTipCap, "newGasFeeCap", newGasFeeCap) - return t.ethClient.UpdateGas(ctx, tx, tx.Value(), newGasTipCap, newGasFeeCap) -} - -// increaseGasPrice increases the gas price by specified percentage. -// i.e. gasPrice + ((gasPrice * gasPricePercentageMultiplier + 99) / 100) -func increaseGasPrice(gasPrice *big.Int) *big.Int { - if gasPrice == nil { - return nil - } - bump := new(big.Int).Mul(gasPrice, gasPricePercentageMultiplier) - bump = roundUpDivideBig(bump, hundred) - return new(big.Int).Add(gasPrice, bump) -} - -func roundUpDivideBig(a, b *big.Int) *big.Int { - if a == nil || b == nil || b.Cmp(big.NewInt(0)) == 0 { - return nil - } - one := new(big.Int).SetUint64(1) - num := new(big.Int).Sub(new(big.Int).Add(a, b), one) // a + b - 1 - res := new(big.Int).Div(num, b) // (a + b - 1) / b - return res -} diff --git a/disperser/batcher/txn_manager_test.go b/disperser/batcher/txn_manager_test.go deleted file mode 100644 index 0eecdd1715..0000000000 --- a/disperser/batcher/txn_manager_test.go +++ /dev/null @@ -1,295 +0,0 @@ -package batcher_test - -import ( - "context" - "errors" - "math/big" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/common/mock" - "github.com/Layr-Labs/eigenda/disperser/batcher" - "github.com/Layr-Labs/eigenda/test" - sdkmock "github.com/Layr-Labs/eigensdk-go/chainio/clients/mocks" - walletsdk "github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" -) - -func TestProcessTransaction(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, 100*time.Millisecond, 100*time.Millisecond, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txID := "1234" - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil) - ethClient.On("BlockNumber").Return(uint64(123), nil) - gomock.InOrder( - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(txID, nil), - w.EXPECT().GetTransactionReceipt(gomock.Any(), gomock.Any()).Return(&types.Receipt{ - BlockNumber: new(big.Int).SetUint64(1), - }, nil).Times(2), - ) - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - assert.NoError(t, err) - receiptOrErr := <-txnManager.ReceiptChan() - assert.NoError(t, receiptOrErr.Err) - assert.Equal(t, uint64(1), receiptOrErr.Receipt.BlockNumber.Uint64()) - - // now test the case where the replacement transaction fails - randomErr := errors.New("random error") - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(txID, nil) - w.EXPECT().GetTransactionReceipt(gomock.Any(), gomock.Any()).Return(nil, randomErr).AnyTimes() - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return("", randomErr).AnyTimes() - - err = txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - receiptOrErr = <-txnManager.ReceiptChan() - assert.Error(t, receiptOrErr.Err, randomErr) - assert.Nil(t, receiptOrErr.Receipt) -} - -func TestReplaceGasFee(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, 100*time.Millisecond, 100*time.Millisecond, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil) - ethClient.On("BlockNumber").Return(uint64(123), nil) - - // assume that the transaction is not mined within the timeout - badTxID := "1234" - validTxID := "4321" - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(badTxID, nil) - w.EXPECT().GetTransactionReceipt(gomock.Any(), badTxID).Return(nil, walletsdk.ErrReceiptNotYetAvailable).AnyTimes() - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(validTxID, nil) - w.EXPECT().GetTransactionReceipt(gomock.Any(), validTxID).Return(&types.Receipt{ - BlockNumber: new(big.Int).SetUint64(1), - }, nil) - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - ethClient.AssertNumberOfCalls(t, "GetLatestGasCaps", 2) - ethClient.AssertNumberOfCalls(t, "UpdateGas", 2) -} - -func TestTransactionReplacementFailure(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, time.Second, 48*time.Second, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil).Once() - // now assume that the transaction fails on retry - speedUpFailure := errors.New("speed up failure") - ethClient.On("UpdateGas").Return(nil, speedUpFailure).Once() - - // assume that the transaction is not mined within the timeout - badTxID := "1234" - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(badTxID, nil) - w.EXPECT().GetTransactionReceipt(gomock.Any(), badTxID).Return(nil, errors.New("blah")).AnyTimes() - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - res := <-txnManager.ReceiptChan() - assert.Error(t, res.Err, speedUpFailure) -} - -func TestSendTransactionReceiptRetry(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, time.Second, 48*time.Second, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil) - ethClient.On("BlockNumber").Return(uint64(123), nil) - txID := "1234" - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(txID, nil) - // assume that the transaction is not mined within the timeout - w.EXPECT().GetTransactionReceipt(gomock.Any(), txID).Return(nil, walletsdk.ErrReceiptNotYetAvailable).Times(3) - // assume that it fails to send the replacement transaction once - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return("", errors.New("send txn failure")) - w.EXPECT().GetTransactionReceipt(gomock.Any(), txID).Return(&types.Receipt{ - BlockNumber: new(big.Int).SetUint64(1), - }, nil) - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - res := <-txnManager.ReceiptChan() - assert.NoError(t, res.Err) - assert.Equal(t, uint64(1), res.Receipt.BlockNumber.Uint64()) - ethClient.AssertNumberOfCalls(t, "GetLatestGasCaps", 2) - ethClient.AssertNumberOfCalls(t, "UpdateGas", 2) -} - -func TestSendTransactionRetrySuccess(t *testing.T) { - ctx := t.Context() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - logger := test.GetLogger() - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, time.Second, 48*time.Second, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil) - ethClient.On("BlockNumber").Return(uint64(123), nil) - txID := "1234" - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(txID, nil) - // assume that the transaction is not mined within the timeout - w.EXPECT().GetTransactionReceipt(gomock.Any(), txID).Return(nil, walletsdk.ErrReceiptNotYetAvailable).AnyTimes() - - // assume that it fails to send the replacement transaction once - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return("", errors.New("send txn failure")) - newTxID := "4321" - // second try succeeds - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(newTxID, nil) - w.EXPECT().GetTransactionReceipt(gomock.Any(), newTxID).Return(&types.Receipt{ - BlockNumber: new(big.Int).SetUint64(1), - }, nil) - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - res := <-txnManager.ReceiptChan() - assert.NoError(t, res.Err) - assert.Equal(t, uint64(1), res.Receipt.BlockNumber.Uint64()) - ethClient.AssertNumberOfCalls(t, "GetLatestGasCaps", 3) - ethClient.AssertNumberOfCalls(t, "UpdateGas", 3) -} - -func TestSendTransactionRetryFailure(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, time.Second, 48*time.Second, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil) - ethClient.On("BlockNumber").Return(uint64(123), nil) - txID := "1234" - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(txID, nil) - - // assume that it keeps failing to send the replacement transaction - sendErr := errors.New("send txn failure") - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return("", sendErr).Times(4) - - // assume that the transaction is not mined within the timeout - w.EXPECT().GetTransactionReceipt(gomock.Any(), txID).Return(nil, walletsdk.ErrReceiptNotYetAvailable).AnyTimes() - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - res := <-txnManager.ReceiptChan() - assert.Error(t, res.Err, sendErr) - assert.Nil(t, res.Receipt) - ethClient.AssertNumberOfCalls(t, "GetLatestGasCaps", 5) - ethClient.AssertNumberOfCalls(t, "UpdateGas", 5) -} - -func TestTransactionNotBroadcasted(t *testing.T) { - ctx := t.Context() - logger := test.GetLogger() - ethClient := &mock.MockEthClient{} - ctrl := gomock.NewController(t) - w := sdkmock.NewMockWallet(ctrl) - metrics := batcher.NewMetrics("9100", logger) - txnManager := batcher.NewTxnManager(ethClient, w, 0, 5, 100*time.Millisecond, 48*time.Second, logger, metrics.TxnManagerMetrics) - ctx, cancel := context.WithTimeout(ctx, time.Second*1) - defer cancel() - txnManager.Start(ctx) - txn := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1e18), 100000, big.NewInt(1e9), []byte{}) - ethClient.On("GetLatestGasCaps").Return(big.NewInt(1e9), big.NewInt(1e9), nil) - ethClient.On("UpdateGas").Return(txn, nil) - ethClient.On("BlockNumber").Return(uint64(123), nil) - txID := "1234" - w.EXPECT().SendTransaction(gomock.Any(), gomock.Any()).Return(txID, nil) - - // assume that the transaction does not get broadcasted to the network - w.EXPECT().GetTransactionReceipt(gomock.Any(), txID).Return(nil, walletsdk.ErrNotYetBroadcasted).AnyTimes() - - err := txnManager.ProcessTransaction(ctx, &batcher.TxnRequest{ - Tx: txn, - Tag: "test transaction", - Value: nil, - }) - <-ctx.Done() - assert.NoError(t, err) - res := <-txnManager.ReceiptChan() - assert.ErrorAs(t, res.Err, &batcher.ErrTransactionNotBroadcasted) - assert.Nil(t, res.Receipt) -} diff --git a/disperser/cmd/batcher/config.go b/disperser/cmd/batcher/config.go deleted file mode 100644 index a647901247..0000000000 --- a/disperser/cmd/batcher/config.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "github.com/Layr-Labs/eigenda/common" - "github.com/Layr-Labs/eigenda/common/aws" - "github.com/Layr-Labs/eigenda/common/geth" - "github.com/Layr-Labs/eigenda/core/thegraph" - "github.com/Layr-Labs/eigenda/disperser/batcher" - "github.com/Layr-Labs/eigenda/disperser/cmd/batcher/flags" - "github.com/Layr-Labs/eigenda/disperser/common/blobstore" - "github.com/Layr-Labs/eigenda/encoding/v1/kzg" - "github.com/Layr-Labs/eigenda/indexer" - "github.com/urfave/cli" -) - -type Config struct { - BatcherConfig batcher.Config - TimeoutConfig batcher.TimeoutConfig - BlobstoreConfig blobstore.Config - EthClientConfig geth.EthClientConfig - AwsClientConfig aws.ClientConfig - EncoderConfig kzg.KzgConfig - LoggerConfig common.LoggerConfig - MetricsConfig batcher.MetricsConfig - IndexerConfig indexer.Config - KMSKeyConfig common.KMSKeyConfig - ChainStateConfig thegraph.Config - UseGraph bool - - IndexerDataDir string - - OperatorStateRetrieverAddr string - EigenDAServiceManagerAddr string - EigenDADirectory string - - EnableGnarkBundleEncoding bool -} - -func NewConfig(ctx *cli.Context) (Config, error) { - loggerConfig, err := common.ReadLoggerCLIConfig(ctx, flags.FlagPrefix) - if err != nil { - return Config{}, err - } - ethClientConfig := geth.ReadEthClientConfig(ctx) - kmsConfig := common.ReadKMSKeyConfig(ctx, flags.FlagPrefix) - if !kmsConfig.Disable { - ethClientConfig = geth.ReadEthClientConfigRPCOnly(ctx) - } - - config := Config{ - BlobstoreConfig: blobstore.Config{ - BucketName: ctx.GlobalString(flags.S3BucketNameFlag.Name), - TableName: ctx.GlobalString(flags.DynamoDBTableNameFlag.Name), - Backend: blobstore.ObjectStorageBackend(ctx.GlobalString(flags.ObjectStorageBackendFlag.Name)), - OCIRegion: ctx.GlobalString(flags.OCIRegionFlag.Name), - OCICompartmentID: ctx.GlobalString(flags.OCICompartmentIDFlag.Name), - OCINamespace: ctx.GlobalString(flags.OCINamespaceFlag.Name), - }, - EthClientConfig: ethClientConfig, - AwsClientConfig: aws.ReadClientConfig(ctx, flags.FlagPrefix), - EncoderConfig: kzg.ReadCLIConfig(ctx), - LoggerConfig: *loggerConfig, - BatcherConfig: batcher.Config{ - PullInterval: ctx.GlobalDuration(flags.PullIntervalFlag.Name), - FinalizerInterval: ctx.GlobalDuration(flags.FinalizerIntervalFlag.Name), - FinalizerPoolSize: ctx.GlobalInt(flags.FinalizerPoolSizeFlag.Name), - EncoderSocket: ctx.GlobalString(flags.EncoderSocket.Name), - NumConnections: ctx.GlobalInt(flags.NumConnectionsFlag.Name), - EncodingRequestQueueSize: ctx.GlobalInt(flags.EncodingRequestQueueSizeFlag.Name), - BatchSizeMBLimit: ctx.GlobalUint(flags.BatchSizeLimitFlag.Name), - SRSOrder: ctx.GlobalInt(flags.SRSOrderFlag.Name), - MaxNumRetriesPerBlob: ctx.GlobalUint(flags.MaxNumRetriesPerBlobFlag.Name), - TargetNumChunks: ctx.GlobalUint64(flags.TargetNumChunksFlag.Name), - MaxBlobsToFetchFromStore: ctx.GlobalInt(flags.MaxBlobsToFetchFromStoreFlag.Name), - FinalizationBlockDelay: ctx.GlobalUint(flags.FinalizationBlockDelayFlag.Name), - }, - TimeoutConfig: batcher.TimeoutConfig{ - EncodingTimeout: ctx.GlobalDuration(flags.EncodingTimeoutFlag.Name), - AttestationTimeout: ctx.GlobalDuration(flags.AttestationTimeoutFlag.Name), - BatchAttestationTimeout: ctx.GlobalDuration(flags.BatchAttestationTimeoutFlag.Name), - ChainReadTimeout: ctx.GlobalDuration(flags.ChainReadTimeoutFlag.Name), - ChainWriteTimeout: ctx.GlobalDuration(flags.ChainWriteTimeoutFlag.Name), - ChainStateTimeout: ctx.GlobalDuration(flags.ChainStateTimeoutFlag.Name), - TxnBroadcastTimeout: ctx.GlobalDuration(flags.TransactionBroadcastTimeoutFlag.Name), - }, - MetricsConfig: batcher.MetricsConfig{ - HTTPPort: ctx.GlobalString(flags.MetricsHTTPPort.Name), - EnableMetrics: ctx.GlobalBool(flags.EnableMetrics.Name), - }, - ChainStateConfig: thegraph.ReadCLIConfig(ctx), - UseGraph: ctx.Bool(flags.UseGraphFlag.Name), - EigenDADirectory: ctx.GlobalString(flags.EigenDADirectoryFlag.Name), - OperatorStateRetrieverAddr: ctx.GlobalString(flags.OperatorStateRetrieverFlag.Name), - EigenDAServiceManagerAddr: ctx.GlobalString(flags.EigenDAServiceManagerFlag.Name), - IndexerDataDir: ctx.GlobalString(flags.IndexerDataDirFlag.Name), - IndexerConfig: indexer.ReadIndexerConfig(ctx), - KMSKeyConfig: kmsConfig, - EnableGnarkBundleEncoding: ctx.Bool(flags.EnableGnarkBundleEncodingFlag.Name), - } - return config, nil -} diff --git a/disperser/cmd/batcher/flags/flags.go b/disperser/cmd/batcher/flags/flags.go deleted file mode 100644 index 03ea31bb93..0000000000 --- a/disperser/cmd/batcher/flags/flags.go +++ /dev/null @@ -1,309 +0,0 @@ -package flags - -import ( - "time" - - "github.com/Layr-Labs/eigenda/common" - "github.com/Layr-Labs/eigenda/common/aws" - "github.com/Layr-Labs/eigenda/common/geth" - "github.com/Layr-Labs/eigenda/core/thegraph" - "github.com/Layr-Labs/eigenda/indexer" - "github.com/urfave/cli" -) - -const ( - FlagPrefix = "batcher" - envVarPrefix = "BATCHER" -) - -var ( - /* Required Flags */ - S3BucketNameFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "s3-bucket-name"), - Usage: "Name of the bucket to store blobs", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "S3_BUCKET_NAME"), - } - DynamoDBTableNameFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "dynamodb-table-name"), - Usage: "Name of the dynamodb table to store blob metadata", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "DYNAMODB_TABLE_NAME"), - } - ObjectStorageBackendFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "object-storage-backend"), - Usage: "Object storage backend to use (s3 or oci)", - Required: false, - Value: "s3", - EnvVar: common.PrefixEnvVar(envVarPrefix, "OBJECT_STORAGE_BACKEND"), - } - OCIRegionFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "oci-region"), - Usage: "OCI region (only used when object-storage-backend is oci)", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "OCI_REGION"), - } - OCICompartmentIDFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "oci-compartment-id"), - Usage: "OCI compartment ID (only used when object-storage-backend is oci)", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "OCI_COMPARTMENT_ID"), - } - OCINamespaceFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "oci-namespace"), - Usage: "OCI namespace (only used when object-storage-backend is oci)", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "OCI_NAMESPACE"), - } - PullIntervalFlag = cli.DurationFlag{ - Name: common.PrefixFlag(FlagPrefix, "pull-interval"), - Usage: "Interval at which to pull from the queue", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "PULL_INTERVAL"), - } - OperatorStateRetrieverFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "bls-operator-state-retriever"), - Usage: "[Deprecated: use EigenDADirectory instead] Address of the OperatorStateRetriever contract. " + - "Note that the contract no longer uses the BLS prefix.", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "BLS_OPERATOR_STATE_RETRIVER"), - } - EigenDAServiceManagerFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "eigenda-service-manager"), - Usage: "[Deprecated: use EigenDADirectory instead] Address of the EigenDA Service Manager", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "EIGENDA_SERVICE_MANAGER"), - } - EigenDADirectoryFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "eigenda-directory"), - Usage: "Address of the EigenDA Address Directory", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "EIGENDA_DIRECTORY"), - } - EncoderSocket = cli.StringFlag{ - Name: "encoder-socket", - Usage: "the http ip:port which the distributed encoder server is listening", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "ENCODER_ADDRESS"), - } - EnableMetrics = cli.BoolFlag{ - Name: common.PrefixFlag(FlagPrefix, "enable-metrics"), - Usage: "start metrics server", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "ENABLE_METRICS"), - } - UseGraphFlag = cli.BoolFlag{ - Name: common.PrefixFlag(FlagPrefix, "use-graph"), - Usage: "Whether to use the graph node", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "USE_GRAPH"), - } - BatchSizeLimitFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "batch-size-limit"), - Usage: "the maximum batch size in MiB", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "BATCH_SIZE_LIMIT"), - } - /* Optional Flags*/ - MetricsHTTPPort = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "metrics-http-port"), - Usage: "the http port which the metrics prometheus server is listening", - Required: false, - Value: "9100", - EnvVar: common.PrefixEnvVar(envVarPrefix, "METRICS_HTTP_PORT"), - } - IndexerDataDirFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "indexer-data-dir"), - Usage: "the data directory for the indexer", - EnvVar: common.PrefixEnvVar(envVarPrefix, "INDEXER_DATA_DIR"), - Value: "./data/", - } - EncodingTimeoutFlag = cli.DurationFlag{ - Name: "encoding-timeout", - Usage: "connection timeout from grpc call to encoder", - Required: false, - Value: 10 * time.Second, - EnvVar: common.PrefixEnvVar(envVarPrefix, "ENCODING_TIMEOUT"), - } - AttestationTimeoutFlag = cli.DurationFlag{ - Name: "attestation-timeout", - Usage: "connection timeout from grpc call to DA nodes for attestation", - Required: false, - Value: 20 * time.Second, - EnvVar: common.PrefixEnvVar(envVarPrefix, "ATTESTATION_TIMEOUT"), - } - BatchAttestationTimeoutFlag = cli.DurationFlag{ - Name: "batch-attestation-timeout", - Usage: "connection timeout from grpc call to DA nodes for batch attestation", - Required: false, - Value: 25 * time.Second, - EnvVar: common.PrefixEnvVar(envVarPrefix, "BATCH_ATTESTATION_TIMEOUT"), - } - ChainReadTimeoutFlag = cli.DurationFlag{ - Name: "chain-read-timeout", - Usage: "connection timeout to read from chain", - Required: false, - Value: 5 * time.Second, - EnvVar: common.PrefixEnvVar(envVarPrefix, "CHAIN_READ_TIMEOUT"), - } - ChainWriteTimeoutFlag = cli.DurationFlag{ - Name: "chain-write-timeout", - Usage: "connection timeout to write to chain", - Required: false, - Value: 90 * time.Second, - EnvVar: common.PrefixEnvVar(envVarPrefix, "CHAIN_WRITE_TIMEOUT"), - } - ChainStateTimeoutFlag = cli.DurationFlag{ - Name: "chain-state-timeout", - Usage: "connection timeout to read state from chain", - Required: false, - Value: 15 * time.Second, - EnvVar: common.PrefixEnvVar(envVarPrefix, "CHAIN_STATE_TIMEOUT"), - } - TransactionBroadcastTimeoutFlag = cli.DurationFlag{ - Name: "transaction-broadcast-timeout", - Usage: "timeout to broadcast transaction", - Required: false, - Value: 10 * time.Minute, - EnvVar: common.PrefixEnvVar(envVarPrefix, "TRANSACTION_BROADCAST_TIMEOUT"), - } - NumConnectionsFlag = cli.IntFlag{ - Name: "num-connections", - Usage: "maximum number of connections to encoders (defaults to 256)", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "NUM_CONNECTIONS"), - Value: 256, - } - FinalizerIntervalFlag = cli.DurationFlag{ - Name: common.PrefixFlag(FlagPrefix, "finalizer-interval"), - Usage: "Interval at which to check for finalized blobs", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "FINALIZER_INTERVAL"), - Value: 6 * time.Minute, - } - FinalizerPoolSizeFlag = cli.IntFlag{ - Name: common.PrefixFlag(FlagPrefix, "finalizer-pool-size"), - Usage: "Size of the finalizer workerpool", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "FINALIZER_POOL_SIZE"), - Value: 4, - } - EncodingRequestQueueSizeFlag = cli.IntFlag{ - Name: common.PrefixFlag(FlagPrefix, "encoding-request-queue-size"), - Usage: "Size of the encoding request queue", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "ENCODING_REQUEST_QUEUE_SIZE"), - Value: 500, - } - SRSOrderFlag = cli.IntFlag{ - Name: common.PrefixFlag(FlagPrefix, "srs-order"), - Usage: "Size of the encoding request queue", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "SRS_ORDER"), - } - MaxNumRetriesPerBlobFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "max-num-retries-per-blob"), - Usage: "Maximum number of retries to process a blob before marking the blob as FAILED", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "MAX_NUM_RETRIES_PER_BLOB"), - Value: 2, - } - // This flag is available so that we can manually adjust the number of chunks if desired for testing purposes or for other reasons. - // For instance, we may want to increase the number of chunks / reduce the chunk size to reduce the amount of data that needs to be - // downloaded by light clients for DAS. - TargetNumChunksFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "target-num-chunks"), - Usage: "Target number of chunks per blob. If set to zero, the number of chunks will be calculated based on the ratio of the total stake to the minimum stake", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "TARGET_NUM_CHUNKS"), - Value: 0, - } - MaxBlobsToFetchFromStoreFlag = cli.IntFlag{ - Name: common.PrefixFlag(FlagPrefix, "max-blobs-to-fetch-from-store"), - Usage: "Limit used to specify how many blobs to fetch from store at time when used with dynamodb pagination", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "MAX_BLOBS_TO_FETCH_FROM_STORE"), - Value: 100, - } - FinalizationBlockDelayFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "finalization-block-delay"), - Usage: "The block delay to use for pulling operator state in order to ensure the state is finalized", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "FINALIZATION_BLOCK_DELAY"), - Value: 75, - } - EnableGnarkBundleEncodingFlag = cli.BoolFlag{ - Name: common.PrefixFlag(FlagPrefix, "enable-gnark-bundle-encoding"), - Usage: "Enable Gnark bundle encoding for chunks", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "ENABLE_GNARK_BUNDLE_ENCODING"), - } - MaxNodeConnectionsFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "max-node-connections"), - Usage: "Maximum number of connections to the node. Only used when minibatching is enabled. Defaults to 1024.", - Required: false, - Value: 1024, - EnvVar: common.PrefixEnvVar(envVarPrefix, "MAX_NODE_CONNECTIONS"), - } - MaxNumRetriesPerDispersalFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "max-num-retries-per-dispersal"), - Usage: "Maximum number of retries to disperse a minibatch. Only used when minibatching is enabled. Defaults to 3.", - Required: false, - EnvVar: common.PrefixEnvVar(envVarPrefix, "MAX_NUM_RETRIES_PER_DISPERSAL"), - Value: 3, - } -) - -var requiredFlags = []cli.Flag{ - S3BucketNameFlag, - DynamoDBTableNameFlag, - PullIntervalFlag, - EncoderSocket, - EnableMetrics, - BatchSizeLimitFlag, - UseGraphFlag, - SRSOrderFlag, -} - -var optionalFlags = []cli.Flag{ - MetricsHTTPPort, - IndexerDataDirFlag, - EncodingTimeoutFlag, - AttestationTimeoutFlag, - BatchAttestationTimeoutFlag, - ChainReadTimeoutFlag, - ChainWriteTimeoutFlag, - ChainStateTimeoutFlag, - TransactionBroadcastTimeoutFlag, - NumConnectionsFlag, - FinalizerIntervalFlag, - FinalizerPoolSizeFlag, - EncodingRequestQueueSizeFlag, - MaxNumRetriesPerBlobFlag, - TargetNumChunksFlag, - MaxBlobsToFetchFromStoreFlag, - FinalizationBlockDelayFlag, - MaxNodeConnectionsFlag, - MaxNumRetriesPerDispersalFlag, - EnableGnarkBundleEncodingFlag, - OperatorStateRetrieverFlag, - EigenDAServiceManagerFlag, - EigenDADirectoryFlag, - ObjectStorageBackendFlag, - OCIRegionFlag, - OCICompartmentIDFlag, - OCINamespaceFlag, -} - -// Flags contains the list of configuration options available to the binary. -var Flags []cli.Flag - -func init() { - Flags = append(requiredFlags, optionalFlags...) - Flags = append(Flags, geth.EthClientFlags(envVarPrefix)...) - Flags = append(Flags, common.LoggerCLIFlags(envVarPrefix, FlagPrefix)...) - Flags = append(Flags, indexer.CLIFlags(envVarPrefix)...) - Flags = append(Flags, aws.ClientFlags(envVarPrefix, FlagPrefix)...) - Flags = append(Flags, thegraph.CLIFlags(envVarPrefix)...) - Flags = append(Flags, common.KMSWalletCLIFlags(envVarPrefix, FlagPrefix)...) -} diff --git a/disperser/cmd/batcher/main.go b/disperser/cmd/batcher/main.go deleted file mode 100644 index 9729850370..0000000000 --- a/disperser/cmd/batcher/main.go +++ /dev/null @@ -1,278 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - "os" - "time" - - "github.com/Layr-Labs/eigenda/common" - "github.com/Layr-Labs/eigenda/core/thegraph" - - "github.com/Layr-Labs/eigenda/common/aws/dynamodb" - "github.com/Layr-Labs/eigenda/common/geth" - "github.com/Layr-Labs/eigenda/core" - coreeth "github.com/Layr-Labs/eigenda/core/eth" - "github.com/Layr-Labs/eigenda/disperser/batcher" - dispatcher "github.com/Layr-Labs/eigenda/disperser/batcher/grpc" - "github.com/Layr-Labs/eigenda/disperser/cmd/batcher/flags" - "github.com/Layr-Labs/eigenda/disperser/common/blobstore" - "github.com/Layr-Labs/eigenda/disperser/encoder" - "github.com/Layr-Labs/eigensdk-go/aws/kms" - walletsdk "github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet" - "github.com/Layr-Labs/eigensdk-go/signerv2" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/urfave/cli" -) - -var ( - // version is the version of the binary. - version string - gitCommit string - gitDate string - // Note: Changing these paths will require updating the k8s deployment - readinessProbePath string = "/tmp/ready" - healthProbePath string = "/tmp/health" - maxStallDuration time.Duration = 240 * time.Second - handleBatchLivenessChan = make(chan time.Time, 1) -) - -func main() { - app := cli.NewApp() - app.Flags = flags.Flags - app.Version = fmt.Sprintf("%s-%s-%s", version, gitCommit, gitDate) - app.Name = "batcher" - app.Usage = "EigenDA Batcher" - app.Description = "Service for creating a batch from queued blobs, distributing coded chunks to nodes, and confirming onchain" - - app.Action = RunBatcher - err := app.Run(os.Args) - if err != nil { - log.Fatalf("application failed: %v", err) - } - - if _, err := os.Create(healthProbePath); err != nil { - log.Printf("Failed to create healthProbe file: %v", err) - } - - // Start HeartBeat Monitor - go heartbeatMonitor(healthProbePath, maxStallDuration) - - select {} -} - -func RunBatcher(ctx *cli.Context) error { - - // Clean up readiness file - if err := os.Remove(readinessProbePath); err != nil { - log.Printf("Failed to clean up readiness file: %v at path %v \n", err, readinessProbePath) - } - - config, err := NewConfig(ctx) - if err != nil { - return err - } - - logger, err := common.NewLogger(&config.LoggerConfig) - if err != nil { - return err - } - - bucketName := config.BlobstoreConfig.BucketName - s3Client, err := blobstore.CreateObjectStorageClient( - context.Background(), - config.BlobstoreConfig, - config.AwsClientConfig, - logger, - ) - if err != nil { - return err - } - logger.Info("Initialized S3 client", "bucket", bucketName) - - dynamoClient, err := dynamodb.NewClient(config.AwsClientConfig, logger) - if err != nil { - return err - } - - metrics := batcher.NewMetrics(config.MetricsConfig.HTTPPort, logger) - - logger.Debugf("Configured attestation timeout: %v, batch attestation timeout: %v", - config.TimeoutConfig.AttestationTimeout, config.TimeoutConfig.BatchAttestationTimeout) - - dispatcher := dispatcher.NewDispatcher(&dispatcher.Config{ - Timeout: config.TimeoutConfig.AttestationTimeout, - EnableGnarkBundleEncoding: config.EnableGnarkBundleEncoding, - }, logger, metrics.DispatcherMetrics) - asgn := &core.StdAssignmentCoordinator{} - - var wallet walletsdk.Wallet - var client *geth.MultiHomingClient - if !config.KMSKeyConfig.Disable { - if config.KMSKeyConfig.KeyID == "" || config.KMSKeyConfig.Region == "" { - return errors.New("KMS key ID and region must be specified unless KMS wallet is disabled") - } - kmsClient, err := kms.NewKMSClient(context.Background(), config.KMSKeyConfig.Region) - if err != nil { - return fmt.Errorf("failed to create KMS client: %w", err) - } - pubKey, err := kms.GetECDSAPublicKey(context.Background(), kmsClient, config.KMSKeyConfig.KeyID) - if err != nil { - return fmt.Errorf("failed to get public key from KMS: %w", err) - } - addr := crypto.PubkeyToAddress(*pubKey) - client, err = geth.NewMultiHomingClient(config.EthClientConfig, addr, logger) - if err != nil { - logger.Error("Cannot create chain.Client", "err", err) - return err - } - chainID, err := client.ChainID(context.Background()) - if err != nil { - return fmt.Errorf("failed to get chain ID: %w", err) - } - signer := signerv2.NewKMSSigner(context.Background(), kmsClient, pubKey, config.KMSKeyConfig.KeyID, chainID) - if err != nil { - return err - } - wallet, err = walletsdk.NewPrivateKeyWallet(client, signer, addr, logger) - if err != nil { - return err - } - logger.Info("Initialized KMS wallet", "address", addr.Hex()) - } else if len(config.EthClientConfig.PrivateKeyString) > 0 { - privateKey, err := crypto.HexToECDSA(config.EthClientConfig.PrivateKeyString) - if err != nil { - return fmt.Errorf("failed to parse private key: %w", err) - } - client, err = geth.NewMultiHomingClient(config.EthClientConfig, gethcommon.Address{}, logger) - if err != nil { - logger.Error("Cannot create chain.Client", "err", err) - return err - } - chainID, err := client.ChainID(context.Background()) - if err != nil { - return fmt.Errorf("failed to get chain ID: %w", err) - } - signerV2, address, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: privateKey}, chainID) - if err != nil { - return err - } - wallet, err = walletsdk.NewPrivateKeyWallet(client, signerV2, address, logger.With("component", "PrivateKeyWallet")) - if err != nil { - return err - } - logger.Info("Initialized PrivateKey wallet", "address", address.Hex()) - } else { - return errors.New("no wallet is configured. Either Fireblocks or PrivateKey wallet should be configured") - } - - if wallet == nil { - return errors.New("wallet is not configured") - } - if client == nil { - return errors.New("eth client is not configured") - } - - // used by non graph indexer - ethClient, err := geth.SafeDial(context.Background(), config.EthClientConfig.RPCURLs[0]) - if err != nil { - return err - } - rpcClient := ethClient.Client() - tx, err := coreeth.NewWriter(logger, client, config.OperatorStateRetrieverAddr, config.EigenDAServiceManagerAddr) - if err != nil { - return err - } - agg, err := core.NewStdSignatureAggregator(logger, tx) - if err != nil { - return err - } - blockStaleMeasure, err := tx.GetBlockStaleMeasure(context.Background()) - if err != nil { - return fmt.Errorf("failed to get BLOCK_STALE_MEASURE: %w", err) - } - storeDurationBlocks, err := tx.GetStoreDurationBlocks(context.Background()) - if err != nil || storeDurationBlocks == 0 { - return fmt.Errorf("failed to get STORE_DURATION_BLOCKS: %w", err) - } - blobMetadataStore := blobstore.NewBlobMetadataStore(dynamoClient, logger, config.BlobstoreConfig.TableName, time.Duration((storeDurationBlocks+blockStaleMeasure)*12)*time.Second) - queue := blobstore.NewSharedStorage(bucketName, s3Client, blobMetadataStore, logger) - - cs := coreeth.NewChainState(tx, client) - - var ics core.IndexedChainState - if config.UseGraph { - logger.Info("Using graph node") - - logger.Info("Connecting to subgraph", "url", config.ChainStateConfig.Endpoint) - ics = thegraph.MakeIndexedChainState(config.ChainStateConfig, cs, logger) - } else { - return fmt.Errorf("built-in indexer is deprecated and will be removed soon, please use UseGraph=true") - } - - if len(config.BatcherConfig.EncoderSocket) == 0 { - return errors.New("encoder socket must be specified") - } - encoderClient, err := encoder.NewEncoderClient(config.BatcherConfig.EncoderSocket, config.TimeoutConfig.EncodingTimeout) - if err != nil { - return err - } - finalizer := batcher.NewFinalizer(config.TimeoutConfig.ChainReadTimeout, config.BatcherConfig.FinalizerInterval, queue, client, rpcClient, config.BatcherConfig.MaxNumRetriesPerBlob, 1000, config.BatcherConfig.FinalizerPoolSize, logger, metrics.FinalizerMetrics) - txnManager := batcher.NewTxnManager(client, wallet, config.EthClientConfig.NumConfirmations, 20, config.TimeoutConfig.TxnBroadcastTimeout, config.TimeoutConfig.ChainWriteTimeout, logger, metrics.TxnManagerMetrics) - - // Enable Metrics Block - if config.MetricsConfig.EnableMetrics { - httpSocket := fmt.Sprintf(":%s", config.MetricsConfig.HTTPPort) - metrics.Start(context.Background()) - logger.Info("Enabled metrics for Batcher", "socket", httpSocket) - } - - batcher, err := batcher.NewBatcher(config.BatcherConfig, config.TimeoutConfig, queue, dispatcher, ics, asgn, encoderClient, agg, client, finalizer, tx, txnManager, logger, metrics, handleBatchLivenessChan) - if err != nil { - return err - } - err = batcher.Start(context.Background()) - if err != nil { - return err - } - - // Signal readiness - if _, err := os.Create(readinessProbePath); err != nil { - log.Printf("Failed to create readiness file: %v at path %v \n", err, readinessProbePath) - } - return nil -} - -// process liveness signal from handleBatch Go Routine -func heartbeatMonitor(filePath string, maxStallDuration time.Duration) { - var lastHeartbeat time.Time - stallTimer := time.NewTimer(maxStallDuration) - - for { - select { - // HeartBeat from Goroutine on Batcher Pull Interval - case heartbeat, ok := <-handleBatchLivenessChan: - if !ok { - log.Println("handleBatchLivenessChan closed, stopping health probe") - return - } - log.Printf("Received heartbeat from HandleBatch GoRoutine: %v\n", heartbeat) - lastHeartbeat = heartbeat - if err := os.WriteFile(filePath, []byte(lastHeartbeat.String()), 0666); err != nil { - log.Printf("Failed to update heartbeat file: %v", err) - } else { - log.Printf("Updated heartbeat file: %v with time %v\n", filePath, lastHeartbeat) - } - stallTimer.Reset(maxStallDuration) // Reset timer on new heartbeat - - case <-stallTimer.C: - // Instead of stopping the function, log a warning - log.Println("Warning: No heartbeat received within max stall duration.") - // Reset the timer to continue monitoring - stallTimer.Reset(maxStallDuration) - } - } -} diff --git a/disperser/cmd/dataapi/config.go b/disperser/cmd/dataapi/config.go index c84ecabf6d..50308aa312 100644 --- a/disperser/cmd/dataapi/config.go +++ b/disperser/cmd/dataapi/config.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/Layr-Labs/eigenda/common" "github.com/Layr-Labs/eigenda/common/aws" "github.com/Layr-Labs/eigenda/common/geth" @@ -15,7 +13,6 @@ import ( ) type Config struct { - ServerVersion uint AwsClientConfig aws.ClientConfig BlobstoreConfig blobstore.Config EthClientConfig geth.EthClientConfig @@ -36,17 +33,11 @@ type Config struct { OperatorStateRetrieverAddr string EigenDAServiceManagerAddr string - DisperserHostname string - ChurnerHostname string - BatcherHealthEndpt string + DisperserHostname string + ChurnerHostname string } func NewConfig(ctx *cli.Context) (Config, error) { - version := ctx.GlobalUint(flags.DataApiServerVersionFlag.Name) - if version != 1 && version != 2 { - return Config{}, fmt.Errorf("unknown server version %d, must be in [1, 2]", version) - } - loggerConfig, err := common.ReadLoggerCLIConfig(ctx, flags.FlagPrefix) if err != nil { return Config{}, err @@ -69,7 +60,6 @@ func NewConfig(ctx *cli.Context) (Config, error) { EigenDAServiceManagerAddr: ctx.GlobalString(flags.EigenDAServiceManagerFlag.Name), EigenDADirectory: ctx.GlobalString(flags.EigenDADirectoryFlag.Name), ServerMode: ctx.GlobalString(flags.ServerModeFlag.Name), - ServerVersion: version, PrometheusConfig: prometheus.Config{ ServerURL: ctx.GlobalString(flags.PrometheusServerURLFlag.Name), Username: ctx.GlobalString(flags.PrometheusServerUsernameFlag.Name), @@ -82,10 +72,9 @@ func NewConfig(ctx *cli.Context) (Config, error) { HTTPPort: ctx.GlobalString(flags.MetricsHTTPPort.Name), EnableMetrics: ctx.GlobalBool(flags.EnableMetricsFlag.Name), }, - DisperserHostname: ctx.GlobalString(flags.DisperserHostnameFlag.Name), - ChurnerHostname: ctx.GlobalString(flags.ChurnerHostnameFlag.Name), - BatcherHealthEndpt: ctx.GlobalString(flags.BatcherHealthEndptFlag.Name), - ChainStateConfig: thegraph.ReadCLIConfig(ctx), + DisperserHostname: ctx.GlobalString(flags.DisperserHostnameFlag.Name), + ChurnerHostname: ctx.GlobalString(flags.ChurnerHostnameFlag.Name), + ChainStateConfig: thegraph.ReadCLIConfig(ctx), } return config, nil } diff --git a/disperser/cmd/dataapi/flags/flags.go b/disperser/cmd/dataapi/flags/flags.go index 112b3ab84b..c60eb0a22f 100644 --- a/disperser/cmd/dataapi/flags/flags.go +++ b/disperser/cmd/dataapi/flags/flags.go @@ -132,12 +132,6 @@ var ( Required: true, EnvVar: common.PrefixEnvVar(envVarPrefix, "EIGENDA_CHURNER_HOSTNAME"), } - BatcherHealthEndptFlag = cli.StringFlag{ - Name: common.PrefixFlag(FlagPrefix, "eigenda-batcher-health-endpoint"), - Usage: "Endpt of EigenDA Batcher Health Sidecar", - Required: true, - EnvVar: common.PrefixEnvVar(envVarPrefix, "EIGENDA_BATCHER_HEALTH_ENDPOINT"), - } /* Optional Flags*/ MetricsHTTPPort = cli.StringFlag{ Name: common.PrefixFlag(FlagPrefix, "metrics-http-port"), @@ -146,13 +140,6 @@ var ( Value: "9100", EnvVar: common.PrefixEnvVar(envVarPrefix, "METRICS_HTTP_PORT"), } - DataApiServerVersionFlag = cli.UintFlag{ - Name: common.PrefixFlag(FlagPrefix, "dataapi-version"), - Usage: "DataApi server version. Options are 1 and 2.", - Required: false, - Value: 1, - EnvVar: common.PrefixEnvVar(envVarPrefix, "DATA_API_VERSION"), - } ) var requiredFlags = []cli.Flag{ @@ -170,13 +157,11 @@ var requiredFlags = []cli.Flag{ EnableMetricsFlag, DisperserHostnameFlag, ChurnerHostnameFlag, - BatcherHealthEndptFlag, } var optionalFlags = []cli.Flag{ ServerModeFlag, MetricsHTTPPort, - DataApiServerVersionFlag, EigenDADirectoryFlag, OperatorStateRetrieverFlag, EigenDAServiceManagerFlag, diff --git a/disperser/cmd/dataapi/main.go b/disperser/cmd/dataapi/main.go index 2d0e16cc56..b01ca55a8d 100644 --- a/disperser/cmd/dataapi/main.go +++ b/disperser/cmd/dataapi/main.go @@ -11,11 +11,9 @@ import ( "github.com/Layr-Labs/eigenda/common" "github.com/Layr-Labs/eigenda/common/aws/dynamodb" "github.com/Layr-Labs/eigenda/common/geth" - commonaws "github.com/Layr-Labs/eigenda/common/s3/aws" coreeth "github.com/Layr-Labs/eigenda/core/eth" "github.com/Layr-Labs/eigenda/core/thegraph" "github.com/Layr-Labs/eigenda/disperser/cmd/dataapi/flags" - "github.com/Layr-Labs/eigenda/disperser/common/blobstore" blobstorev2 "github.com/Layr-Labs/eigenda/disperser/common/v2/blobstore" "github.com/Layr-Labs/eigenda/disperser/dataapi" dataapiprometheus "github.com/Layr-Labs/eigenda/disperser/dataapi/prometheus" @@ -67,20 +65,6 @@ func RunDataApi(ctx *cli.Context) error { return err } - s3Client, err := commonaws.NewAwsS3Client( - context.Background(), - logger, - config.AwsClientConfig.EndpointURL, - config.AwsClientConfig.Region, - config.AwsClientConfig.FragmentParallelismFactor, - config.AwsClientConfig.FragmentParallelismConstant, - config.AwsClientConfig.AccessKey, - config.AwsClientConfig.SecretAccessKey, - ) - if err != nil { - return err - } - dynamoClient, err := dynamodb.NewClient(config.AwsClientConfig, logger) if err != nil { return err @@ -110,65 +94,28 @@ func RunDataApi(ctx *cli.Context) error { indexedChainState = thegraph.MakeIndexedChainState(config.ChainStateConfig, chainState, logger) ) - if config.ServerVersion == 2 { - baseBlobMetadataStorev2 := blobstorev2.NewBlobMetadataStore(dynamoClient, logger, config.BlobstoreConfig.TableName) - blobMetadataStorev2 := blobstorev2.NewInstrumentedMetadataStore(baseBlobMetadataStorev2, blobstorev2.InstrumentedMetadataStoreConfig{ + baseBlobMetadataStorev2 := blobstorev2.NewBlobMetadataStore(dynamoClient, logger, config.BlobstoreConfig.TableName) + blobMetadataStorev2 := blobstorev2.NewInstrumentedMetadataStore( + baseBlobMetadataStorev2, blobstorev2.InstrumentedMetadataStoreConfig{ ServiceName: "dataapi", Registry: reg, Backend: blobstorev2.BackendDynamoDB, }) - // Register reservation collector - reservationCollector := serverv2.NewReservationExpirationCollector(subgraphClient, logger) - reg.MustRegister(reservationCollector) - - metrics := dataapi.NewMetrics(config.ServerVersion, reg, blobMetadataStorev2, config.MetricsConfig.HTTPPort, logger) - serverv2, err := serverv2.NewServerV2( - dataapi.Config{ - ServerMode: config.ServerMode, - SocketAddr: config.SocketAddr, - AllowOrigins: config.AllowOrigins, - DisperserHostname: config.DisperserHostname, - ChurnerHostname: config.ChurnerHostname, - BatcherHealthEndpt: config.BatcherHealthEndpt, - }, - blobMetadataStorev2, - promClient, - subgraphClient, - tx, - chainState, - indexedChainState, - logger, - metrics, - ) - if err != nil { - return fmt.Errorf("failed to create v2 server: %w", err) - } - - // Enable Metrics Block - if config.MetricsConfig.EnableMetrics { - httpSocket := fmt.Sprintf(":%s", config.MetricsConfig.HTTPPort) - metrics.Start(context.Background()) - logger.Info("Enabled metrics for Data Access API", "socket", httpSocket) - } - - return runServer(serverv2, logger) - } - - blobMetadataStore := blobstore.NewBlobMetadataStore(dynamoClient, logger, config.BlobstoreConfig.TableName, 0) - sharedStorage := blobstore.NewSharedStorage(config.BlobstoreConfig.BucketName, s3Client, blobMetadataStore, logger) - metrics := dataapi.NewMetrics(config.ServerVersion, reg, blobMetadataStore, config.MetricsConfig.HTTPPort, logger) + // Register reservation collector + reservationCollector := serverv2.NewReservationExpirationCollector(subgraphClient, logger) + reg.MustRegister(reservationCollector) - server, err := dataapi.NewServer( + metrics := dataapi.NewMetrics(reg, blobMetadataStorev2, config.MetricsConfig.HTTPPort, logger) + server, err := serverv2.NewServerV2( dataapi.Config{ - ServerMode: config.ServerMode, - SocketAddr: config.SocketAddr, - AllowOrigins: config.AllowOrigins, - DisperserHostname: config.DisperserHostname, - ChurnerHostname: config.ChurnerHostname, - BatcherHealthEndpt: config.BatcherHealthEndpt, + ServerMode: config.ServerMode, + SocketAddr: config.SocketAddr, + AllowOrigins: config.AllowOrigins, + DisperserHostname: config.DisperserHostname, + ChurnerHostname: config.ChurnerHostname, }, - sharedStorage, + blobMetadataStorev2, promClient, subgraphClient, tx, @@ -176,12 +123,9 @@ func RunDataApi(ctx *cli.Context) error { indexedChainState, logger, metrics, - nil, - nil, - nil, ) if err != nil { - return fmt.Errorf("failed to create v1 server: %w", err) + return fmt.Errorf("failed to create server: %w", err) } // Enable Metrics Block diff --git a/disperser/common/blobstore/blob_metadata_store.go b/disperser/common/blobstore/blob_metadata_store.go deleted file mode 100644 index 3388d81082..0000000000 --- a/disperser/common/blobstore/blob_metadata_store.go +++ /dev/null @@ -1,627 +0,0 @@ -package blobstore - -import ( - "context" - "fmt" - "strconv" - "time" - - commondynamodb "github.com/Layr-Labs/eigenda/common/aws/dynamodb" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/common" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/ethereum/go-ethereum/common/hexutil" -) - -const ( - statusIndexName = "StatusIndex" - batchIndexName = "BatchIndex" - expiryIndexName = "Status-Expiry-Index" -) - -// BlobMetadataStore is a blob metadata storage backed by DynamoDB -// The blob metadata is stored in a single table and replicated in several indexes. -// - Metadata: (Partition Key: BlobKey, Sort Key: MetadataHash) -> Metadata -// - Indexes -// - StatusIndex: (Partition Key: Status, Sort Key: RequestedAt) -> Metadata -// - BatchIndex: (Partition Key: BatchHeaderHash, Sort Key: BlobIndex) -> Metadata -type BlobMetadataStore struct { - dynamoDBClient commondynamodb.Client - logger logging.Logger - tableName string - ttl time.Duration -} - -func NewBlobMetadataStore(dynamoDBClient commondynamodb.Client, logger logging.Logger, tableName string, ttl time.Duration) *BlobMetadataStore { - logger.Debugf("creating blob metadata store with table %s with TTL: %s", tableName, ttl) - return &BlobMetadataStore{ - dynamoDBClient: dynamoDBClient, - logger: logger.With("component", "BlobMetadataStore"), - tableName: tableName, - ttl: ttl, - } -} - -func (s *BlobMetadataStore) QueueNewBlobMetadata(ctx context.Context, blobMetadata *disperser.BlobMetadata) error { - item, err := MarshalBlobMetadata(blobMetadata) - if err != nil { - return err - } - - return s.dynamoDBClient.PutItem(ctx, s.tableName, item) -} - -func (s *BlobMetadataStore) GetBlobMetadata(ctx context.Context, blobKey disperser.BlobKey) (*disperser.BlobMetadata, error) { - item, err := s.dynamoDBClient.GetItem(ctx, s.tableName, map[string]types.AttributeValue{ - "BlobHash": &types.AttributeValueMemberS{ - Value: blobKey.BlobHash, - }, - "MetadataHash": &types.AttributeValueMemberS{ - Value: blobKey.MetadataHash, - }, - }) - - if item == nil { - return nil, fmt.Errorf("%w: metadata not found for key %s", common.ErrMetadataNotFound, blobKey) - } - - if err != nil { - return nil, err - } - - metadata, err := UnmarshalBlobMetadata(item) - if err != nil { - return nil, err - } - - return metadata, nil -} - -// GetBulkBlobMetadata returns the metadata for the given blob keys -// Note: ordering of items is not guaranteed -func (s *BlobMetadataStore) GetBulkBlobMetadata(ctx context.Context, blobKeys []disperser.BlobKey) ([]*disperser.BlobMetadata, error) { - keys := make([]map[string]types.AttributeValue, len(blobKeys)) - for i := 0; i < len(blobKeys); i += 1 { - keys[i] = map[string]types.AttributeValue{ - "BlobHash": &types.AttributeValueMemberS{Value: blobKeys[i].BlobHash}, - "MetadataHash": &types.AttributeValueMemberS{Value: blobKeys[i].MetadataHash}, - } - } - items, err := s.dynamoDBClient.GetItems(ctx, s.tableName, keys, false) - if err != nil { - return nil, err - } - - metadata := make([]*disperser.BlobMetadata, len(items)) - for i, item := range items { - metadata[i], err = UnmarshalBlobMetadata(item) - if err != nil { - return nil, err - } - } - - return metadata, nil -} - -// GetBlobMetadataByStatus returns all the metadata with the given status -// Because this function scans the entire index, it should only be used for status with a limited number of items. -// It should only be used to filter "Processing" status. To support other status, a streaming version should be implemented. -func (s *BlobMetadataStore) GetBlobMetadataByStatus(ctx context.Context, status disperser.BlobStatus) ([]*disperser.BlobMetadata, error) { - items, err := s.dynamoDBClient.QueryIndex(ctx, s.tableName, expiryIndexName, "BlobStatus = :status AND Expiry > :expiry", commondynamodb.ExpressionValues{ - ":status": &types.AttributeValueMemberN{ - Value: strconv.Itoa(int(status)), - }, - ":expiry": &types.AttributeValueMemberN{ - Value: strconv.FormatInt(time.Now().Unix(), 10), - }}) - if err != nil { - return nil, err - } - - metadata := make([]*disperser.BlobMetadata, len(items)) - for i, item := range items { - metadata[i], err = UnmarshalBlobMetadata(item) - if err != nil { - return nil, err - } - } - - return metadata, nil -} - -// GetBlobMetadataCountByStatus returns the count of all the metadata with the given status -// Because this function scans the entire index, it should only be used for status with a limited number of items. -// It should only be used to filter "Processing" status. To support other status, a streaming version should be implemented. -func (s *BlobMetadataStore) GetBlobMetadataCountByStatus(ctx context.Context, status disperser.BlobStatus) (int32, error) { - count, err := s.dynamoDBClient.QueryIndexCount(ctx, s.tableName, expiryIndexName, "BlobStatus = :status AND Expiry > :expiry", commondynamodb.ExpressionValues{ - ":status": &types.AttributeValueMemberN{ - Value: strconv.Itoa(int(status)), - }, - ":expiry": &types.AttributeValueMemberN{ - Value: strconv.FormatInt(time.Now().Unix(), 10), - }, - }) - if err != nil { - return 0, err - } - - return count, nil -} - -// GetBlobMetadataByStatusWithPagination returns all the metadata with the given status upto the specified limit -// along with items, also returns a pagination token that can be used to fetch the next set of items -// -// Note that this may not return all the metadata for the batch if dynamodb query limit is reached. -// e.g 1mb limit for a single query -func (s *BlobMetadataStore) GetBlobMetadataByStatusWithPagination(ctx context.Context, status disperser.BlobStatus, limit int32, exclusiveStartKey *disperser.BlobStoreExclusiveStartKey) ([]*disperser.BlobMetadata, *disperser.BlobStoreExclusiveStartKey, error) { - - var attributeMap map[string]types.AttributeValue - var err error - - // Convert the exclusive start key to a map of AttributeValue - if exclusiveStartKey != nil { - attributeMap, err = convertToAttribMap(exclusiveStartKey) - if err != nil { - return nil, nil, err - } - } - - queryResult, err := s.dynamoDBClient.QueryIndexWithPagination(ctx, s.tableName, expiryIndexName, "BlobStatus = :status AND Expiry > :expiry", commondynamodb.ExpressionValues{ - ":status": &types.AttributeValueMemberN{ - Value: strconv.Itoa(int(status)), - }, - ":expiry": &types.AttributeValueMemberN{ - Value: strconv.FormatInt(time.Now().Unix(), 10), - }, - }, limit, attributeMap, true) - - if err != nil { - return nil, nil, err - } - - // When no more results to fetch, the LastEvaluatedKey is nil - if queryResult.Items == nil && queryResult.LastEvaluatedKey == nil { - return nil, nil, nil - } - - metadata := make([]*disperser.BlobMetadata, len(queryResult.Items)) - for i, item := range queryResult.Items { - metadata[i], err = UnmarshalBlobMetadata(item) - if err != nil { - return nil, nil, err - } - } - - lastEvaluatedKey := queryResult.LastEvaluatedKey - if lastEvaluatedKey == nil { - return metadata, nil, nil - } - - // Convert the last evaluated key to a disperser.BlobStoreExclusiveStartKey - exclusiveStartKey, err = convertToExclusiveStartKey(lastEvaluatedKey) - if err != nil { - return nil, nil, err - } - return metadata, exclusiveStartKey, nil -} - -func (s *BlobMetadataStore) GetAllBlobMetadataByBatch(ctx context.Context, batchHeaderHash [32]byte) ([]*disperser.BlobMetadata, error) { - items, err := s.dynamoDBClient.QueryIndex(ctx, s.tableName, batchIndexName, "BatchHeaderHash = :batch_header_hash", commondynamodb.ExpressionValues{ - ":batch_header_hash": &types.AttributeValueMemberB{ - Value: batchHeaderHash[:], - }, - }) - if err != nil { - return nil, err - } - - if len(items) == 0 { - return nil, fmt.Errorf("there is no metadata for batch %x", batchHeaderHash) - } - - metadatas := make([]*disperser.BlobMetadata, len(items)) - for i, item := range items { - metadatas[i], err = UnmarshalBlobMetadata(item) - if err != nil { - return nil, err - } - } - - return metadatas, nil -} - -// GetBlobMetadataByStatusWithPagination returns all the metadata with the given status upto the specified limit -// along with items, also returns a pagination token that can be used to fetch the next set of items -// -// Note that this may not return all the metadata for the batch if dynamodb query limit is reached. -// e.g 1mb limit for a single query -func (s *BlobMetadataStore) GetAllBlobMetadataByBatchWithPagination( - ctx context.Context, - batchHeaderHash [32]byte, - limit int32, - exclusiveStartKey *disperser.BatchIndexExclusiveStartKey, -) ([]*disperser.BlobMetadata, *disperser.BatchIndexExclusiveStartKey, error) { - var attributeMap map[string]types.AttributeValue - var err error - - // Convert the exclusive start key to a map of AttributeValue - if exclusiveStartKey != nil { - attributeMap, err = convertToAttribMapBatchIndex(exclusiveStartKey) - if err != nil { - return nil, nil, err - } - } - - queryResult, err := s.dynamoDBClient.QueryIndexWithPagination( - ctx, - s.tableName, - batchIndexName, - "BatchHeaderHash = :batch_header_hash", - commondynamodb.ExpressionValues{ - ":batch_header_hash": &types.AttributeValueMemberB{ - Value: batchHeaderHash[:], - }, - }, - limit, - attributeMap, - true, - ) - if err != nil { - return nil, nil, err - } - - s.logger.Info("Query result", "items", len(queryResult.Items), "lastEvaluatedKey", queryResult.LastEvaluatedKey) - // When no more results to fetch, the LastEvaluatedKey is nil - if queryResult.Items == nil && queryResult.LastEvaluatedKey == nil { - return nil, nil, nil - } - - metadata := make([]*disperser.BlobMetadata, len(queryResult.Items)) - for i, item := range queryResult.Items { - metadata[i], err = UnmarshalBlobMetadata(item) - if err != nil { - return nil, nil, err - } - } - - lastEvaluatedKey := queryResult.LastEvaluatedKey - if lastEvaluatedKey == nil { - return metadata, nil, nil - } - - // Convert the last evaluated key to a disperser.BatchIndexExclusiveStartKey - exclusiveStartKey, err = convertToExclusiveStartKeyBatchIndex(lastEvaluatedKey) - if err != nil { - return nil, nil, err - } - return metadata, exclusiveStartKey, nil -} - -func (s *BlobMetadataStore) GetBlobMetadataInBatch(ctx context.Context, batchHeaderHash [32]byte, blobIndex uint32) (*disperser.BlobMetadata, error) { - items, err := s.dynamoDBClient.QueryIndex(ctx, s.tableName, batchIndexName, "BatchHeaderHash = :batch_header_hash AND BlobIndex = :blob_index", commondynamodb.ExpressionValues{ - ":batch_header_hash": &types.AttributeValueMemberB{ - Value: batchHeaderHash[:], - }, - ":blob_index": &types.AttributeValueMemberN{ - Value: strconv.Itoa(int(blobIndex)), - }}) - if err != nil { - return nil, err - } - - if len(items) == 0 { - return nil, fmt.Errorf("%w: there is no metadata for batch %s and blob index %d", common.ErrMetadataNotFound, hexutil.Encode(batchHeaderHash[:]), blobIndex) - } - - if len(items) > 1 { - s.logger.Error("there are multiple metadata for batch %s and blob index %d", hexutil.Encode(batchHeaderHash[:]), blobIndex) - } - - metadata, err := UnmarshalBlobMetadata(items[0]) - if err != nil { - return nil, err - } - return metadata, nil -} - -func (s *BlobMetadataStore) IncrementNumRetries(ctx context.Context, existingMetadata *disperser.BlobMetadata) error { - _, err := s.dynamoDBClient.UpdateItem(ctx, s.tableName, map[string]types.AttributeValue{ - "BlobHash": &types.AttributeValueMemberS{ - Value: existingMetadata.BlobHash, - }, - "MetadataHash": &types.AttributeValueMemberS{ - Value: existingMetadata.MetadataHash, - }, - }, commondynamodb.Item{ - "NumRetries": &types.AttributeValueMemberN{ - Value: strconv.Itoa(int(existingMetadata.NumRetries + 1)), - }, - }) - - return err -} - -func (s *BlobMetadataStore) UpdateConfirmationBlockNumber(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationBlockNumber uint32) error { - updated := *existingMetadata - if updated.ConfirmationInfo == nil { - return fmt.Errorf("failed to update confirmation block number because confirmation info is missing for blob key %s", existingMetadata.GetBlobKey().String()) - } - - updated.ConfirmationInfo.ConfirmationBlockNumber = confirmationBlockNumber - item, err := MarshalBlobMetadata(&updated) - if err != nil { - return err - } - - _, err = s.dynamoDBClient.UpdateItem(ctx, s.tableName, map[string]types.AttributeValue{ - "BlobHash": &types.AttributeValueMemberS{ - Value: existingMetadata.BlobHash, - }, - "MetadataHash": &types.AttributeValueMemberS{ - Value: existingMetadata.MetadataHash, - }, - }, item) - - return err -} - -func (s *BlobMetadataStore) UpdateBlobMetadata(ctx context.Context, metadataKey disperser.BlobKey, updated *disperser.BlobMetadata) error { - item, err := MarshalBlobMetadata(updated) - if err != nil { - return err - } - - _, err = s.dynamoDBClient.UpdateItem(ctx, s.tableName, map[string]types.AttributeValue{ - "BlobHash": &types.AttributeValueMemberS{ - Value: metadataKey.BlobHash, - }, - "MetadataHash": &types.AttributeValueMemberS{ - Value: metadataKey.MetadataHash, - }, - }, item) - - return err -} - -func (s *BlobMetadataStore) SetBlobStatus(ctx context.Context, metadataKey disperser.BlobKey, status disperser.BlobStatus) error { - _, err := s.dynamoDBClient.UpdateItem(ctx, s.tableName, map[string]types.AttributeValue{ - "BlobHash": &types.AttributeValueMemberS{ - Value: metadataKey.BlobHash, - }, - "MetadataHash": &types.AttributeValueMemberS{ - Value: metadataKey.MetadataHash, - }, - }, commondynamodb.Item{ - "BlobStatus": &types.AttributeValueMemberN{ - Value: strconv.Itoa(int(status)), - }, - }) - - return err -} - -func GenerateTableSchema(metadataTableName string, readCapacityUnits int64, writeCapacityUnits int64) *dynamodb.CreateTableInput { - return &dynamodb.CreateTableInput{ - AttributeDefinitions: []types.AttributeDefinition{ - { - AttributeName: aws.String("BlobHash"), - AttributeType: types.ScalarAttributeTypeS, - }, - { - AttributeName: aws.String("MetadataHash"), - AttributeType: types.ScalarAttributeTypeS, - }, - { - AttributeName: aws.String("BlobStatus"), - AttributeType: types.ScalarAttributeTypeN, - }, - { - AttributeName: aws.String("RequestedAt"), - AttributeType: types.ScalarAttributeTypeN, - }, - { - AttributeName: aws.String("BatchHeaderHash"), - AttributeType: types.ScalarAttributeTypeB, - }, - { - AttributeName: aws.String("BlobIndex"), - AttributeType: types.ScalarAttributeTypeN, - }, - { - AttributeName: aws.String("Expiry"), - AttributeType: types.ScalarAttributeTypeN, - }, - }, - KeySchema: []types.KeySchemaElement{ - { - AttributeName: aws.String("BlobHash"), - KeyType: types.KeyTypeHash, - }, - { - AttributeName: aws.String("MetadataHash"), - KeyType: types.KeyTypeRange, - }, - }, - TableName: aws.String(metadataTableName), - GlobalSecondaryIndexes: []types.GlobalSecondaryIndex{ - { - IndexName: aws.String(statusIndexName), - KeySchema: []types.KeySchemaElement{ - { - AttributeName: aws.String("BlobStatus"), - KeyType: types.KeyTypeHash, - }, - { - AttributeName: aws.String("RequestedAt"), - KeyType: types.KeyTypeRange, - }, - }, - Projection: &types.Projection{ - ProjectionType: types.ProjectionTypeAll, - }, - ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: aws.Int64(readCapacityUnits), - WriteCapacityUnits: aws.Int64(writeCapacityUnits), - }, - }, - { - IndexName: aws.String(batchIndexName), - KeySchema: []types.KeySchemaElement{ - { - AttributeName: aws.String("BatchHeaderHash"), - KeyType: types.KeyTypeHash, - }, - { - AttributeName: aws.String("BlobIndex"), - KeyType: types.KeyTypeRange, - }, - }, - Projection: &types.Projection{ - ProjectionType: types.ProjectionTypeAll, - }, - ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: aws.Int64(readCapacityUnits), - WriteCapacityUnits: aws.Int64(writeCapacityUnits), - }, - }, - { - IndexName: aws.String(expiryIndexName), - KeySchema: []types.KeySchemaElement{ - { - AttributeName: aws.String("BlobStatus"), - KeyType: types.KeyTypeHash, - }, - { - AttributeName: aws.String("Expiry"), - KeyType: types.KeyTypeRange, - }, - }, - Projection: &types.Projection{ - ProjectionType: types.ProjectionTypeAll, - }, - ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: aws.Int64(readCapacityUnits), - WriteCapacityUnits: aws.Int64(writeCapacityUnits), - }, - }, - }, - ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: aws.Int64(readCapacityUnits), - WriteCapacityUnits: aws.Int64(writeCapacityUnits), - }, - } -} - -func MarshalBlobMetadata(metadata *disperser.BlobMetadata) (commondynamodb.Item, error) { - basicFields, err := attributevalue.MarshalMap(metadata) - if err != nil { - return nil, err - } - - if metadata.RequestMetadata == nil { - return basicFields, nil - } - - requestMetadata, err := attributevalue.MarshalMap(metadata.RequestMetadata) - if err != nil { - return nil, err - } - - // Flatten the request metadata - for k, v := range requestMetadata { - basicFields[k] = v - } - - if metadata.ConfirmationInfo == nil { - return basicFields, nil - } - - confirmationInfo, err := attributevalue.MarshalMap(metadata.ConfirmationInfo) - if err != nil { - return nil, err - } - - // Flatten the confirmation info - for k, v := range confirmationInfo { - basicFields[k] = v - } - - return basicFields, nil -} - -func UnmarshalBlobMetadata(item commondynamodb.Item) (*disperser.BlobMetadata, error) { - metadata := disperser.BlobMetadata{} - err := attributevalue.UnmarshalMap(item, &metadata) - if err != nil { - return nil, err - } - - requestMetadata := disperser.RequestMetadata{} - err = attributevalue.UnmarshalMap(item, &requestMetadata) - if err != nil { - return nil, err - } - metadata.RequestMetadata = &requestMetadata - if metadata.BlobStatus != disperser.Confirmed && metadata.BlobStatus != disperser.Finalized { - return &metadata, nil - } - - confirmationInfo := disperser.ConfirmationInfo{} - err = attributevalue.UnmarshalMap(item, &confirmationInfo) - if err != nil { - return nil, err - } - metadata.ConfirmationInfo = &confirmationInfo - - return &metadata, nil -} - -func convertToExclusiveStartKey(exclusiveStartKeyMap map[string]types.AttributeValue) (*disperser.BlobStoreExclusiveStartKey, error) { - blobStoreExclusiveStartKey := disperser.BlobStoreExclusiveStartKey{} - err := attributevalue.UnmarshalMap(exclusiveStartKeyMap, &blobStoreExclusiveStartKey) - if err != nil { - return nil, err - } - - return &blobStoreExclusiveStartKey, nil -} - -func convertToExclusiveStartKeyBatchIndex(exclusiveStartKeyMap map[string]types.AttributeValue) (*disperser.BatchIndexExclusiveStartKey, error) { - blobStoreExclusiveStartKey := disperser.BatchIndexExclusiveStartKey{} - err := attributevalue.UnmarshalMap(exclusiveStartKeyMap, &blobStoreExclusiveStartKey) - if err != nil { - return nil, err - } - - return &blobStoreExclusiveStartKey, nil -} - -func convertToAttribMap(blobStoreExclusiveStartKey *disperser.BlobStoreExclusiveStartKey) (map[string]types.AttributeValue, error) { - if blobStoreExclusiveStartKey == nil { - // Return an empty map or nil - return nil, nil - } - - avMap, err := attributevalue.MarshalMap(blobStoreExclusiveStartKey) - if err != nil { - return nil, err - } - return avMap, nil -} - -func convertToAttribMapBatchIndex(blobStoreExclusiveStartKey *disperser.BatchIndexExclusiveStartKey) (map[string]types.AttributeValue, error) { - if blobStoreExclusiveStartKey == nil { - // Return an empty map or nil - return nil, nil - } - - avMap, err := attributevalue.MarshalMap(blobStoreExclusiveStartKey) - if err != nil { - return nil, err - } - return avMap, nil -} diff --git a/disperser/common/blobstore/blob_metadata_store_test.go b/disperser/common/blobstore/blob_metadata_store_test.go deleted file mode 100644 index b2de1696e2..0000000000 --- a/disperser/common/blobstore/blob_metadata_store_test.go +++ /dev/null @@ -1,397 +0,0 @@ -package blobstore_test - -import ( - "testing" - "time" - - commondynamodb "github.com/Layr-Labs/eigenda/common/aws/dynamodb" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/consensys/gnark-crypto/ecc/bn254/fp" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" -) - -func TestBlobMetadataStoreOperations(t *testing.T) { - ctx := t.Context() - - blobKey1 := disperser.BlobKey{ - BlobHash: blobHash, - MetadataHash: "hash", - } - now := time.Now() - metadata1 := &disperser.BlobMetadata{ - MetadataHash: blobKey1.MetadataHash, - BlobHash: blobHash, - BlobStatus: disperser.Processing, - Expiry: uint64(now.Add(time.Hour).Unix()), - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: uint64(now.Unix()), - }, - } - blobKey2 := disperser.BlobKey{ - BlobHash: "blob2", - MetadataHash: "hash2", - } - metadata2 := &disperser.BlobMetadata{ - MetadataHash: blobKey2.MetadataHash, - BlobHash: blobKey2.BlobHash, - BlobStatus: disperser.Finalized, - Expiry: uint64(now.Add(time.Hour).Unix()), - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: uint64(now.Unix()), - }, - ConfirmationInfo: &disperser.ConfirmationInfo{}, - } - err := blobMetadataStore.QueueNewBlobMetadata(ctx, metadata1) - assert.NoError(t, err) - err = blobMetadataStore.QueueNewBlobMetadata(ctx, metadata2) - assert.NoError(t, err) - - fetchedMetadata, err := blobMetadataStore.GetBlobMetadata(ctx, blobKey1) - assert.NoError(t, err) - assert.Equal(t, metadata1, fetchedMetadata) - fetchedMetadata, err = blobMetadataStore.GetBlobMetadata(ctx, blobKey2) - assert.NoError(t, err) - assert.Equal(t, metadata2, fetchedMetadata) - - fetchBulk, err := blobMetadataStore.GetBulkBlobMetadata(ctx, []disperser.BlobKey{blobKey1, blobKey2}) - assert.NoError(t, err) - assert.Equal(t, metadata1, fetchBulk[0]) - assert.Equal(t, metadata2, fetchBulk[1]) - - processing, err := blobMetadataStore.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.NoError(t, err) - assert.Len(t, processing, 1) - assert.Equal(t, metadata1, processing[0]) - - processingCount, err := blobMetadataStore.GetBlobMetadataCountByStatus(ctx, disperser.Processing) - assert.NoError(t, err) - assert.Equal(t, int32(1), processingCount) - - err = blobMetadataStore.IncrementNumRetries(ctx, metadata1) - assert.NoError(t, err) - fetchedMetadata, err = blobMetadataStore.GetBlobMetadata(ctx, blobKey1) - assert.NoError(t, err) - metadata1.NumRetries = 1 - assert.Equal(t, metadata1, fetchedMetadata) - - finalized, err := blobMetadataStore.GetBlobMetadataByStatus(ctx, disperser.Finalized) - assert.NoError(t, err) - assert.Len(t, finalized, 1) - assert.Equal(t, metadata2, finalized[0]) - - finalizedCount, err := blobMetadataStore.GetBlobMetadataCountByStatus(ctx, disperser.Finalized) - assert.NoError(t, err) - assert.Equal(t, int32(1), finalizedCount) - - confirmedMetadata := getConfirmedMetadata(t, metadata1, 1) - err = blobMetadataStore.UpdateBlobMetadata(ctx, blobKey1, confirmedMetadata) - assert.NoError(t, err) - - metadata, err := blobMetadataStore.GetBlobMetadataInBatch(ctx, confirmedMetadata.ConfirmationInfo.BatchHeaderHash, confirmedMetadata.ConfirmationInfo.BlobIndex) - assert.NoError(t, err) - assert.Equal(t, metadata, confirmedMetadata) - - confirmedCount, err := blobMetadataStore.GetBlobMetadataCountByStatus(ctx, disperser.Confirmed) - assert.NoError(t, err) - assert.Equal(t, int32(1), confirmedCount) - - deleteItems(t, []commondynamodb.Key{ - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey1.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey1.BlobHash}, - }, - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey2.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey2.BlobHash}, - }, - }) -} - -func TestBlobMetadataStoreOperationsWithPagination(t *testing.T) { - ctx := t.Context() - - blobKey1 := disperser.BlobKey{ - BlobHash: blobHash, - MetadataHash: "hash", - } - now := time.Now() - metadata1 := &disperser.BlobMetadata{ - MetadataHash: blobKey1.MetadataHash, - BlobHash: blobHash, - BlobStatus: disperser.Processing, - Expiry: uint64(now.Add(time.Hour).Unix()), - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: uint64(now.Unix()), - }, - } - blobKey2 := disperser.BlobKey{ - BlobHash: "blob2", - MetadataHash: "hash2", - } - metadata2 := &disperser.BlobMetadata{ - MetadataHash: blobKey2.MetadataHash, - BlobHash: blobKey2.BlobHash, - BlobStatus: disperser.Finalized, - Expiry: uint64(now.Add(time.Hour).Unix()), - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: uint64(now.Unix()), - }, - ConfirmationInfo: &disperser.ConfirmationInfo{}, - } - err := blobMetadataStore.QueueNewBlobMetadata(ctx, metadata1) - assert.NoError(t, err) - err = blobMetadataStore.QueueNewBlobMetadata(ctx, metadata2) - assert.NoError(t, err) - - fetchedMetadata, err := blobMetadataStore.GetBlobMetadata(ctx, blobKey1) - assert.NoError(t, err) - assert.Equal(t, metadata1, fetchedMetadata) - fetchedMetadata, err = blobMetadataStore.GetBlobMetadata(ctx, blobKey2) - assert.NoError(t, err) - assert.Equal(t, metadata2, fetchedMetadata) - - processing, lastEvaluatedKey, err := blobMetadataStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Processing, 1, nil) - assert.NoError(t, err) - assert.Len(t, processing, 1) - assert.Equal(t, metadata1, processing[0]) - assert.NotNil(t, lastEvaluatedKey) - - finalized, lastEvaluatedKey, err := blobMetadataStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Finalized, 1, nil) - assert.NoError(t, err) - assert.Len(t, finalized, 1) - assert.Equal(t, metadata2, finalized[0]) - assert.NotNil(t, lastEvaluatedKey) - - finalized, lastEvaluatedKey, err = blobMetadataStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Finalized, 1, lastEvaluatedKey) - assert.NoError(t, err) - assert.Len(t, finalized, 0) - assert.Nil(t, lastEvaluatedKey) - - deleteItems(t, []commondynamodb.Key{ - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey1.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey1.BlobHash}, - }, - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey2.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey2.BlobHash}, - }, - }) -} - -func TestGetAllBlobMetadataByBatchWithPagination(t *testing.T) { - ctx := t.Context() - - blobKey1 := disperser.BlobKey{ - BlobHash: blobHash, - MetadataHash: "hash", - } - expiry := uint64(time.Now().Add(time.Hour).Unix()) - metadata1 := &disperser.BlobMetadata{ - MetadataHash: blobKey1.MetadataHash, - BlobHash: blobHash, - BlobStatus: disperser.Processing, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: 123, - }, - } - blobKey2 := disperser.BlobKey{ - BlobHash: "blob2", - MetadataHash: "hash2", - } - metadata2 := &disperser.BlobMetadata{ - MetadataHash: blobKey2.MetadataHash, - BlobHash: blobKey2.BlobHash, - BlobStatus: disperser.Finalized, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: 123, - }, - ConfirmationInfo: &disperser.ConfirmationInfo{}, - } - err := blobMetadataStore.QueueNewBlobMetadata(ctx, metadata1) - assert.NoError(t, err) - err = blobMetadataStore.QueueNewBlobMetadata(ctx, metadata2) - assert.NoError(t, err) - - confirmedMetadata1 := getConfirmedMetadata(t, metadata1, 1) - err = blobMetadataStore.UpdateBlobMetadata(ctx, blobKey1, confirmedMetadata1) - assert.NoError(t, err) - - confirmedMetadata2 := getConfirmedMetadata(t, metadata2, 2) - err = blobMetadataStore.UpdateBlobMetadata(ctx, blobKey2, confirmedMetadata2) - assert.NoError(t, err) - - // Fetch the blob metadata with limit 1 - metadata, exclusiveStartKey, err := blobMetadataStore.GetAllBlobMetadataByBatchWithPagination(ctx, confirmedMetadata1.ConfirmationInfo.BatchHeaderHash, 1, nil) - assert.NoError(t, err) - assert.Equal(t, metadata[0], confirmedMetadata1) - assert.NotNil(t, exclusiveStartKey) - assert.Equal(t, confirmedMetadata1.ConfirmationInfo.BlobIndex, exclusiveStartKey.BlobIndex) - - // Get the next blob metadata with limit 1 and the exclusive start key - metadata, exclusiveStartKey, err = blobMetadataStore.GetAllBlobMetadataByBatchWithPagination(ctx, confirmedMetadata1.ConfirmationInfo.BatchHeaderHash, 1, exclusiveStartKey) - assert.NoError(t, err) - assert.Equal(t, metadata[0], confirmedMetadata2) - assert.Equal(t, confirmedMetadata2.ConfirmationInfo.BlobIndex, exclusiveStartKey.BlobIndex) - - // Fetching the next blob metadata should return an empty list - metadata, exclusiveStartKey, err = blobMetadataStore.GetAllBlobMetadataByBatchWithPagination(ctx, confirmedMetadata1.ConfirmationInfo.BatchHeaderHash, 1, exclusiveStartKey) - assert.NoError(t, err) - assert.Len(t, metadata, 0) - assert.Nil(t, exclusiveStartKey) - - // Fetch the blob metadata with limit 2 - metadata, exclusiveStartKey, err = blobMetadataStore.GetAllBlobMetadataByBatchWithPagination(ctx, confirmedMetadata1.ConfirmationInfo.BatchHeaderHash, 2, nil) - assert.NoError(t, err) - assert.Len(t, metadata, 2) - assert.Equal(t, metadata[0], confirmedMetadata1) - assert.Equal(t, metadata[1], confirmedMetadata2) - assert.NotNil(t, exclusiveStartKey) - assert.Equal(t, confirmedMetadata2.ConfirmationInfo.BlobIndex, exclusiveStartKey.BlobIndex) - - // Fetch the blob metadata with limit 3 should return only 2 items - metadata, exclusiveStartKey, err = blobMetadataStore.GetAllBlobMetadataByBatchWithPagination(ctx, confirmedMetadata1.ConfirmationInfo.BatchHeaderHash, 3, nil) - assert.NoError(t, err) - assert.Len(t, metadata, 2) - assert.Equal(t, metadata[0], confirmedMetadata1) - assert.Equal(t, metadata[1], confirmedMetadata2) - assert.Nil(t, exclusiveStartKey) - - deleteItems(t, []commondynamodb.Key{ - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey1.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey1.BlobHash}, - }, - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey2.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey2.BlobHash}, - }, - }) -} - -func TestBlobMetadataStoreOperationsWithPaginationNoStoredBlob(t *testing.T) { - ctx := t.Context() - - // Query BlobMetadataStore for a blob that does not exist - // This should return nil for both the blob and lastEvaluatedKey - processing, lastEvaluatedKey, err := blobMetadataStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Processing, 1, nil) - assert.NoError(t, err) - assert.Nil(t, processing) - assert.Nil(t, lastEvaluatedKey) -} - -func TestFilterOutExpiredBlobMetadata(t *testing.T) { - ctx := t.Context() - - blobKey := disperser.BlobKey{ - BlobHash: "blob1", - MetadataHash: "hash1", - } - now := time.Now() - metadata := &disperser.BlobMetadata{ - MetadataHash: blobKey.MetadataHash, - BlobHash: blobKey.BlobHash, - BlobStatus: disperser.Processing, - Expiry: uint64(now.Add(-1).Unix()), - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: uint64(now.Add(-1000).Unix()), - }, - ConfirmationInfo: &disperser.ConfirmationInfo{}, - } - - err := blobMetadataStore.QueueNewBlobMetadata(ctx, metadata) - assert.NoError(t, err) - - processing, err := blobMetadataStore.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.NoError(t, err) - assert.Len(t, processing, 0) - - processingCount, err := blobMetadataStore.GetBlobMetadataCountByStatus(ctx, disperser.Processing) - assert.NoError(t, err) - assert.Equal(t, int32(0), processingCount) - - processing, _, err = blobMetadataStore.GetBlobMetadataByStatusWithPagination(ctx, disperser.Processing, 10, nil) - assert.NoError(t, err) - assert.Len(t, processing, 0) - - deleteItems(t, []commondynamodb.Key{ - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey.BlobHash}, - }, - }) -} - -func deleteItems(t *testing.T, keys []commondynamodb.Key) { - t.Helper() - ctx := t.Context() - _, err := dynamoClient.DeleteItems(ctx, metadataTableName, keys) - assert.NoError(t, err) -} - -func getConfirmedMetadata(t *testing.T, metadata *disperser.BlobMetadata, blobIndex uint32) *disperser.BlobMetadata { - t.Helper() - - batchHeaderHash := [32]byte{1, 2, 3} - var commitX, commitY fp.Element - _, err := commitX.SetString("21661178944771197726808973281966770251114553549453983978976194544185382599016") - assert.NoError(t, err) - _, err = commitY.SetString("9207254729396071334325696286939045899948985698134704137261649190717970615186") - assert.NoError(t, err) - commitment := &encoding.G1Commitment{ - X: commitX, - Y: commitY, - } - batchID := uint32(99) - batchRoot := []byte("hello") - referenceBlockNumber := uint32(132) - confirmationBlockNumber := uint32(150) - sigRecordHash := [32]byte{0} - fee := []byte{0} - inclusionProof := []byte{1, 2, 3, 4, 5} - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - SignatoryRecordHash: sigRecordHash, - ReferenceBlockNumber: referenceBlockNumber, - BatchRoot: batchRoot, - BlobInclusionProof: inclusionProof, - BlobCommitment: &encoding.BlobCommitments{ - Commitment: commitment, - Length: 32, - }, - BatchID: batchID, - ConfirmationTxnHash: common.HexToHash("0x123"), - ConfirmationBlockNumber: confirmationBlockNumber, - Fee: fee, - } - metadata.BlobStatus = disperser.Confirmed - metadata.ConfirmationInfo = confirmationInfo - return metadata -} diff --git a/disperser/common/blobstore/blobstore_test.go b/disperser/common/blobstore/blobstore_test.go deleted file mode 100644 index faed0c9e3e..0000000000 --- a/disperser/common/blobstore/blobstore_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package blobstore_test - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/common/aws" - "github.com/Layr-Labs/eigenda/common/aws/dynamodb" - test_utils "github.com/Layr-Labs/eigenda/common/aws/dynamodb/utils" - s3common "github.com/Layr-Labs/eigenda/common/s3" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser/common/blobstore" - "github.com/Layr-Labs/eigenda/test" - "github.com/Layr-Labs/eigenda/test/testbed" - "github.com/google/uuid" -) - -var ( - logger = test.GetLogger() - securityParams = []*core.SecurityParam{{ - QuorumID: 1, - AdversaryThreshold: 80, - QuorumRate: 32000, - }, - } - blob = &core.Blob{ - RequestHeader: core.BlobRequestHeader{ - SecurityParams: securityParams, - }, - Data: []byte("test"), - } - s3Client = s3common.NewMockS3Client() - bucketName = "test-eigenda-blobstore" - blobHash = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" - blobSize = uint(len(blob.Data)) - - localstackContainer *testbed.LocalStackContainer - - deployLocalStack bool - localstackPort = "4569" - - dynamoClient dynamodb.Client - blobMetadataStore *blobstore.BlobMetadataStore - sharedStorage *blobstore.SharedBlobStore - - UUID = uuid.New() - metadataTableName = fmt.Sprintf("test-BlobMetadata-%v", UUID) - shadowMetadataTableName = fmt.Sprintf("test-BlobMetadata-Shadow-%v", UUID) -) - -func TestMain(m *testing.M) { - setup(m) - code := m.Run() - teardown() - os.Exit(code) -} - -func setup(_ *testing.M) { - ctx := context.Background() - - deployLocalStack = (os.Getenv("DEPLOY_LOCALSTACK") != "false") - if !deployLocalStack { - localstackPort = os.Getenv("LOCALSTACK_PORT") - } - - if deployLocalStack { - var err error - localstackContainer, err = testbed.NewLocalStackContainerWithOptions(ctx, testbed.LocalStackOptions{ - ExposeHostPort: true, - HostPort: localstackPort, - Services: []string{"s3", "dynamodb"}, - Logger: logger, - }) - if err != nil { - teardown() - logger.Fatal("Failed to start localstack container:", err) - } - - } - - cfg := aws.ClientConfig{ - Region: "us-east-1", - AccessKey: "localstack", - SecretAccessKey: "localstack", - EndpointURL: fmt.Sprintf("http://0.0.0.0:%s", localstackPort), - } - - _, err := test_utils.CreateTable(ctx, cfg, metadataTableName, blobstore.GenerateTableSchema(metadataTableName, 10, 10)) - if err != nil { - teardown() - logger.Fatal("Failed to create dynamodb table:", err) - } - - if shadowMetadataTableName != "" { - _, err = test_utils.CreateTable(ctx, cfg, shadowMetadataTableName, - blobstore.GenerateTableSchema(shadowMetadataTableName, 10, 10)) - if err != nil { - teardown() - logger.Fatal("Failed to create shadow dynamodb table:", err) - } - } - - dynamoClient, err = dynamodb.NewClient(cfg, logger) - if err != nil { - teardown() - logger.Fatal("Failed to create dynamodb client:", err) - } - - blobMetadataStore = blobstore.NewBlobMetadataStore(dynamoClient, logger, metadataTableName, time.Hour) - sharedStorage = blobstore.NewSharedStorage(bucketName, s3Client, blobMetadataStore, logger) -} - -func teardown() { - if deployLocalStack && localstackContainer != nil { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = localstackContainer.Terminate(ctx) - } -} diff --git a/disperser/common/blobstore/client_factory_test.go b/disperser/common/blobstore/client_factory_test.go deleted file mode 100644 index c89eebd7e3..0000000000 --- a/disperser/common/blobstore/client_factory_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package blobstore - -import ( - "context" - "testing" - - "github.com/Layr-Labs/eigenda/common/aws" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/stretchr/testify/assert" -) - -// mockLogger is a simple mock logger for testing -type mockLogger struct{} - -func (m *mockLogger) Debug(msg string, args ...interface{}) {} -func (m *mockLogger) Info(msg string, args ...interface{}) {} -func (m *mockLogger) Warn(msg string, args ...interface{}) {} -func (m *mockLogger) Error(msg string, args ...interface{}) {} -func (m *mockLogger) Fatal(msg string, args ...interface{}) {} -func (m *mockLogger) Debugf(template string, args ...interface{}) {} -func (m *mockLogger) Infof(template string, args ...interface{}) {} -func (m *mockLogger) Warnf(template string, args ...interface{}) {} -func (m *mockLogger) Errorf(template string, args ...interface{}) {} -func (m *mockLogger) Fatalf(template string, args ...interface{}) {} -func (m *mockLogger) With(tags ...any) logging.Logger { return m } - -func TestCreateObjectStorageClient_S3Backend(t *testing.T) { - ctx := context.Background() - config := Config{ - Backend: S3Backend, - BucketName: "test-bucket", - TableName: "test-table", - } - awsConfig := aws.ClientConfig{ - Region: "us-east-1", - AccessKey: "test-access-key", - SecretAccessKey: "test-secret-key", - EndpointURL: "", - FragmentParallelismConstant: 1, - FragmentParallelismFactor: 0, - } - logger := &mockLogger{} - - // This test will fail without AWS credentials, but it tests the factory logic - client, err := CreateObjectStorageClient(ctx, config, awsConfig, logger) - - // We expect an error in test environment without AWS setup - if err != nil { - assert.Contains(t, err.Error(), "failed to create S3 client") - } else { - assert.NotNil(t, client) - } -} - -func TestCreateObjectStorageClient_OCIBackend(t *testing.T) { - ctx := context.Background() - config := Config{ - Backend: OCIBackend, - BucketName: "test-bucket", - TableName: "test-table", - } - awsConfig := aws.ClientConfig{ - Region: "us-east-1", - FragmentParallelismConstant: 1, - FragmentParallelismFactor: 0, - } - logger := &mockLogger{} - - // This test will fail without OCI credentials, but it tests the factory logic - client, err := CreateObjectStorageClient(ctx, config, awsConfig, logger) - - // We expect an error in test environment without OCI setup - if err != nil { - assert.Contains(t, err.Error(), "failed to create OCI object storage client") - } else { - assert.NotNil(t, client) - } -} - -func TestCreateObjectStorageClient_UnsupportedBackend(t *testing.T) { - ctx := context.Background() - config := Config{ - Backend: "unsupported-backend", - BucketName: "test-bucket", - TableName: "test-table", - } - awsConfig := aws.ClientConfig{ - Region: "us-east-1", - } - logger := &mockLogger{} - - client, err := CreateObjectStorageClient(ctx, config, awsConfig, logger) - - assert.Nil(t, client) - assert.Error(t, err) - assert.Contains(t, err.Error(), "unsupported object storage backend: unsupported-backend") -} - -func TestCreateObjectStorageClient_EmptyBackend(t *testing.T) { - ctx := context.Background() - config := Config{ - Backend: "", // Empty backend should default somewhere or error - BucketName: "test-bucket", - TableName: "test-table", - } - awsConfig := aws.ClientConfig{ - Region: "us-east-1", - } - logger := &mockLogger{} - - client, err := CreateObjectStorageClient(ctx, config, awsConfig, logger) - - // Should error due to unsupported backend - assert.Nil(t, client) - assert.Error(t, err) - assert.Contains(t, err.Error(), "unsupported object storage backend") -} - -func TestCreateObjectStorageClient_OCIWithFragmentParallelismFactor(t *testing.T) { - ctx := context.Background() - config := Config{ - Backend: OCIBackend, - BucketName: "test-bucket", - TableName: "test-table", - } - awsConfig := aws.ClientConfig{ - Region: "us-east-1", - FragmentParallelismFactor: 2, // Should result in 2 * runtime.NumCPU() workers - } - logger := &mockLogger{} - - // This test will fail without OCI credentials, but it tests the configuration logic - client, err := CreateObjectStorageClient(ctx, config, awsConfig, logger) - - // We expect an error in test environment, but the config should be passed correctly - if err != nil { - assert.Contains(t, err.Error(), "failed to create OCI object storage client") - } else { - assert.NotNil(t, client) - } -} - -func TestObjectStorageBackend_Constants(t *testing.T) { - assert.Equal(t, ObjectStorageBackend("s3"), S3Backend) - assert.Equal(t, ObjectStorageBackend("oci"), OCIBackend) -} - -func TestConfig_Struct(t *testing.T) { - config := Config{ - BucketName: "test-bucket", - TableName: "test-table", - Backend: S3Backend, - } - - assert.Equal(t, "test-bucket", config.BucketName) - assert.Equal(t, "test-table", config.TableName) - assert.Equal(t, S3Backend, config.Backend) -} - -func TestCreateObjectStorageClient_OCIMinimalConfig(t *testing.T) { - ctx := context.Background() - config := Config{ - Backend: OCIBackend, - BucketName: "test-bucket", - TableName: "test-table", - } - awsConfig := aws.ClientConfig{ - // Minimal AWS config for OCI (only fragment settings used) - FragmentParallelismConstant: 0, - FragmentParallelismFactor: 0, - } - logger := &mockLogger{} - - // This should still work (but fail due to credentials) - client, err := CreateObjectStorageClient(ctx, config, awsConfig, logger) - - if err != nil { - assert.Contains(t, err.Error(), "failed to create OCI object storage client") - } else { - assert.NotNil(t, client) - } -} diff --git a/disperser/common/blobstore/shared_storage.go b/disperser/common/blobstore/shared_storage.go index cbc44b4a50..fb9cb27827 100644 --- a/disperser/common/blobstore/shared_storage.go +++ b/disperser/common/blobstore/shared_storage.go @@ -1,52 +1,5 @@ package blobstore -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "time" - - "github.com/Layr-Labs/eigenda/common/s3" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/gammazero/workerpool" -) - -const ( - maxS3BlobFetchWorkers = 64 -) - -var errProcessingToDispersing = errors.New("blob transit to dispersing from non processing") - -// The shared blob store that the disperser is operating on. -// The metadata store is backed by DynamoDB and the blob store is backed by S3. -// -// Note: -// - For each entry in the store (i.e. an S3 object), the user has to ensure there is no -// concurrent writers -// -// The blobs are identified by blobKey, which is hash(blob), where blob contains the content -// of the blob (bytes). -// -// The same blob (sameness determined by blobKey) at different requests are processed as different -// blobs in disperser. This is distinguished via requestAt, the timestamp (in ns) at which the -// request arrives, as well as security parameters. -// The blob object is reused for different requests in blobstore. -// -// This store tracks the blob, the state of the blob and the index (to facilitate retrieval). -// -// The blobs stored in S3 are key'd by the blob key and the metadata stored in DynamoDB. -// See blob_metadata_store.go for more details on BlobMetadataStore. -type SharedBlobStore struct { - bucketName string - s3Client s3.S3Client - blobMetadataStore *BlobMetadataStore - logger logging.Logger -} - type ObjectStorageBackend string const ( @@ -63,274 +16,3 @@ type Config struct { OCIRegion string OCICompartmentID string } - -// This represents the s3 fetch result for a blob. -type blobResultOrError struct { - // Indicating if the s3 fetch succeeded. - err error - - // The actual fetch results. Undefined if the err above isn't nil. - blob []byte - blobKey disperser.BlobKey - blobRequestHeader core.BlobRequestHeader -} - -var _ disperser.BlobStore = (*SharedBlobStore)(nil) - -func NewSharedStorage( - bucketName string, - s3Client s3.S3Client, - blobMetadataStore *BlobMetadataStore, - logger logging.Logger, -) *SharedBlobStore { - return &SharedBlobStore{ - bucketName: bucketName, - s3Client: s3Client, - blobMetadataStore: blobMetadataStore, - logger: logger.With("component", "SharedBlobStore"), - } -} - -func (s *SharedBlobStore) StoreBlob(ctx context.Context, blob *core.Blob, requestedAt uint64) (disperser.BlobKey, error) { - metadataKey := disperser.BlobKey{} - if blob == nil { - return metadataKey, errors.New("blob is nil") - } - - blobHash := getBlobHash(blob) - metadataHash, err := getMetadataHash(requestedAt, blob.RequestHeader.SecurityParams) - if err != nil { - s.logger.Error("error creating metadata key", "err", err) - return metadataKey, err - } - metadataKey.BlobHash = blobHash - metadataKey.MetadataHash = metadataHash - - err = s.s3Client.UploadObject(ctx, s.bucketName, blobObjectKey(blobHash), blob.Data) - if err != nil { - s.logger.Error("error uploading blob", "err", err) - return metadataKey, err - } - - // don't expire if ttl is 0 - expiry := uint64(0) - if s.blobMetadataStore.ttl > 0 { - expiry = uint64(time.Now().Add(s.blobMetadataStore.ttl).Unix()) - } - metadata := disperser.BlobMetadata{ - BlobHash: blobHash, - MetadataHash: metadataHash, - NumRetries: 0, - BlobStatus: disperser.Processing, - Expiry: expiry, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: uint(len(blob.Data)), - RequestedAt: requestedAt, - }, - } - err = s.blobMetadataStore.QueueNewBlobMetadata(ctx, &metadata) - if err != nil { - if errors.Is(err, context.Canceled) { - s.logger.Warn("context canceled while queuing new blob metadata", "err", err) - } else if errors.Is(err, context.DeadlineExceeded) { - s.logger.Warn("context deadline exceeded while queuing new blob metadata", "err", err) - } else { - s.logger.Error("error uploading blob metadata", "err", err) - } - return metadataKey, err - } - - return metadataKey, nil -} - -// GetBlobContent retrieves blob content by the blob key. -func (s *SharedBlobStore) GetBlobContent(ctx context.Context, blobHash disperser.BlobHash) ([]byte, error) { - data, found, err := s.s3Client.DownloadObject(ctx, s.bucketName, blobObjectKey(blobHash)) - if err != nil { - return nil, fmt.Errorf("error downloading blob content: %w", err) - } - if !found { - return nil, fmt.Errorf("blob not found for blob hash: %s", blobHash) - } - return data, nil -} - -func (s *SharedBlobStore) getBlobContentParallel(ctx context.Context, blobKey disperser.BlobKey, blobRequestHeader core.BlobRequestHeader, resultChan chan<- blobResultOrError) { - blob, found, err := s.s3Client.DownloadObject(ctx, s.bucketName, blobObjectKey(blobKey.BlobHash)) - if !found { - err = fmt.Errorf("blob not found for blob key: %s", blobKey.String()) - } - if err != nil { - resultChan <- blobResultOrError{err: err} - return - } - resultChan <- blobResultOrError{blob: blob, blobKey: blobKey, blobRequestHeader: blobRequestHeader} -} - -func (s *SharedBlobStore) MarkBlobConfirmed(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationInfo *disperser.ConfirmationInfo) (*disperser.BlobMetadata, error) { - // TODO (ian-shim): remove this check once we are sure that the metadata is never overwritten - refreshedMetadata, err := s.GetBlobMetadata(ctx, existingMetadata.GetBlobKey()) - if err != nil { - s.logger.Error("error getting blob metadata", "err", err) - return nil, err - } - alreadyConfirmed, _ := refreshedMetadata.IsConfirmed() - if alreadyConfirmed { - s.logger.Warn("trying to confirm blob already marked as confirmed", "blobKey", existingMetadata.GetBlobKey().String()) - return refreshedMetadata, nil - } - newMetadata := *existingMetadata - // Update the TTL if needed - ttlFromNow := time.Now().Add(s.blobMetadataStore.ttl) - if existingMetadata.Expiry < uint64(ttlFromNow.Unix()) { - newMetadata.Expiry = uint64(ttlFromNow.Unix()) - } - newMetadata.BlobStatus = disperser.Confirmed - newMetadata.ConfirmationInfo = confirmationInfo - return &newMetadata, s.blobMetadataStore.UpdateBlobMetadata(ctx, existingMetadata.GetBlobKey(), &newMetadata) -} - -func (s *SharedBlobStore) MarkBlobDispersing(ctx context.Context, metadataKey disperser.BlobKey) error { - refreshedMetadata, err := s.GetBlobMetadata(ctx, metadataKey) - if err != nil { - s.logger.Error("error getting blob metadata while marking blobDispersing", "err", err) - return err - } - - status := refreshedMetadata.BlobStatus - if status != disperser.Processing { - s.logger.Error("error marking blob as dispersing from non processing state", "blobKey", metadataKey.String(), "status", status) - return errProcessingToDispersing - } - - return s.blobMetadataStore.SetBlobStatus(ctx, metadataKey, disperser.Dispersing) -} - -func (s *SharedBlobStore) MarkBlobInsufficientSignatures(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationInfo *disperser.ConfirmationInfo) (*disperser.BlobMetadata, error) { - if existingMetadata == nil { - return nil, errors.New("metadata is nil") - } - newMetadata := *existingMetadata - newMetadata.BlobStatus = disperser.InsufficientSignatures - if confirmationInfo != nil { - newMetadata.ConfirmationInfo = confirmationInfo - } - return &newMetadata, s.blobMetadataStore.UpdateBlobMetadata(ctx, existingMetadata.GetBlobKey(), &newMetadata) -} - -func (s *SharedBlobStore) MarkBlobFinalized(ctx context.Context, blobKey disperser.BlobKey) error { - return s.blobMetadataStore.SetBlobStatus(ctx, blobKey, disperser.Finalized) -} - -func (s *SharedBlobStore) MarkBlobProcessing(ctx context.Context, metadataKey disperser.BlobKey) error { - return s.blobMetadataStore.SetBlobStatus(ctx, metadataKey, disperser.Processing) -} - -func (s *SharedBlobStore) MarkBlobFailed(ctx context.Context, metadataKey disperser.BlobKey) error { - // Log failed blob - s.logger.Info("marking blob as failed", "blobKey", metadataKey.String()) - return s.blobMetadataStore.SetBlobStatus(ctx, metadataKey, disperser.Failed) -} - -func (s *SharedBlobStore) IncrementBlobRetryCount(ctx context.Context, existingMetadata *disperser.BlobMetadata) error { - return s.blobMetadataStore.IncrementNumRetries(ctx, existingMetadata) -} - -func (s *SharedBlobStore) UpdateConfirmationBlockNumber(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationBlockNumber uint32) error { - return s.blobMetadataStore.UpdateConfirmationBlockNumber(ctx, existingMetadata, confirmationBlockNumber) -} - -func (s *SharedBlobStore) GetBlobsByMetadata(ctx context.Context, metadata []*disperser.BlobMetadata) (map[disperser.BlobKey]*core.Blob, error) { - pool := workerpool.New(maxS3BlobFetchWorkers) - resultChan := make(chan blobResultOrError, len(metadata)) - - blobs := make(map[disperser.BlobKey]*core.Blob, 0) - - for _, m := range metadata { - mCopy := m // avoid capturing loop variable "m" directly by making a copy - pool.Submit(func() { - // Fetch blob content from S3 - s.getBlobContentParallel(ctx, mCopy.GetBlobKey(), mCopy.RequestMetadata.BlobRequestHeader, resultChan) - }) - } - - pool.StopWait() // wait for pending tasks to complete - close(resultChan) - - // Collect results from channel - for result := range resultChan { - if result.err != nil { - return nil, result.err - } - blobs[result.blobKey] = &core.Blob{ - RequestHeader: result.blobRequestHeader, - Data: result.blob, - } - } - - return blobs, nil -} - -func (s *SharedBlobStore) GetBlobMetadataByStatus(ctx context.Context, blobStatus disperser.BlobStatus) ([]*disperser.BlobMetadata, error) { - return s.blobMetadataStore.GetBlobMetadataByStatus(ctx, blobStatus) -} - -func (s *SharedBlobStore) GetBlobMetadataByStatusWithPagination(ctx context.Context, blobStatus disperser.BlobStatus, limit int32, exclusiveStartKey *disperser.BlobStoreExclusiveStartKey) ([]*disperser.BlobMetadata, *disperser.BlobStoreExclusiveStartKey, error) { - return s.blobMetadataStore.GetBlobMetadataByStatusWithPagination(ctx, blobStatus, limit, exclusiveStartKey) -} - -func (s *SharedBlobStore) GetMetadataInBatch(ctx context.Context, batchHeaderHash [32]byte, blobIndex uint32) (*disperser.BlobMetadata, error) { - return s.blobMetadataStore.GetBlobMetadataInBatch(ctx, batchHeaderHash, blobIndex) -} - -func (s *SharedBlobStore) GetAllBlobMetadataByBatch(ctx context.Context, batchHeaderHash [32]byte) ([]*disperser.BlobMetadata, error) { - return s.blobMetadataStore.GetAllBlobMetadataByBatch(ctx, batchHeaderHash) -} - -func (s *SharedBlobStore) GetAllBlobMetadataByBatchWithPagination(ctx context.Context, batchHeaderHash [32]byte, limit int32, exclusiveStartKey *disperser.BatchIndexExclusiveStartKey) ([]*disperser.BlobMetadata, *disperser.BatchIndexExclusiveStartKey, error) { - return s.blobMetadataStore.GetAllBlobMetadataByBatchWithPagination(ctx, batchHeaderHash, limit, exclusiveStartKey) -} - -// GetMetadata returns a blob metadata given a metadata key -func (s *SharedBlobStore) GetBlobMetadata(ctx context.Context, metadataKey disperser.BlobKey) (*disperser.BlobMetadata, error) { - return s.blobMetadataStore.GetBlobMetadata(ctx, metadataKey) -} - -func (s *SharedBlobStore) GetBulkBlobMetadata(ctx context.Context, blobKeys []disperser.BlobKey) ([]*disperser.BlobMetadata, error) { - return s.blobMetadataStore.GetBulkBlobMetadata(ctx, blobKeys) -} - -func (s *SharedBlobStore) HandleBlobFailure(ctx context.Context, metadata *disperser.BlobMetadata, maxRetry uint) (bool, error) { - if metadata.NumRetries < maxRetry { - if err := s.MarkBlobProcessing(ctx, metadata.GetBlobKey()); err != nil { - return true, err - } - return true, s.IncrementBlobRetryCount(ctx, metadata) - } else { - return false, s.MarkBlobFailed(ctx, metadata.GetBlobKey()) - } -} - -func getMetadataHash(requestedAt uint64, securityParams []*core.SecurityParam) (string, error) { - var str string - str = fmt.Sprintf("%d/", requestedAt) - for _, param := range securityParams { - appendStr := fmt.Sprintf("%d/%d/", param.QuorumID, param.AdversaryThreshold) - // Append String incase of multiple securityParams - str = str + appendStr - } - bytes := []byte(str) - return hex.EncodeToString(sha256.New().Sum(bytes)), nil -} - -func blobObjectKey(blobHash disperser.BlobHash) string { - return fmt.Sprintf("blob/%s.json", blobHash) -} - -func getBlobHash(blob *core.Blob) disperser.BlobHash { - hasher := sha256.New() - hasher.Write(blob.Data) - hash := hasher.Sum(nil) - return hex.EncodeToString(hash) -} diff --git a/disperser/common/blobstore/shared_storage_test.go b/disperser/common/blobstore/shared_storage_test.go deleted file mode 100644 index 443947ccc6..0000000000 --- a/disperser/common/blobstore/shared_storage_test.go +++ /dev/null @@ -1,455 +0,0 @@ -package blobstore_test - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "testing" - "time" - - commondynamodb "github.com/Layr-Labs/eigenda/common/aws/dynamodb" - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/stretchr/testify/assert" - - "github.com/ethereum/go-ethereum/common" -) - -func TestSharedBlobStore(t *testing.T) { - ctx := t.Context() - requestedAt := uint64(time.Now().UnixNano()) - blobKey, err := sharedStorage.StoreBlob(ctx, blob, requestedAt) - assert.Nil(t, err) - assert.Equal(t, blobHash, blobKey.BlobHash) - - metadatas, err := sharedStorage.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.Nil(t, err) - assert.Len(t, metadatas, 1) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Processing, metadatas[0]) - - blobs, err := sharedStorage.GetBlobsByMetadata(ctx, metadatas) - assert.Nil(t, err) - assert.Len(t, blobs, 1) - assertBlob(t, blobs[blobKey]) - - data, err := sharedStorage.GetBlobContent(ctx, blobKey.BlobHash) - assert.Nil(t, err) - assert.Equal(t, blob.Data, data) - - err = sharedStorage.MarkBlobFailed(ctx, blobKey) - assert.Nil(t, err) - - metadata1, err := sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Failed, metadata1) - - err = sharedStorage.MarkBlobProcessing(ctx, blobKey) - assert.Nil(t, err) - - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Processing, metadata1) - - err = sharedStorage.IncrementBlobRetryCount(ctx, metadata1) - assert.Nil(t, err) - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assert.Equal(t, uint(1), metadata1.NumRetries) - - err = sharedStorage.IncrementBlobRetryCount(ctx, metadata1) - assert.Nil(t, err) - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assert.Equal(t, uint(2), metadata1.NumRetries) - - batchHeaderHash := [32]byte{1, 2, 3} - blobIndex := uint32(0) - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - BlobCount: 2, - SignatoryRecordHash: [32]byte{0}, - ReferenceBlockNumber: 132, - BatchRoot: []byte("hello"), - BlobCommitment: &encoding.BlobCommitments{}, - BatchID: 99, - ConfirmationTxnHash: common.HexToHash("0x123"), - ConfirmationBlockNumber: 150, - Fee: []byte{0}, - } - metadata := &disperser.BlobMetadata{ - BlobHash: blobKey.BlobHash, - MetadataHash: blobKey.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: 0, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: securityParams, - }, - RequestedAt: requestedAt, - BlobSize: blobSize, - }, - } - updatedMetadata, err := sharedStorage.MarkBlobConfirmed(ctx, metadata, confirmationInfo) - assert.Nil(t, err) - assert.Equal(t, disperser.Confirmed, updatedMetadata.BlobStatus) - - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Confirmed, metadata1) - - err = sharedStorage.UpdateConfirmationBlockNumber(ctx, metadata1, 151) - assert.Nil(t, err) - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assert.Equal(t, uint32(151), metadata1.ConfirmationInfo.ConfirmationBlockNumber) - - err = sharedStorage.MarkBlobFinalized(ctx, blobKey) - assert.Nil(t, err) - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assert.Equal(t, disperser.Finalized, metadata1.BlobStatus) - - metadata1, err = sharedStorage.GetBlobMetadata(ctx, blobKey) - assert.Nil(t, err) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Finalized, metadata1) - - allMetadata, err := sharedStorage.GetAllBlobMetadataByBatch(ctx, batchHeaderHash) - assert.Nil(t, err) - assert.Equal(t, 1, len(allMetadata)) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Finalized, allMetadata[0]) - - // Store the second blob and then check the metadata. - blob.Data = []byte("foo") - blobSize2 := uint(len(blob.Data)) - blobKey2, err := sharedStorage.StoreBlob(ctx, blob, requestedAt) - assert.Nil(t, err) - assert.NotEqual(t, blobKey, blobKey2) - confirmationInfo = &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: uint32(1), - BlobCount: 2, - SignatoryRecordHash: [32]byte{0}, - ReferenceBlockNumber: 132, - BatchRoot: []byte("hello"), - BlobCommitment: &encoding.BlobCommitments{}, - BatchID: 99, - ConfirmationBlockNumber: 150, - Fee: []byte{0}, - } - metadata = &disperser.BlobMetadata{ - BlobHash: blobKey2.BlobHash, - MetadataHash: blobKey2.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: 0, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: securityParams, - }, - RequestedAt: requestedAt, - BlobSize: blobSize2, - }, - } - updatedMetadata, err = sharedStorage.MarkBlobInsufficientSignatures(ctx, metadata, confirmationInfo) - assert.Nil(t, err) - assert.Equal(t, disperser.InsufficientSignatures, updatedMetadata.BlobStatus) - - allMetadata, err = sharedStorage.GetAllBlobMetadataByBatch(ctx, batchHeaderHash) - assert.Nil(t, err) - assert.Equal(t, 2, len(allMetadata)) - var blob1Metadata, blob2Metadata *disperser.BlobMetadata - for i, metadata := range allMetadata { - switch metadata.BlobHash { - case metadata1.BlobHash: - blob1Metadata = allMetadata[i] - case updatedMetadata.BlobHash: - blob2Metadata = allMetadata[i] - default: - t.Fatalf("Unexpected blob hash in metadata: %s", metadata.BlobHash) - } - } - assert.NotNil(t, blob1Metadata) - assert.NotNil(t, blob2Metadata) - assertMetadata(t, blobKey, blobSize, requestedAt, disperser.Finalized, blob1Metadata) - assertMetadata(t, blobKey2, blobSize2, requestedAt, disperser.InsufficientSignatures, blob2Metadata) - - // Cleanup: Delete test items - t.Cleanup(func() { - deleteItemsWithBackgroundContext(t, []commondynamodb.Key{ - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey.BlobHash}, - }, - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey2.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey2.BlobHash}, - }, - }) - }) -} - -func TestSharedBlobStoreBlobMetadataStoreOperationsWithPagination(t *testing.T) { - ctx := t.Context() - blobKey1 := disperser.BlobKey{ - BlobHash: blobHash, - MetadataHash: "hash", - } - expiry := uint64(time.Now().Add(time.Hour).Unix()) - metadata1 := &disperser.BlobMetadata{ - MetadataHash: blobKey1.MetadataHash, - BlobHash: blobHash, - BlobStatus: disperser.Processing, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: 123, - }, - } - blobKey2 := disperser.BlobKey{ - BlobHash: "blob2", - MetadataHash: "hash2", - } - metadata2 := &disperser.BlobMetadata{ - MetadataHash: blobKey2.MetadataHash, - BlobHash: blobKey2.BlobHash, - BlobStatus: disperser.Finalized, - Expiry: expiry, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: 123, - }, - ConfirmationInfo: &disperser.ConfirmationInfo{}, - } - - // Setup: Queue new blob metadata - err := blobMetadataStore.QueueNewBlobMetadata(ctx, metadata1) - assert.NoError(t, err) - err = blobMetadataStore.QueueNewBlobMetadata(ctx, metadata2) - assert.NoError(t, err) - - // Test: Fetch individual blob metadata - fetchedMetadata, err := sharedStorage.GetBlobMetadata(ctx, blobKey1) - assert.NoError(t, err) - assert.Equal(t, metadata1, fetchedMetadata) - fetchedMetadata, err = sharedStorage.GetBlobMetadata(ctx, blobKey2) - assert.NoError(t, err) - assert.Equal(t, metadata2, fetchedMetadata) - - // Test: Fetch blob metadata by status with pagination - t.Run("Fetch Processing Blobs", func(t *testing.T) { - processing, lastEvaluatedKey, err := sharedStorage.GetBlobMetadataByStatusWithPagination(ctx, disperser.Processing, 1, nil) - assert.NoError(t, err) - assert.Len(t, processing, 1) - assert.Equal(t, metadata1, processing[0]) - assert.NotNil(t, lastEvaluatedKey) - - // Fetch next page (should be empty) - nextProcessing, nextLastEvaluatedKey, err := sharedStorage.GetBlobMetadataByStatusWithPagination(ctx, disperser.Processing, 1, lastEvaluatedKey) - assert.NoError(t, err) - assert.Len(t, nextProcessing, 0) - assert.Nil(t, nextLastEvaluatedKey) - }) - - t.Run("Fetch Finalized Blobs", func(t *testing.T) { - finalized, lastEvaluatedKey, err := sharedStorage.GetBlobMetadataByStatusWithPagination(ctx, disperser.Finalized, 1, nil) - assert.NoError(t, err) - assert.Len(t, finalized, 1) - assert.Equal(t, metadata2, finalized[0]) - assert.NotNil(t, lastEvaluatedKey) - - // Fetch next page (should be empty) - nextFinalized, nextLastEvaluatedKey, err := sharedStorage.GetBlobMetadataByStatusWithPagination(ctx, disperser.Finalized, 1, lastEvaluatedKey) - assert.NoError(t, err) - assert.Len(t, nextFinalized, 0) - assert.Nil(t, nextLastEvaluatedKey) - }) - - // Cleanup: Delete test items - t.Cleanup(func() { - deleteItemsWithBackgroundContext(t, []commondynamodb.Key{ - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey1.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey1.BlobHash}, - }, - { - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey2.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey2.BlobHash}, - }, - }) - }) -} - -func TestSharedBlobStoreGetAllBlobMetadataByBatchWithPagination(t *testing.T) { - ctx := t.Context() - batchHeaderHash := [32]byte{1, 2, 3} - - // Create and store multiple blob metadata for the same batch - numBlobs := 5 - blobKeys := make([]disperser.BlobKey, numBlobs) - for i := 0; i < numBlobs; i++ { - blobKey := disperser.BlobKey{ - BlobHash: fmt.Sprintf("blob%d", i), - MetadataHash: fmt.Sprintf("hash%d", i), - } - blobKeys[i] = blobKey - - metadata := &disperser.BlobMetadata{ - BlobHash: blobKey.BlobHash, - MetadataHash: blobKey.MetadataHash, - BlobStatus: disperser.Confirmed, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: blobSize, - RequestedAt: uint64(time.Now().UnixNano()), - }, - ConfirmationInfo: &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: uint32(i), - }, - } - - err := blobMetadataStore.QueueNewBlobMetadata(ctx, metadata) - assert.NoError(t, err) - } - - // Test pagination with a page size of 2 - t.Run("Fetch All Blobs with Pagination", func(t *testing.T) { - var allFetchedMetadata []*disperser.BlobMetadata - var lastEvaluatedKey *disperser.BatchIndexExclusiveStartKey - pageSize := int32(2) - - for { - fetchedMetadata, newLastEvaluatedKey, err := sharedStorage.GetAllBlobMetadataByBatchWithPagination(ctx, batchHeaderHash, pageSize, lastEvaluatedKey) - assert.NoError(t, err) - - allFetchedMetadata = append(allFetchedMetadata, fetchedMetadata...) - - if newLastEvaluatedKey == nil { - assert.Len(t, fetchedMetadata, numBlobs%int(pageSize)) - break - } else { - assert.Len(t, fetchedMetadata, int(pageSize)) - } - lastEvaluatedKey = newLastEvaluatedKey - } - - assert.Len(t, allFetchedMetadata, numBlobs) - - // Verify that all blob metadata is fetched and in the correct order - for i, metadata := range allFetchedMetadata { - assert.Equal(t, fmt.Sprintf("blob%d", i), metadata.BlobHash) - assert.Equal(t, fmt.Sprintf("hash%d", i), metadata.MetadataHash) - assert.Equal(t, uint32(i), metadata.ConfirmationInfo.BlobIndex) - } - }) - - // Test pagination with a page size of 10 - t.Run("Fetch All Blobs with Pagination (Page Size > Num Blobs)", func(t *testing.T) { - var allFetchedMetadata []*disperser.BlobMetadata - var lastEvaluatedKey *disperser.BatchIndexExclusiveStartKey - pageSize := int32(10) - - for { - fetchedMetadata, newLastEvaluatedKey, err := sharedStorage.GetAllBlobMetadataByBatchWithPagination(ctx, batchHeaderHash, pageSize, lastEvaluatedKey) - assert.NoError(t, err) - - allFetchedMetadata = append(allFetchedMetadata, fetchedMetadata...) - - if newLastEvaluatedKey == nil { - assert.Len(t, fetchedMetadata, numBlobs) - break - } else { - assert.Len(t, fetchedMetadata, int(pageSize)) - } - - lastEvaluatedKey = newLastEvaluatedKey - } - - assert.Len(t, allFetchedMetadata, numBlobs) - - // Verify that all blob metadata is fetched and in the correct order - for i, metadata := range allFetchedMetadata { - assert.Equal(t, fmt.Sprintf("blob%d", i), metadata.BlobHash) - assert.Equal(t, fmt.Sprintf("hash%d", i), metadata.MetadataHash) - assert.Equal(t, uint32(i), metadata.ConfirmationInfo.BlobIndex) - } - }) - - // Test invalid batch header hash - t.Run("Fetch All Blobs with Invalid Batch Header Hash", func(t *testing.T) { - invalidBatchHeaderHash := [32]byte{4, 5, 6} - allFetchedMetadata, lastEvaluatedKey, err := sharedStorage.GetAllBlobMetadataByBatchWithPagination(ctx, invalidBatchHeaderHash, 10, nil) - assert.NoError(t, err) - assert.Len(t, allFetchedMetadata, 0) - assert.Nil(t, lastEvaluatedKey) - }) - - // Cleanup: Delete test items - t.Cleanup(func() { - var keys []commondynamodb.Key - for _, blobKey := range blobKeys { - keys = append(keys, commondynamodb.Key{ - "MetadataHash": &types.AttributeValueMemberS{Value: blobKey.MetadataHash}, - "BlobHash": &types.AttributeValueMemberS{Value: blobKey.BlobHash}, - }) - } - deleteItemsWithBackgroundContext(t, keys) - }) -} - -func assertMetadata(t *testing.T, blobKey disperser.BlobKey, expectedBlobSize uint, expectedRequestedAt uint64, expectedStatus disperser.BlobStatus, actualMetadata *disperser.BlobMetadata) { - t.Helper() - - assert.NotNil(t, actualMetadata) - assert.Equal(t, expectedStatus, actualMetadata.BlobStatus) - assert.Equal(t, blob.RequestHeader, actualMetadata.RequestMetadata.BlobRequestHeader) - assert.Equal(t, blobKey.BlobHash, actualMetadata.BlobHash) - assert.Equal(t, blobKey.MetadataHash, actualMetadata.MetadataHash) - assert.Equal(t, expectedBlobSize, actualMetadata.RequestMetadata.BlobSize) - assert.Equal(t, expectedRequestedAt, actualMetadata.RequestMetadata.RequestedAt) - metadataSuffix, err := metadataSuffix(t, actualMetadata.RequestMetadata.RequestedAt, - actualMetadata.RequestMetadata.SecurityParams) - assert.Nil(t, err) - assert.Equal(t, metadataSuffix, actualMetadata.MetadataHash) -} - -func assertBlob(t *testing.T, blob *core.Blob) { - t.Helper() - - assert.NotNil(t, blob) - assert.Equal(t, blob.Data, blob.Data) - assert.Equal(t, blob.RequestHeader.SecurityParams, blob.RequestHeader.SecurityParams) -} - -func metadataSuffix(t *testing.T, requestedAt uint64, securityParams []*core.SecurityParam) (string, error) { - t.Helper() - - var str string - str = fmt.Sprintf("%d/", requestedAt) - for _, param := range securityParams { - appendStr := fmt.Sprintf("%d/%d/", param.QuorumID, param.AdversaryThreshold) - // Append String incase of multiple securityParams - str = str + appendStr - } - bytes := []byte(str) - return hex.EncodeToString(sha256.New().Sum(bytes)), nil -} - -func deleteItemsWithBackgroundContext(t *testing.T, keys []commondynamodb.Key) { - t.Helper() - // Use context.Background() instead of t.Context() to avoid "context canceled" errors - // during cleanup. When tests complete or fail, t.Context() gets cancelled, which can - // interrupt database cleanup operations. - ctx := context.Background() - _, err := dynamoClient.DeleteItems(ctx, metadataTableName, keys) - assert.NoError(t, err) -} diff --git a/disperser/common/inmem/store.go b/disperser/common/inmem/store.go deleted file mode 100644 index 8814975805..0000000000 --- a/disperser/common/inmem/store.go +++ /dev/null @@ -1,380 +0,0 @@ -package inmem - -import ( - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "sort" - "strconv" - "sync" - "time" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/common" -) - -// BlobStore is an in-memory implementation of the BlobStore interface -type BlobStore struct { - mu sync.RWMutex - Blobs map[disperser.BlobHash]*BlobHolder - Metadata map[disperser.BlobKey]*disperser.BlobMetadata -} - -// BlobHolder stores the blob along with its status and any other metadata -type BlobHolder struct { - Data []byte -} - -var _ disperser.BlobStore = (*BlobStore)(nil) - -// NewBlobStore creates an empty BlobStore -func NewBlobStore() disperser.BlobStore { - return &BlobStore{ - Blobs: make(map[disperser.BlobHash]*BlobHolder), - Metadata: make(map[disperser.BlobKey]*disperser.BlobMetadata), - } -} - -func (q *BlobStore) StoreBlob(ctx context.Context, blob *core.Blob, requestedAt uint64) (disperser.BlobKey, error) { - q.mu.Lock() - defer q.mu.Unlock() - blobKey := disperser.BlobKey{} - // Generate the blob key - blobHash, err := q.getNewBlobHash() - if err != nil { - return blobKey, err - } - blobKey.BlobHash = blobHash - blobKey.MetadataHash = getMetadataHash(requestedAt) - - // Add the blob to the queue - q.Blobs[blobHash] = &BlobHolder{ - Data: blob.Data, - } - - q.Metadata[blobKey] = &disperser.BlobMetadata{ - BlobHash: blobHash, - MetadataHash: blobKey.MetadataHash, - BlobStatus: disperser.Processing, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: blob.RequestHeader, - BlobSize: uint(len(blob.Data)), - RequestedAt: requestedAt, - }, - Expiry: requestedAt + uint64(time.Hour), - } - - return blobKey, nil -} - -func (q *BlobStore) GetBlobContent(ctx context.Context, blobHash disperser.BlobHash) ([]byte, error) { - q.mu.RLock() - defer q.mu.RUnlock() - if holder, ok := q.Blobs[blobHash]; ok { - return holder.Data, nil - } else { - return nil, common.ErrBlobNotFound - } -} - -func (q *BlobStore) MarkBlobConfirmed(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationInfo *disperser.ConfirmationInfo) (*disperser.BlobMetadata, error) { - q.mu.Lock() - defer q.mu.Unlock() - // TODO (ian-shim): remove this check once we are sure that the metadata is never overwritten - refreshedMetadata, err := q.GetBlobMetadata(ctx, existingMetadata.GetBlobKey()) - if err != nil { - return nil, err - } - alreadyConfirmed, _ := refreshedMetadata.IsConfirmed() - if alreadyConfirmed { - return refreshedMetadata, nil - } - blobKey := existingMetadata.GetBlobKey() - if _, ok := q.Metadata[blobKey]; !ok { - return nil, common.ErrBlobNotFound - } - newMetadata := *existingMetadata - newMetadata.BlobStatus = disperser.Confirmed - newMetadata.ConfirmationInfo = confirmationInfo - q.Metadata[blobKey] = &newMetadata - return &newMetadata, nil -} - -func (q *BlobStore) MarkBlobDispersing(ctx context.Context, blobKey disperser.BlobKey) error { - q.mu.Lock() - defer q.mu.Unlock() - if _, ok := q.Metadata[blobKey]; !ok { - return common.ErrBlobNotFound - } - q.Metadata[blobKey].BlobStatus = disperser.Dispersing - return nil -} - -func (q *BlobStore) MarkBlobInsufficientSignatures(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationInfo *disperser.ConfirmationInfo) (*disperser.BlobMetadata, error) { - q.mu.Lock() - defer q.mu.Unlock() - blobKey := existingMetadata.GetBlobKey() - if _, ok := q.Metadata[blobKey]; !ok { - return nil, common.ErrBlobNotFound - } - newMetadata := *existingMetadata - newMetadata.BlobStatus = disperser.InsufficientSignatures - newMetadata.ConfirmationInfo = confirmationInfo - q.Metadata[blobKey] = &newMetadata - return &newMetadata, nil -} - -func (q *BlobStore) MarkBlobFinalized(ctx context.Context, blobKey disperser.BlobKey) error { - q.mu.Lock() - defer q.mu.Unlock() - if _, ok := q.Metadata[blobKey]; !ok { - return common.ErrBlobNotFound - } - - q.Metadata[blobKey].BlobStatus = disperser.Finalized - return nil -} - -func (q *BlobStore) MarkBlobProcessing(ctx context.Context, blobKey disperser.BlobKey) error { - q.mu.Lock() - defer q.mu.Unlock() - if _, ok := q.Metadata[blobKey]; !ok { - return common.ErrBlobNotFound - } - - q.Metadata[blobKey].BlobStatus = disperser.Processing - return nil -} - -func (q *BlobStore) MarkBlobFailed(ctx context.Context, blobKey disperser.BlobKey) error { - q.mu.Lock() - defer q.mu.Unlock() - if _, ok := q.Metadata[blobKey]; !ok { - return common.ErrBlobNotFound - } - - q.Metadata[blobKey].BlobStatus = disperser.Failed - return nil -} - -func (q *BlobStore) IncrementBlobRetryCount(ctx context.Context, existingMetadata *disperser.BlobMetadata) error { - q.mu.Lock() - defer q.mu.Unlock() - if _, ok := q.Metadata[existingMetadata.GetBlobKey()]; !ok { - return common.ErrBlobNotFound - } - - q.Metadata[existingMetadata.GetBlobKey()].NumRetries++ - return nil -} - -func (q *BlobStore) UpdateConfirmationBlockNumber(ctx context.Context, existingMetadata *disperser.BlobMetadata, confirmationBlockNumber uint32) error { - q.mu.Lock() - defer q.mu.Unlock() - if _, ok := q.Metadata[existingMetadata.GetBlobKey()]; !ok { - return common.ErrBlobNotFound - } - - if q.Metadata[existingMetadata.GetBlobKey()].ConfirmationInfo == nil { - return fmt.Errorf("cannot update confirmation block number for blob without confirmation info: %s", existingMetadata.GetBlobKey().String()) - } - - q.Metadata[existingMetadata.GetBlobKey()].ConfirmationInfo.ConfirmationBlockNumber = confirmationBlockNumber - return nil -} - -func (q *BlobStore) GetBlobsByMetadata(ctx context.Context, metadata []*disperser.BlobMetadata) (map[disperser.BlobKey]*core.Blob, error) { - q.mu.RLock() - defer q.mu.RUnlock() - blobs := make(map[disperser.BlobKey]*core.Blob) - for _, meta := range metadata { - if holder, ok := q.Blobs[meta.BlobHash]; ok { - blobs[meta.GetBlobKey()] = &core.Blob{ - RequestHeader: meta.RequestMetadata.BlobRequestHeader, - Data: holder.Data, - } - } else { - return nil, common.ErrBlobNotFound - } - } - return blobs, nil -} - -func (q *BlobStore) GetBlobMetadataByStatus(ctx context.Context, status disperser.BlobStatus) ([]*disperser.BlobMetadata, error) { - q.mu.RLock() - defer q.mu.RUnlock() - metas := make([]*disperser.BlobMetadata, 0) - for _, meta := range q.Metadata { - if meta.BlobStatus == status { - metas = append(metas, meta) - } - } - return metas, nil -} - -func (q *BlobStore) GetBlobMetadataByStatusWithPagination(ctx context.Context, status disperser.BlobStatus, limit int32, exclusiveStartKey *disperser.BlobStoreExclusiveStartKey) ([]*disperser.BlobMetadata, *disperser.BlobStoreExclusiveStartKey, error) { - q.mu.RLock() - defer q.mu.RUnlock() - metas := make([]*disperser.BlobMetadata, 0) - foundStart := exclusiveStartKey == nil - - keys := make([]disperser.BlobKey, len(q.Metadata)) - i := 0 - for k := range q.Metadata { - keys[i] = k - i++ - } - sort.Slice(keys, func(i, j int) bool { - return q.Metadata[keys[i]].Expiry < q.Metadata[keys[j]].Expiry - }) - for _, key := range keys { - meta := q.Metadata[key] - if meta.BlobStatus == status { - if foundStart { - metas = append(metas, meta) - if len(metas) == int(limit) { - return metas, &disperser.BlobStoreExclusiveStartKey{ - BlobStatus: int32(meta.BlobStatus), - Expiry: int64(meta.Expiry), - }, nil - } - } else if meta.BlobStatus == disperser.BlobStatus(exclusiveStartKey.BlobStatus) && meta.Expiry > uint64(exclusiveStartKey.Expiry) { - foundStart = true // Found the starting point, start appending metas from next item - metas = append(metas, meta) - if len(metas) == int(limit) { - return metas, &disperser.BlobStoreExclusiveStartKey{ - BlobStatus: int32(meta.BlobStatus), - Expiry: int64(meta.Expiry), - }, nil - } - } - } - } - - // Return all the metas if limit is not reached - return metas, nil, nil -} - -func (q *BlobStore) GetMetadataInBatch(ctx context.Context, batchHeaderHash [32]byte, blobIndex uint32) (*disperser.BlobMetadata, error) { - q.mu.RLock() - defer q.mu.RUnlock() - for _, meta := range q.Metadata { - if meta.ConfirmationInfo != nil && meta.ConfirmationInfo.BatchHeaderHash == batchHeaderHash && meta.ConfirmationInfo.BlobIndex == blobIndex { - return meta, nil - } - } - - return nil, common.ErrBlobNotFound -} - -func (q *BlobStore) GetAllBlobMetadataByBatch(ctx context.Context, batchHeaderHash [32]byte) ([]*disperser.BlobMetadata, error) { - q.mu.RLock() - defer q.mu.RUnlock() - metas := make([]*disperser.BlobMetadata, 0) - for _, meta := range q.Metadata { - if meta.ConfirmationInfo != nil && meta.ConfirmationInfo.BatchHeaderHash == batchHeaderHash { - metas = append(metas, meta) - } - } - return metas, nil -} - -func (q *BlobStore) GetAllBlobMetadataByBatchWithPagination(ctx context.Context, batchHeaderHash [32]byte, limit int32, exclusiveStartKey *disperser.BatchIndexExclusiveStartKey) ([]*disperser.BlobMetadata, *disperser.BatchIndexExclusiveStartKey, error) { - q.mu.RLock() - defer q.mu.RUnlock() - metas := make([]*disperser.BlobMetadata, 0) - foundStart := exclusiveStartKey == nil - - keys := make([]disperser.BlobKey, 0, len(q.Metadata)) - for k, v := range q.Metadata { - if v.ConfirmationInfo != nil && v.ConfirmationInfo.BatchHeaderHash == batchHeaderHash { - keys = append(keys, k) - } - } - sort.Slice(keys, func(i, j int) bool { - return q.Metadata[keys[i]].ConfirmationInfo.BlobIndex < q.Metadata[keys[j]].ConfirmationInfo.BlobIndex - }) - - for _, key := range keys { - meta := q.Metadata[key] - if foundStart { - metas = append(metas, meta) - if len(metas) == int(limit) { - return metas, &disperser.BatchIndexExclusiveStartKey{ - BatchHeaderHash: meta.ConfirmationInfo.BatchHeaderHash[:], - BlobIndex: meta.ConfirmationInfo.BlobIndex, - }, nil - } - } else if exclusiveStartKey != nil && meta.ConfirmationInfo.BlobIndex > uint32(exclusiveStartKey.BlobIndex) { - foundStart = true - metas = append(metas, meta) - if len(metas) == int(limit) { - return metas, &disperser.BatchIndexExclusiveStartKey{ - BatchHeaderHash: meta.ConfirmationInfo.BatchHeaderHash[:], - BlobIndex: meta.ConfirmationInfo.BlobIndex, - }, nil - } - } - } - - // Return all the metas if limit is not reached - return metas, nil, nil -} - -func (q *BlobStore) GetBlobMetadata(ctx context.Context, blobKey disperser.BlobKey) (*disperser.BlobMetadata, error) { - if meta, ok := q.Metadata[blobKey]; ok { - return meta, nil - } - return nil, common.ErrBlobNotFound -} - -func (q *BlobStore) GetBulkBlobMetadata(ctx context.Context, blobKeys []disperser.BlobKey) ([]*disperser.BlobMetadata, error) { - q.mu.RLock() - defer q.mu.RUnlock() - metas := make([]*disperser.BlobMetadata, len(blobKeys)) - for i, key := range blobKeys { - if meta, ok := q.Metadata[key]; ok { - metas[i] = meta - } - } - return metas, nil -} - -func (q *BlobStore) HandleBlobFailure(ctx context.Context, metadata *disperser.BlobMetadata, maxRetry uint) (bool, error) { - if metadata.NumRetries < maxRetry { - if err := q.MarkBlobProcessing(ctx, metadata.GetBlobKey()); err != nil { - return true, err - } - return true, q.IncrementBlobRetryCount(ctx, metadata) - } else { - return false, q.MarkBlobFailed(ctx, metadata.GetBlobKey()) - } -} - -// getNewBlobHash generates a new blob key -func (q *BlobStore) getNewBlobHash() (disperser.BlobHash, error) { - var key disperser.BlobHash - for { - buf := [32]byte{} - // then we can call rand.Read. - _, err := rand.Read(buf[:]) - if err != nil { - return "", err - } - - key = disperser.BlobHash(hex.EncodeToString(buf[:])) - // If the key is already in use, try again - if _, used := q.Blobs[key]; !used { - break - } - } - - return key, nil -} - -func getMetadataHash(requestedAt uint64) string { - return strconv.FormatUint(requestedAt, 10) -} diff --git a/disperser/common/inmem/store_test.go b/disperser/common/inmem/store_test.go deleted file mode 100644 index 2d6b57bc33..0000000000 --- a/disperser/common/inmem/store_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package inmem_test - -import ( - "context" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/common/inmem" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" -) - -func TestBlobStore(t *testing.T) { - bs := inmem.NewBlobStore() - numBlobs := 10 - requestedAt := uint64(time.Now().UnixNano()) - securityParams := []*core.SecurityParam{} - - ctx := context.Background() - keys := make([]disperser.BlobKey, numBlobs) - for i := 0; i < numBlobs; i++ { - blobKey, err := bs.StoreBlob(ctx, &core.Blob{ - RequestHeader: core.BlobRequestHeader{ - SecurityParams: []*core.SecurityParam{}, - }, - Data: []byte{byte(i)}, - }, requestedAt) - assert.Nil(t, err) - keys[i] = blobKey - } - - metas, err := bs.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.Nil(t, err) - assert.Len(t, metas, numBlobs) - - data, err := bs.GetBlobContent(ctx, keys[1].BlobHash) - assert.Nil(t, err) - assert.Equal(t, data, []byte{byte(1)}) - - metadatas, err := bs.GetBlobMetadataByStatus(ctx, disperser.Processing) - assert.Nil(t, err) - assert.Len(t, metadatas, numBlobs) - - blobs, err := bs.GetBlobsByMetadata(ctx, []*disperser.BlobMetadata{metadatas[2], metadatas[5]}) - assert.Nil(t, err) - assert.Len(t, blobs, 2) - blobKey1 := metadatas[2].GetBlobKey() - blobKey2 := metadatas[5].GetBlobKey() - assert.Len(t, blobs[blobKey1].Data, 1) - assert.Len(t, blobs[blobKey2].Data, 1) - - meta1, err := bs.GetBlobMetadata(ctx, blobKey1) - assert.Nil(t, err) - assert.Equal(t, meta1.BlobStatus, disperser.Processing) - meta2, err := bs.GetBlobMetadata(ctx, blobKey2) - assert.Nil(t, err) - assert.Equal(t, meta2.BlobStatus, disperser.Processing) - - batchHeaderHash := [32]byte{1, 2, 3} - blobIndex := uint32(0) - sigRecordHash := [32]byte{0} - inclusionProof := []byte{1, 2, 3, 4, 5} - - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - BlobCount: uint32(numBlobs), - SignatoryRecordHash: sigRecordHash, - ReferenceBlockNumber: 132, - BatchRoot: []byte("hello"), - BlobInclusionProof: inclusionProof, - BlobCommitment: &encoding.BlobCommitments{}, - BatchID: 99, - ConfirmationTxnHash: common.HexToHash("0x123"), - ConfirmationBlockNumber: uint32(150), - Fee: []byte{0}, - } - metadata := &disperser.BlobMetadata{ - BlobHash: meta2.BlobHash, - MetadataHash: meta2.MetadataHash, - BlobStatus: disperser.Processing, - Expiry: 0, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: securityParams, - }, - RequestedAt: requestedAt, - BlobSize: 1, - }, - } - updated, err := bs.MarkBlobConfirmed(ctx, metadata, confirmationInfo) - assert.Nil(t, err) - assert.Equal(t, disperser.Confirmed, updated.BlobStatus) - - err = bs.UpdateConfirmationBlockNumber(ctx, updated, 151) - assert.Nil(t, err) - - meta2, err = bs.GetBlobMetadata(ctx, blobKey2) - assert.Nil(t, err) - assert.Equal(t, meta2.BlobStatus, disperser.Confirmed) - assert.Equal(t, uint32(151), meta2.ConfirmationInfo.ConfirmationBlockNumber) - - meta1, err = bs.GetBlobMetadata(ctx, blobKey1) - assert.Nil(t, err) - assert.Equal(t, meta1.BlobStatus, disperser.Processing) - - err = bs.MarkBlobFailed(ctx, blobKey1) - assert.Nil(t, err) - - meta1, err = bs.GetBlobMetadata(ctx, blobKey1) - assert.Nil(t, err) - assert.Equal(t, meta1.BlobStatus, disperser.Failed) - - allMeta, err := bs.GetAllBlobMetadataByBatch(ctx, batchHeaderHash) - assert.Nil(t, err) - assert.Equal(t, 1, len(allMeta)) - assert.Equal(t, allMeta[0].BlobStatus, disperser.Confirmed) -} diff --git a/disperser/dataapi/Makefile b/disperser/dataapi/Makefile index 1dcb088c47..e6d8d48d00 100644 --- a/disperser/dataapi/Makefile +++ b/disperser/dataapi/Makefile @@ -4,18 +4,11 @@ build: test: go test -v ./... -generate-swagger-v1: - @echo " > Generating v1 swagger..." - swag init -g ../cmd/dataapi/main.go --parseDependency --output docs/v1 --instanceName V1 --packageName v1 --parseDepth 0 --exclude ./v2 --dir . - swag fmt --dir . --exclude ./v2/server_v2.go - -generate-swagger-v2: - @echo " > Generating v2 swagger..." +generate-swagger: + @echo " > Generating swagger..." swag init -g swagger.go --parseDependency --output docs/v2 --instanceName V2 --packageName v2 --dir ./v2 --parseDepth 0 swag fmt --dir ./v2 -generate-swagger: generate-swagger-v1 generate-swagger-v2 - run: build @echo " > Running dataapi..." cd .. && ./bin/dataapi \ No newline at end of file diff --git a/disperser/dataapi/blobs_handlers.go b/disperser/dataapi/blobs_handlers.go deleted file mode 100644 index 41cbfa451a..0000000000 --- a/disperser/dataapi/blobs_handlers.go +++ /dev/null @@ -1,217 +0,0 @@ -package dataapi - -import ( - "context" - "encoding/hex" - "sort" - - "github.com/Layr-Labs/eigenda/disperser" -) - -func (s *server) getBlob(ctx context.Context, key string) (*BlobMetadataResponse, error) { - s.logger.Info("Calling get blob", "key", key) - blobKey, err := disperser.ParseBlobKey(string(key)) - if err != nil { - return nil, err - } - metadata, err := s.blobstore.GetBlobMetadata(ctx, blobKey) - if err != nil { - return nil, err - } - - s.logger.Debug("Got blob metadata", "metadata", metadata) - return convertMetadataToBlobMetadataResponse(metadata) -} - -func (s *server) getBlobs(ctx context.Context, limit int) ([]*BlobMetadataResponse, error) { - _, blobMetadatas, err := s.getBlobMetadataByBatchesWithLimit(ctx, limit) - if err != nil { - return nil, err - } - if len(blobMetadatas) == 0 { - return nil, errNotFound - } - - return s.convertBlobMetadatasToBlobMetadataResponse(ctx, blobMetadatas) -} - -func (s *server) getBlobsFromBatchHeaderHash(ctx context.Context, batcherHeaderHash [32]byte, limit int, exclusiveStartKey *disperser.BatchIndexExclusiveStartKey) ([]*BlobMetadataResponse, *disperser.BatchIndexExclusiveStartKey, error) { - blobMetadatas, newExclusiveStartKey, err := s.getBlobMetadataByBatchHeaderHashWithLimit(ctx, batcherHeaderHash, int32(limit), exclusiveStartKey) - if err != nil { - return nil, nil, err - } - if len(blobMetadatas) == 0 { - return nil, nil, errNotFound - } - - responses, err := s.convertBlobMetadatasToBlobMetadataResponse(ctx, blobMetadatas) - if err != nil { - return nil, nil, err - } - - return responses, newExclusiveStartKey, nil -} - -func (s *server) convertBlobMetadatasToBlobMetadataResponse(ctx context.Context, metadatas []*disperser.BlobMetadata) ([]*BlobMetadataResponse, error) { - var ( - err error - responseMetadatas = make([]*BlobMetadataResponse, len(metadatas)) - ) - - sort.SliceStable(metadatas, func(i, j int) bool { - // We may have unconfirmed blobs to fetch, which will not have the ConfirmationInfo. - // In such case, we order them by request timestamp. - if metadatas[i].ConfirmationInfo == nil || metadatas[j].ConfirmationInfo == nil { - return metadatas[i].RequestMetadata.RequestedAt < metadatas[j].RequestMetadata.RequestedAt - } - if metadatas[i].ConfirmationInfo.BatchID != metadatas[j].ConfirmationInfo.BatchID { - return metadatas[i].ConfirmationInfo.BatchID < metadatas[j].ConfirmationInfo.BatchID - } - return metadatas[i].ConfirmationInfo.BlobIndex < metadatas[j].ConfirmationInfo.BlobIndex - }) - - for i := range metadatas { - responseMetadatas[i], err = convertMetadataToBlobMetadataResponse(metadatas[i]) - if err != nil { - return nil, err - } - } - - return responseMetadatas, nil -} - -func convertMetadataToBlobMetadataResponse(metadata *disperser.BlobMetadata) (*BlobMetadataResponse, error) { - // If the blob is not confirmed or finalized, return the metadata without the confirmation info - isConfirmed, err := metadata.IsConfirmed() - if err != nil { - return nil, err - } - if !isConfirmed { - return &BlobMetadataResponse{ - BlobKey: metadata.GetBlobKey().String(), - SecurityParams: metadata.RequestMetadata.SecurityParams, - RequestAt: ConvertNanosecondToSecond(metadata.RequestMetadata.RequestedAt), - BlobStatus: metadata.BlobStatus, - }, nil - } - - return &BlobMetadataResponse{ - BlobKey: metadata.GetBlobKey().String(), - BatchHeaderHash: hex.EncodeToString(metadata.ConfirmationInfo.BatchHeaderHash[:]), - BlobIndex: metadata.ConfirmationInfo.BlobIndex, - SignatoryRecordHash: hex.EncodeToString(metadata.ConfirmationInfo.SignatoryRecordHash[:]), - ReferenceBlockNumber: metadata.ConfirmationInfo.ReferenceBlockNumber, - BatchRoot: hex.EncodeToString(metadata.ConfirmationInfo.BatchRoot), - BlobInclusionProof: hex.EncodeToString(metadata.ConfirmationInfo.BlobInclusionProof), - BlobCommitment: metadata.ConfirmationInfo.BlobCommitment, - BatchId: metadata.ConfirmationInfo.BatchID, - ConfirmationBlockNumber: metadata.ConfirmationInfo.ConfirmationBlockNumber, - ConfirmationTxnHash: metadata.ConfirmationInfo.ConfirmationTxnHash.String(), - Fee: hex.EncodeToString(metadata.ConfirmationInfo.Fee), - SecurityParams: metadata.RequestMetadata.SecurityParams, - RequestAt: ConvertNanosecondToSecond(metadata.RequestMetadata.RequestedAt), - BlobStatus: metadata.BlobStatus, - }, nil -} - -func (s *server) getBlobMetadataByBatchesWithLimit(ctx context.Context, limit int) ([]*Batch, []*disperser.BlobMetadata, error) { - var ( - blobMetadatas = make([]*disperser.BlobMetadata, 0) - batches = make([]*Batch, 0) - blobKeyPresence = make(map[string]struct{}) - batchPresence = make(map[string]struct{}) - ) - - for skip := 0; len(blobMetadatas) < limit && skip < limit; skip += maxQueryBatchesLimit { - batchesWithLimit, err := s.subgraphClient.QueryBatchesWithLimit(ctx, maxQueryBatchesLimit, skip) - if err != nil { - s.logger.Error("Failed to query batches", "error", err) - return nil, nil, err - } - - if len(batchesWithLimit) == 0 { - break - } - - for i := range batchesWithLimit { - s.logger.Debug("Getting blob metadata", "batchHeaderHash", batchesWithLimit[i].BatchHeaderHash) - var ( - batch = batchesWithLimit[i] - ) - if batch == nil { - continue - } - batchHeaderHash, err := ConvertHexadecimalToBytes(batch.BatchHeaderHash) - if err != nil { - s.logger.Error("Failed to convert batch header hash to hex string", "error", err) - continue - } - batchKey := string(batchHeaderHash[:]) - if _, found := batchPresence[batchKey]; !found { - batchPresence[batchKey] = struct{}{} - } else { - // The batch has processed, skip it. - s.logger.Error("Getting duplicate batch from the graph", "batch header hash", batchKey) - continue - } - - metadatas, err := s.blobstore.GetAllBlobMetadataByBatch(ctx, batchHeaderHash) - if err != nil { - s.logger.Error("Failed to get blob metadata", "error", err) - continue - } - for _, bm := range metadatas { - blobKey := bm.GetBlobKey().String() - if _, found := blobKeyPresence[blobKey]; !found { - blobKeyPresence[blobKey] = struct{}{} - blobMetadatas = append(blobMetadatas, bm) - } else { - s.logger.Error("Getting duplicate blob key from the blobstore", "blobkey", blobKey) - } - } - batches = append(batches, batch) - if len(blobMetadatas) >= limit { - break - } - } - } - - if len(blobMetadatas) >= limit { - blobMetadatas = blobMetadatas[:limit] - } - - return batches, blobMetadatas, nil -} - -func (s *server) getBlobMetadataByBatchHeaderHashWithLimit(ctx context.Context, batchHeaderHash [32]byte, limit int32, exclusiveStartKey *disperser.BatchIndexExclusiveStartKey) ([]*disperser.BlobMetadata, *disperser.BatchIndexExclusiveStartKey, error) { - var allMetadata []*disperser.BlobMetadata - nextKey := exclusiveStartKey - - const maxLimit int32 = 1000 - remainingLimit := min(limit, maxLimit) - - s.logger.Debug("Getting blob metadata by batch header hash", "batchHeaderHash", batchHeaderHash, "remainingLimit", remainingLimit, "nextKey", nextKey) - for int32(len(allMetadata)) < remainingLimit { - metadatas, newNextKey, err := s.blobstore.GetAllBlobMetadataByBatchWithPagination(ctx, batchHeaderHash, remainingLimit-int32(len(allMetadata)), nextKey) - if err != nil { - s.logger.Error("Failed to get blob metadata", "error", err) - return nil, nil, err - } - - allMetadata = append(allMetadata, metadatas...) - - if newNextKey == nil { - // No more data to fetch - return allMetadata, nil, nil - } - - nextKey = newNextKey - - if int32(len(allMetadata)) == remainingLimit { - // We've reached the limit - break - } - } - - return allMetadata, nextKey, nil -} diff --git a/disperser/dataapi/config.go b/disperser/dataapi/config.go index be200f9a22..c22bd1f054 100644 --- a/disperser/dataapi/config.go +++ b/disperser/dataapi/config.go @@ -1,17 +1,9 @@ package dataapi type Config struct { - SocketAddr string - ServerMode string - AllowOrigins []string - DisperserHostname string - ChurnerHostname string - BatcherHealthEndpt string + SocketAddr string + ServerMode string + AllowOrigins []string + DisperserHostname string + ChurnerHostname string } - -type DataApiVersion uint - -const ( - V1 DataApiVersion = 1 - V2 DataApiVersion = 2 -) diff --git a/disperser/dataapi/docs/v1/V1_docs.go b/disperser/dataapi/docs/v1/V1_docs.go deleted file mode 100644 index dc712db841..0000000000 --- a/disperser/dataapi/docs/v1/V1_docs.go +++ /dev/null @@ -1,1254 +0,0 @@ -// Package v1 Code generated by swaggo/swag. DO NOT EDIT -package v1 - -import "github.com/swaggo/swag" - -const docTemplateV1 = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": {}, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/feed/batches/{batch_header_hash}/blobs": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Feed" - ], - "summary": "Fetch blob metadata by batch header hash", - "parameters": [ - { - "type": "string", - "description": "Batch Header Hash", - "name": "batch_header_hash", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Limit [default: 10]", - "name": "limit", - "in": "query" - }, - { - "type": "string", - "description": "Next page token", - "name": "next_token", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.BlobsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/feed/blobs": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Feed" - ], - "summary": "Fetch blobs metadata list", - "parameters": [ - { - "type": "integer", - "description": "Limit [default: 10]", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.BlobsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/feed/blobs/{blob_key}": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Feed" - ], - "summary": "Fetch blob metadata by blob key", - "parameters": [ - { - "type": "string", - "description": "Blob Key", - "name": "blob_key", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.BlobMetadataResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch metrics", - "parameters": [ - { - "type": "integer", - "description": "Start unix timestamp [default: 1 hour ago]", - "name": "start", - "in": "query" - }, - { - "type": "integer", - "description": "End unix timestamp [default: unix time now]", - "name": "end", - "in": "query" - }, - { - "type": "integer", - "description": "Limit [default: 10]", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.Metric" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/batcher-service-availability": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Batcher Availability" - ], - "summary": "Get status of EigenDA batcher.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.ServiceAvailabilityResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/churner-service-availability": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Churner ServiceAvailability" - ], - "summary": "Get status of EigenDA churner service.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.ServiceAvailabilityResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/disperser-service-availability": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "ServiceAvailability" - ], - "summary": "Get status of EigenDA Disperser service.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.ServiceAvailabilityResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/non-signers": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch non signers", - "parameters": [ - { - "type": "integer", - "description": "Interval to query for non signers in seconds [default: 3600]", - "name": "interval", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.NonSigner" - } - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/operator-nonsigning-percentage": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch operators non signing percentage", - "parameters": [ - { - "type": "integer", - "description": "Interval to query for operators nonsigning percentage [default: 3600]", - "name": "interval", - "in": "query" - }, - { - "type": "string", - "description": "End time (2006-01-02T15:04:05Z) to query for operators nonsigning percentage [default: now]", - "name": "end", - "in": "query" - }, - { - "type": "string", - "description": "Whether return only live nonsigners [default: true]", - "name": "live_only", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.OperatorsNonsigningPercentage" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/throughput": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch throughput time series", - "parameters": [ - { - "type": "integer", - "description": "Start unix timestamp [default: 1 hour ago]", - "name": "start", - "in": "query" - }, - { - "type": "integer", - "description": "End unix timestamp [default: unix time now]", - "name": "end", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.Throughput" - } - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/deregistered-operators": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Fetch list of operators that have been deregistered for days. Days is a query parameter with a default value of 14 and max value of 30.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.QueriedStateOperatorsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/operator-ejections": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Fetch list of operator ejections over last N days.", - "parameters": [ - { - "type": "integer", - "description": "Lookback in days [default: 1]", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Operator ID filter [default: all operators]", - "name": "operator_id", - "in": "query" - }, - { - "type": "integer", - "description": "Return first N ejections [default: 1000]", - "name": "first", - "in": "query" - }, - { - "type": "integer", - "description": "Skip first N ejections [default: 0]", - "name": "skip", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.QueriedOperatorEjectionsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/operators-stake": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsStake" - ], - "summary": "Operator stake distribution query", - "parameters": [ - { - "type": "string", - "description": "Operator ID", - "name": "operator_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.OperatorsStakeResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/port-check": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Operator v1 node reachability port check", - "parameters": [ - { - "type": "string", - "description": "Operator ID", - "name": "operator_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.OperatorPortCheckResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/registered-operators": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Fetch list of operators that have been registered for days. Days is a query parameter with a default value of 14 and max value of 30.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.QueriedStateOperatorsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/semver-scan": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Active operator semver scan", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.SemverReportResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - } - }, - "definitions": { - "big.Int": { - "type": "object" - }, - "core.SecurityParam": { - "type": "object", - "properties": { - "adversaryThreshold": { - "description": "AdversaryThreshold is the maximum amount of stake that can be controlled by an adversary in the quorum as a percentage of the total stake in the quorum", - "type": "integer" - }, - "confirmationThreshold": { - "description": "ConfirmationThreshold is the amount of stake that must sign a message for it to be considered valid as a percentage of the total stake in the quorum", - "type": "integer" - }, - "quorumID": { - "type": "integer" - }, - "quorumRate": { - "description": "Rate Limit. This is a temporary measure until the node can derive rates on its own using rollup authentication. This is used\nfor restricting the rate at which retrievers are able to download data from the DA node to a multiple of the rate at which the\ndata was posted to the DA node.", - "type": "integer" - } - } - }, - "dataapi.BlobMetadataResponse": { - "type": "object", - "properties": { - "batch_header_hash": { - "type": "string" - }, - "batch_id": { - "type": "integer" - }, - "batch_root": { - "type": "string" - }, - "blob_commitment": { - "$ref": "#/definitions/encoding.BlobCommitments" - }, - "blob_inclusion_proof": { - "type": "string" - }, - "blob_index": { - "type": "integer" - }, - "blob_key": { - "type": "string" - }, - "blob_status": { - "$ref": "#/definitions/github_com_Layr-Labs_eigenda_disperser.BlobStatus" - }, - "confirmation_block_number": { - "type": "integer" - }, - "confirmation_txn_hash": { - "type": "string" - }, - "fee": { - "type": "string" - }, - "reference_block_number": { - "type": "integer" - }, - "requested_at": { - "type": "integer" - }, - "security_params": { - "type": "array", - "items": { - "$ref": "#/definitions/core.SecurityParam" - } - }, - "signatory_record_hash": { - "type": "string" - } - } - }, - "dataapi.BlobsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.BlobMetadataResponse" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.ErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - }, - "dataapi.Meta": { - "type": "object", - "properties": { - "next_token": { - "type": "string" - }, - "size": { - "type": "integer" - } - } - }, - "dataapi.Metric": { - "type": "object", - "properties": { - "cost_in_gas": { - "type": "number" - }, - "throughput": { - "type": "number" - }, - "total_stake": { - "description": "deprecated: use TotalStakePerQuorum instead. Remove when the frontend is updated.", - "allOf": [ - { - "$ref": "#/definitions/big.Int" - } - ] - }, - "total_stake_per_quorum": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/big.Int" - } - } - } - }, - "dataapi.NonSigner": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "operatorId": { - "type": "string" - } - } - }, - "dataapi.OperatorNonsigningPercentageMetrics": { - "type": "object", - "properties": { - "operator_address": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "percentage": { - "type": "number" - }, - "quorum_id": { - "type": "integer" - }, - "stake_percentage": { - "type": "number" - }, - "total_batches": { - "type": "integer" - }, - "total_unsigned_batches": { - "type": "integer" - } - } - }, - "dataapi.OperatorPortCheckResponse": { - "type": "object", - "properties": { - "dispersal_online": { - "type": "boolean" - }, - "dispersal_socket": { - "type": "string" - }, - "dispersal_status": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "retrieval_online": { - "type": "boolean" - }, - "retrieval_socket": { - "type": "string" - }, - "retrieval_status": { - "type": "string" - } - } - }, - "dataapi.OperatorStake": { - "type": "object", - "properties": { - "operator_address": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "quorum_id": { - "type": "string" - }, - "rank": { - "type": "integer" - }, - "stake_amount": { - "type": "number" - }, - "stake_percentage": { - "type": "number" - } - } - }, - "dataapi.OperatorsNonsigningPercentage": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.OperatorNonsigningPercentageMetrics" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.OperatorsStakeResponse": { - "type": "object", - "properties": { - "current_block": { - "type": "integer" - }, - "stake_ranked_operators": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.OperatorStake" - } - } - } - } - }, - "dataapi.QueriedOperatorEjections": { - "type": "object", - "properties": { - "block_number": { - "type": "integer" - }, - "block_timestamp": { - "type": "string" - }, - "operator_address": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "quorum": { - "type": "integer" - }, - "stake_percentage": { - "type": "number" - }, - "transaction_hash": { - "type": "string" - } - } - }, - "dataapi.QueriedOperatorEjectionsResponse": { - "type": "object", - "properties": { - "ejections": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.QueriedOperatorEjections" - } - } - } - }, - "dataapi.QueriedStateOperatorMetadata": { - "type": "object", - "properties": { - "block_number": { - "type": "integer" - }, - "is_online": { - "type": "boolean" - }, - "operator_id": { - "type": "string" - }, - "operator_process_error": { - "type": "string" - }, - "socket": { - "type": "string" - } - } - }, - "dataapi.QueriedStateOperatorsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.QueriedStateOperatorMetadata" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.SemverReportResponse": { - "type": "object", - "properties": { - "semver": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/semver.SemverMetrics" - } - } - } - }, - "dataapi.ServiceAvailability": { - "type": "object", - "properties": { - "service_name": { - "type": "string" - }, - "service_status": { - "type": "string" - } - } - }, - "dataapi.ServiceAvailabilityResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.ServiceAvailability" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.Throughput": { - "type": "object", - "properties": { - "throughput": { - "type": "number" - }, - "timestamp": { - "type": "integer" - } - } - }, - "encoding.BlobCommitments": { - "type": "object", - "properties": { - "commitment": { - "$ref": "#/definitions/encoding.G1Commitment" - }, - "length": { - "description": "this is the length in SYMBOLS (32 byte field elements) of the blob. it must be a power of 2", - "type": "integer" - }, - "length_commitment": { - "$ref": "#/definitions/encoding.G2Commitment" - }, - "length_proof": { - "$ref": "#/definitions/encoding.LengthProof" - } - } - }, - "encoding.G1Commitment": { - "type": "object", - "properties": { - "x": { - "type": "array", - "items": { - "type": "integer" - } - }, - "y": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "encoding.G2Commitment": { - "type": "object", - "properties": { - "x": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - }, - "y": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - } - } - }, - "encoding.LengthProof": { - "type": "object", - "properties": { - "x": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - }, - "y": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - } - } - }, - "github_com_Layr-Labs_eigenda_disperser.BlobStatus": { - "type": "integer", - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "x-enum-varnames": [ - "Processing", - "Confirmed", - "Failed", - "Finalized", - "InsufficientSignatures", - "Dispersing" - ] - }, - "github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2": { - "type": "object", - "properties": { - "a0": { - "type": "array", - "items": { - "type": "integer" - } - }, - "a1": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "semver.SemverMetrics": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "operators": { - "type": "array", - "items": { - "type": "string" - } - }, - "semver": { - "type": "string" - }, - "stake_percentage": { - "type": "object", - "additionalProperties": { - "type": "number" - } - } - } - } - } -}` - -// SwaggerInfoV1 holds exported Swagger Info so clients can modify it -var SwaggerInfoV1 = &swag.Spec{ - Version: "1", - Host: "", - BasePath: "", - Schemes: []string{"https", "http"}, - Title: "EigenDA Data Access API V1", - Description: "This is the EigenDA Data Access API server.", - InfoInstanceName: "V1", - SwaggerTemplate: docTemplateV1, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfoV1.InstanceName(), SwaggerInfoV1) -} diff --git a/disperser/dataapi/docs/v1/V1_swagger.json b/disperser/dataapi/docs/v1/V1_swagger.json deleted file mode 100644 index 7b0862dcb3..0000000000 --- a/disperser/dataapi/docs/v1/V1_swagger.json +++ /dev/null @@ -1,1232 +0,0 @@ -{ - "schemes": [ - "https", - "http" - ], - "swagger": "2.0", - "info": { - "description": "This is the EigenDA Data Access API server.", - "title": "EigenDA Data Access API V1", - "contact": {}, - "version": "1" - }, - "paths": { - "/feed/batches/{batch_header_hash}/blobs": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Feed" - ], - "summary": "Fetch blob metadata by batch header hash", - "parameters": [ - { - "type": "string", - "description": "Batch Header Hash", - "name": "batch_header_hash", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Limit [default: 10]", - "name": "limit", - "in": "query" - }, - { - "type": "string", - "description": "Next page token", - "name": "next_token", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.BlobsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/feed/blobs": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Feed" - ], - "summary": "Fetch blobs metadata list", - "parameters": [ - { - "type": "integer", - "description": "Limit [default: 10]", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.BlobsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/feed/blobs/{blob_key}": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Feed" - ], - "summary": "Fetch blob metadata by blob key", - "parameters": [ - { - "type": "string", - "description": "Blob Key", - "name": "blob_key", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.BlobMetadataResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch metrics", - "parameters": [ - { - "type": "integer", - "description": "Start unix timestamp [default: 1 hour ago]", - "name": "start", - "in": "query" - }, - { - "type": "integer", - "description": "End unix timestamp [default: unix time now]", - "name": "end", - "in": "query" - }, - { - "type": "integer", - "description": "Limit [default: 10]", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.Metric" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/batcher-service-availability": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Batcher Availability" - ], - "summary": "Get status of EigenDA batcher.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.ServiceAvailabilityResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/churner-service-availability": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Churner ServiceAvailability" - ], - "summary": "Get status of EigenDA churner service.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.ServiceAvailabilityResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/disperser-service-availability": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "ServiceAvailability" - ], - "summary": "Get status of EigenDA Disperser service.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.ServiceAvailabilityResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/non-signers": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch non signers", - "parameters": [ - { - "type": "integer", - "description": "Interval to query for non signers in seconds [default: 3600]", - "name": "interval", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.NonSigner" - } - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/operator-nonsigning-percentage": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch operators non signing percentage", - "parameters": [ - { - "type": "integer", - "description": "Interval to query for operators nonsigning percentage [default: 3600]", - "name": "interval", - "in": "query" - }, - { - "type": "string", - "description": "End time (2006-01-02T15:04:05Z) to query for operators nonsigning percentage [default: now]", - "name": "end", - "in": "query" - }, - { - "type": "string", - "description": "Whether return only live nonsigners [default: true]", - "name": "live_only", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.OperatorsNonsigningPercentage" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/metrics/throughput": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Metrics" - ], - "summary": "Fetch throughput time series", - "parameters": [ - { - "type": "integer", - "description": "Start unix timestamp [default: 1 hour ago]", - "name": "start", - "in": "query" - }, - { - "type": "integer", - "description": "End unix timestamp [default: unix time now]", - "name": "end", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.Throughput" - } - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/deregistered-operators": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Fetch list of operators that have been deregistered for days. Days is a query parameter with a default value of 14 and max value of 30.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.QueriedStateOperatorsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/operator-ejections": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Fetch list of operator ejections over last N days.", - "parameters": [ - { - "type": "integer", - "description": "Lookback in days [default: 1]", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Operator ID filter [default: all operators]", - "name": "operator_id", - "in": "query" - }, - { - "type": "integer", - "description": "Return first N ejections [default: 1000]", - "name": "first", - "in": "query" - }, - { - "type": "integer", - "description": "Skip first N ejections [default: 0]", - "name": "skip", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.QueriedOperatorEjectionsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/operators-stake": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsStake" - ], - "summary": "Operator stake distribution query", - "parameters": [ - { - "type": "string", - "description": "Operator ID", - "name": "operator_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.OperatorsStakeResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/port-check": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Operator v1 node reachability port check", - "parameters": [ - { - "type": "string", - "description": "Operator ID", - "name": "operator_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.OperatorPortCheckResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/registered-operators": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Fetch list of operators that have been registered for days. Days is a query parameter with a default value of 14 and max value of 30.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.QueriedStateOperatorsResponse" - } - }, - "400": { - "description": "error: Bad request", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "404": { - "description": "error: Not found", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - }, - "/operators-info/semver-scan": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "OperatorsInfo" - ], - "summary": "Active operator semver scan", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/dataapi.SemverReportResponse" - } - }, - "500": { - "description": "error: Server error", - "schema": { - "$ref": "#/definitions/dataapi.ErrorResponse" - } - } - } - } - } - }, - "definitions": { - "big.Int": { - "type": "object" - }, - "core.SecurityParam": { - "type": "object", - "properties": { - "adversaryThreshold": { - "description": "AdversaryThreshold is the maximum amount of stake that can be controlled by an adversary in the quorum as a percentage of the total stake in the quorum", - "type": "integer" - }, - "confirmationThreshold": { - "description": "ConfirmationThreshold is the amount of stake that must sign a message for it to be considered valid as a percentage of the total stake in the quorum", - "type": "integer" - }, - "quorumID": { - "type": "integer" - }, - "quorumRate": { - "description": "Rate Limit. This is a temporary measure until the node can derive rates on its own using rollup authentication. This is used\nfor restricting the rate at which retrievers are able to download data from the DA node to a multiple of the rate at which the\ndata was posted to the DA node.", - "type": "integer" - } - } - }, - "dataapi.BlobMetadataResponse": { - "type": "object", - "properties": { - "batch_header_hash": { - "type": "string" - }, - "batch_id": { - "type": "integer" - }, - "batch_root": { - "type": "string" - }, - "blob_commitment": { - "$ref": "#/definitions/encoding.BlobCommitments" - }, - "blob_inclusion_proof": { - "type": "string" - }, - "blob_index": { - "type": "integer" - }, - "blob_key": { - "type": "string" - }, - "blob_status": { - "$ref": "#/definitions/github_com_Layr-Labs_eigenda_disperser.BlobStatus" - }, - "confirmation_block_number": { - "type": "integer" - }, - "confirmation_txn_hash": { - "type": "string" - }, - "fee": { - "type": "string" - }, - "reference_block_number": { - "type": "integer" - }, - "requested_at": { - "type": "integer" - }, - "security_params": { - "type": "array", - "items": { - "$ref": "#/definitions/core.SecurityParam" - } - }, - "signatory_record_hash": { - "type": "string" - } - } - }, - "dataapi.BlobsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.BlobMetadataResponse" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.ErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - }, - "dataapi.Meta": { - "type": "object", - "properties": { - "next_token": { - "type": "string" - }, - "size": { - "type": "integer" - } - } - }, - "dataapi.Metric": { - "type": "object", - "properties": { - "cost_in_gas": { - "type": "number" - }, - "throughput": { - "type": "number" - }, - "total_stake": { - "description": "deprecated: use TotalStakePerQuorum instead. Remove when the frontend is updated.", - "allOf": [ - { - "$ref": "#/definitions/big.Int" - } - ] - }, - "total_stake_per_quorum": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/big.Int" - } - } - } - }, - "dataapi.NonSigner": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "operatorId": { - "type": "string" - } - } - }, - "dataapi.OperatorNonsigningPercentageMetrics": { - "type": "object", - "properties": { - "operator_address": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "percentage": { - "type": "number" - }, - "quorum_id": { - "type": "integer" - }, - "stake_percentage": { - "type": "number" - }, - "total_batches": { - "type": "integer" - }, - "total_unsigned_batches": { - "type": "integer" - } - } - }, - "dataapi.OperatorPortCheckResponse": { - "type": "object", - "properties": { - "dispersal_online": { - "type": "boolean" - }, - "dispersal_socket": { - "type": "string" - }, - "dispersal_status": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "retrieval_online": { - "type": "boolean" - }, - "retrieval_socket": { - "type": "string" - }, - "retrieval_status": { - "type": "string" - } - } - }, - "dataapi.OperatorStake": { - "type": "object", - "properties": { - "operator_address": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "quorum_id": { - "type": "string" - }, - "rank": { - "type": "integer" - }, - "stake_amount": { - "type": "number" - }, - "stake_percentage": { - "type": "number" - } - } - }, - "dataapi.OperatorsNonsigningPercentage": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.OperatorNonsigningPercentageMetrics" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.OperatorsStakeResponse": { - "type": "object", - "properties": { - "current_block": { - "type": "integer" - }, - "stake_ranked_operators": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.OperatorStake" - } - } - } - } - }, - "dataapi.QueriedOperatorEjections": { - "type": "object", - "properties": { - "block_number": { - "type": "integer" - }, - "block_timestamp": { - "type": "string" - }, - "operator_address": { - "type": "string" - }, - "operator_id": { - "type": "string" - }, - "quorum": { - "type": "integer" - }, - "stake_percentage": { - "type": "number" - }, - "transaction_hash": { - "type": "string" - } - } - }, - "dataapi.QueriedOperatorEjectionsResponse": { - "type": "object", - "properties": { - "ejections": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.QueriedOperatorEjections" - } - } - } - }, - "dataapi.QueriedStateOperatorMetadata": { - "type": "object", - "properties": { - "block_number": { - "type": "integer" - }, - "is_online": { - "type": "boolean" - }, - "operator_id": { - "type": "string" - }, - "operator_process_error": { - "type": "string" - }, - "socket": { - "type": "string" - } - } - }, - "dataapi.QueriedStateOperatorsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.QueriedStateOperatorMetadata" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.SemverReportResponse": { - "type": "object", - "properties": { - "semver": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/semver.SemverMetrics" - } - } - } - }, - "dataapi.ServiceAvailability": { - "type": "object", - "properties": { - "service_name": { - "type": "string" - }, - "service_status": { - "type": "string" - } - } - }, - "dataapi.ServiceAvailabilityResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/dataapi.ServiceAvailability" - } - }, - "meta": { - "$ref": "#/definitions/dataapi.Meta" - } - } - }, - "dataapi.Throughput": { - "type": "object", - "properties": { - "throughput": { - "type": "number" - }, - "timestamp": { - "type": "integer" - } - } - }, - "encoding.BlobCommitments": { - "type": "object", - "properties": { - "commitment": { - "$ref": "#/definitions/encoding.G1Commitment" - }, - "length": { - "description": "this is the length in SYMBOLS (32 byte field elements) of the blob. it must be a power of 2", - "type": "integer" - }, - "length_commitment": { - "$ref": "#/definitions/encoding.G2Commitment" - }, - "length_proof": { - "$ref": "#/definitions/encoding.LengthProof" - } - } - }, - "encoding.G1Commitment": { - "type": "object", - "properties": { - "x": { - "type": "array", - "items": { - "type": "integer" - } - }, - "y": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "encoding.G2Commitment": { - "type": "object", - "properties": { - "x": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - }, - "y": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - } - } - }, - "encoding.LengthProof": { - "type": "object", - "properties": { - "x": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - }, - "y": { - "$ref": "#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2" - } - } - }, - "github_com_Layr-Labs_eigenda_disperser.BlobStatus": { - "type": "integer", - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "x-enum-varnames": [ - "Processing", - "Confirmed", - "Failed", - "Finalized", - "InsufficientSignatures", - "Dispersing" - ] - }, - "github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2": { - "type": "object", - "properties": { - "a0": { - "type": "array", - "items": { - "type": "integer" - } - }, - "a1": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "semver.SemverMetrics": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "operators": { - "type": "array", - "items": { - "type": "string" - } - }, - "semver": { - "type": "string" - }, - "stake_percentage": { - "type": "object", - "additionalProperties": { - "type": "number" - } - } - } - } - } -} \ No newline at end of file diff --git a/disperser/dataapi/docs/v1/V1_swagger.yaml b/disperser/dataapi/docs/v1/V1_swagger.yaml deleted file mode 100644 index 23efacc282..0000000000 --- a/disperser/dataapi/docs/v1/V1_swagger.yaml +++ /dev/null @@ -1,816 +0,0 @@ -definitions: - big.Int: - type: object - core.SecurityParam: - properties: - adversaryThreshold: - description: AdversaryThreshold is the maximum amount of stake that can be - controlled by an adversary in the quorum as a percentage of the total stake - in the quorum - type: integer - confirmationThreshold: - description: ConfirmationThreshold is the amount of stake that must sign a - message for it to be considered valid as a percentage of the total stake - in the quorum - type: integer - quorumID: - type: integer - quorumRate: - description: |- - Rate Limit. This is a temporary measure until the node can derive rates on its own using rollup authentication. This is used - for restricting the rate at which retrievers are able to download data from the DA node to a multiple of the rate at which the - data was posted to the DA node. - type: integer - type: object - dataapi.BlobMetadataResponse: - properties: - batch_header_hash: - type: string - batch_id: - type: integer - batch_root: - type: string - blob_commitment: - $ref: '#/definitions/encoding.BlobCommitments' - blob_inclusion_proof: - type: string - blob_index: - type: integer - blob_key: - type: string - blob_status: - $ref: '#/definitions/github_com_Layr-Labs_eigenda_disperser.BlobStatus' - confirmation_block_number: - type: integer - confirmation_txn_hash: - type: string - fee: - type: string - reference_block_number: - type: integer - requested_at: - type: integer - security_params: - items: - $ref: '#/definitions/core.SecurityParam' - type: array - signatory_record_hash: - type: string - type: object - dataapi.BlobsResponse: - properties: - data: - items: - $ref: '#/definitions/dataapi.BlobMetadataResponse' - type: array - meta: - $ref: '#/definitions/dataapi.Meta' - type: object - dataapi.ErrorResponse: - properties: - error: - type: string - type: object - dataapi.Meta: - properties: - next_token: - type: string - size: - type: integer - type: object - dataapi.Metric: - properties: - cost_in_gas: - type: number - throughput: - type: number - total_stake: - allOf: - - $ref: '#/definitions/big.Int' - description: 'deprecated: use TotalStakePerQuorum instead. Remove when the - frontend is updated.' - total_stake_per_quorum: - additionalProperties: - $ref: '#/definitions/big.Int' - type: object - type: object - dataapi.NonSigner: - properties: - count: - type: integer - operatorId: - type: string - type: object - dataapi.OperatorNonsigningPercentageMetrics: - properties: - operator_address: - type: string - operator_id: - type: string - percentage: - type: number - quorum_id: - type: integer - stake_percentage: - type: number - total_batches: - type: integer - total_unsigned_batches: - type: integer - type: object - dataapi.OperatorPortCheckResponse: - properties: - dispersal_online: - type: boolean - dispersal_socket: - type: string - dispersal_status: - type: string - operator_id: - type: string - retrieval_online: - type: boolean - retrieval_socket: - type: string - retrieval_status: - type: string - type: object - dataapi.OperatorStake: - properties: - operator_address: - type: string - operator_id: - type: string - quorum_id: - type: string - rank: - type: integer - stake_amount: - type: number - stake_percentage: - type: number - type: object - dataapi.OperatorsNonsigningPercentage: - properties: - data: - items: - $ref: '#/definitions/dataapi.OperatorNonsigningPercentageMetrics' - type: array - meta: - $ref: '#/definitions/dataapi.Meta' - type: object - dataapi.OperatorsStakeResponse: - properties: - current_block: - type: integer - stake_ranked_operators: - additionalProperties: - items: - $ref: '#/definitions/dataapi.OperatorStake' - type: array - type: object - type: object - dataapi.QueriedOperatorEjections: - properties: - block_number: - type: integer - block_timestamp: - type: string - operator_address: - type: string - operator_id: - type: string - quorum: - type: integer - stake_percentage: - type: number - transaction_hash: - type: string - type: object - dataapi.QueriedOperatorEjectionsResponse: - properties: - ejections: - items: - $ref: '#/definitions/dataapi.QueriedOperatorEjections' - type: array - type: object - dataapi.QueriedStateOperatorMetadata: - properties: - block_number: - type: integer - is_online: - type: boolean - operator_id: - type: string - operator_process_error: - type: string - socket: - type: string - type: object - dataapi.QueriedStateOperatorsResponse: - properties: - data: - items: - $ref: '#/definitions/dataapi.QueriedStateOperatorMetadata' - type: array - meta: - $ref: '#/definitions/dataapi.Meta' - type: object - dataapi.SemverReportResponse: - properties: - semver: - additionalProperties: - $ref: '#/definitions/semver.SemverMetrics' - type: object - type: object - dataapi.ServiceAvailability: - properties: - service_name: - type: string - service_status: - type: string - type: object - dataapi.ServiceAvailabilityResponse: - properties: - data: - items: - $ref: '#/definitions/dataapi.ServiceAvailability' - type: array - meta: - $ref: '#/definitions/dataapi.Meta' - type: object - dataapi.Throughput: - properties: - throughput: - type: number - timestamp: - type: integer - type: object - encoding.BlobCommitments: - properties: - commitment: - $ref: '#/definitions/encoding.G1Commitment' - length: - description: this is the length in SYMBOLS (32 byte field elements) of the - blob. it must be a power of 2 - type: integer - length_commitment: - $ref: '#/definitions/encoding.G2Commitment' - length_proof: - $ref: '#/definitions/encoding.LengthProof' - type: object - encoding.G1Commitment: - properties: - x: - items: - type: integer - type: array - "y": - items: - type: integer - type: array - type: object - encoding.G2Commitment: - properties: - x: - $ref: '#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2' - "y": - $ref: '#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2' - type: object - encoding.LengthProof: - properties: - x: - $ref: '#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2' - "y": - $ref: '#/definitions/github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2' - type: object - github_com_Layr-Labs_eigenda_disperser.BlobStatus: - enum: - - 0 - - 1 - - 2 - - 3 - - 4 - - 5 - type: integer - x-enum-varnames: - - Processing - - Confirmed - - Failed - - Finalized - - InsufficientSignatures - - Dispersing - github_com_consensys_gnark-crypto_ecc_bn254_internal_fptower.E2: - properties: - a0: - items: - type: integer - type: array - a1: - items: - type: integer - type: array - type: object - semver.SemverMetrics: - properties: - count: - type: integer - operators: - items: - type: string - type: array - semver: - type: string - stake_percentage: - additionalProperties: - type: number - type: object - type: object -info: - contact: {} - description: This is the EigenDA Data Access API server. - title: EigenDA Data Access API V1 - version: "1" -paths: - /feed/batches/{batch_header_hash}/blobs: - get: - parameters: - - description: Batch Header Hash - in: path - name: batch_header_hash - required: true - type: string - - description: 'Limit [default: 10]' - in: query - name: limit - type: integer - - description: Next page token - in: query - name: next_token - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.BlobsResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch blob metadata by batch header hash - tags: - - Feed - /feed/blobs: - get: - parameters: - - description: 'Limit [default: 10]' - in: query - name: limit - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.BlobsResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch blobs metadata list - tags: - - Feed - /feed/blobs/{blob_key}: - get: - parameters: - - description: Blob Key - in: path - name: blob_key - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.BlobMetadataResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch blob metadata by blob key - tags: - - Feed - /metrics: - get: - parameters: - - description: 'Start unix timestamp [default: 1 hour ago]' - in: query - name: start - type: integer - - description: 'End unix timestamp [default: unix time now]' - in: query - name: end - type: integer - - description: 'Limit [default: 10]' - in: query - name: limit - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.Metric' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch metrics - tags: - - Metrics - /metrics/batcher-service-availability: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.ServiceAvailabilityResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Get status of EigenDA batcher. - tags: - - Batcher Availability - /metrics/churner-service-availability: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.ServiceAvailabilityResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Get status of EigenDA churner service. - tags: - - Churner ServiceAvailability - /metrics/disperser-service-availability: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.ServiceAvailabilityResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Get status of EigenDA Disperser service. - tags: - - ServiceAvailability - /metrics/non-signers: - get: - parameters: - - description: 'Interval to query for non signers in seconds [default: 3600]' - in: query - name: interval - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/dataapi.NonSigner' - type: array - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch non signers - tags: - - Metrics - /metrics/operator-nonsigning-percentage: - get: - parameters: - - description: 'Interval to query for operators nonsigning percentage [default: - 3600]' - in: query - name: interval - type: integer - - description: 'End time (2006-01-02T15:04:05Z) to query for operators nonsigning - percentage [default: now]' - in: query - name: end - type: string - - description: 'Whether return only live nonsigners [default: true]' - in: query - name: live_only - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.OperatorsNonsigningPercentage' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch operators non signing percentage - tags: - - Metrics - /metrics/throughput: - get: - parameters: - - description: 'Start unix timestamp [default: 1 hour ago]' - in: query - name: start - type: integer - - description: 'End unix timestamp [default: unix time now]' - in: query - name: end - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/dataapi.Throughput' - type: array - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch throughput time series - tags: - - Metrics - /operators-info/deregistered-operators: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.QueriedStateOperatorsResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch list of operators that have been deregistered for days. Days - is a query parameter with a default value of 14 and max value of 30. - tags: - - OperatorsInfo - /operators-info/operator-ejections: - get: - parameters: - - description: 'Lookback in days [default: 1]' - in: query - name: days - type: integer - - description: 'Operator ID filter [default: all operators]' - in: query - name: operator_id - type: string - - description: 'Return first N ejections [default: 1000]' - in: query - name: first - type: integer - - description: 'Skip first N ejections [default: 0]' - in: query - name: skip - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.QueriedOperatorEjectionsResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch list of operator ejections over last N days. - tags: - - OperatorsInfo - /operators-info/operators-stake: - get: - parameters: - - description: Operator ID - in: query - name: operator_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.OperatorsStakeResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Operator stake distribution query - tags: - - OperatorsStake - /operators-info/port-check: - get: - parameters: - - description: Operator ID - in: query - name: operator_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.OperatorPortCheckResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Operator v1 node reachability port check - tags: - - OperatorsInfo - /operators-info/registered-operators: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.QueriedStateOperatorsResponse' - "400": - description: 'error: Bad request' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "404": - description: 'error: Not found' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Fetch list of operators that have been registered for days. Days is - a query parameter with a default value of 14 and max value of 30. - tags: - - OperatorsInfo - /operators-info/semver-scan: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/dataapi.SemverReportResponse' - "500": - description: 'error: Server error' - schema: - $ref: '#/definitions/dataapi.ErrorResponse' - summary: Active operator semver scan - tags: - - OperatorsInfo -schemes: -- https -- http -swagger: "2.0" diff --git a/disperser/dataapi/grpc_service_availability_handler.go b/disperser/dataapi/grpc_service_availability_handler.go deleted file mode 100644 index 907d9fff20..0000000000 --- a/disperser/dataapi/grpc_service_availability_handler.go +++ /dev/null @@ -1,148 +0,0 @@ -package dataapi - -import ( - "context" - "crypto/tls" - "fmt" - "strings" - "time" - - "github.com/Layr-Labs/eigenda/core" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/health/grpc_health_v1" -) - -type GRPCConn interface { - Dial(serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) -} - -type GRPCDialerSkipTLS struct{} - -type EigenDAServiceAvailabilityCheck struct { - disperserConn *grpc.ClientConn - churnerConn *grpc.ClientConn -} - -func (s *server) getServiceAvailability(ctx context.Context, services []string) ([]*ServiceAvailability, error) { - if services == nil { - return nil, fmt.Errorf("services cannot be nil") - } - - availabilityStatuses := make([]*ServiceAvailability, len(services)) - - for i, serviceName := range services { - var availabilityStatus *ServiceAvailability - s.logger.Info("checking service health", "service", serviceName) - - response, err := s.eigenDAGRPCServiceChecker.CheckHealth(ctx, serviceName) - if err != nil { - - if err.Error() == "disperser connection is nil" { - s.logger.Error("disperser connection is nil") - availabilityStatus = &ServiceAvailability{ - ServiceName: serviceName, - ServiceStatus: grpc_health_v1.HealthCheckResponse_UNKNOWN.String(), - } - availabilityStatuses[i] = availabilityStatus - continue - } - - if err.Error() == "churner connection is nil" { - s.logger.Error("churner connection is nil") - availabilityStatus = &ServiceAvailability{ - ServiceName: serviceName, - ServiceStatus: grpc_health_v1.HealthCheckResponse_UNKNOWN.String(), - } - availabilityStatuses[i] = availabilityStatus - continue - } - - s.logger.Error("failed to check service health", "service", serviceName, "err", err) - availabilityStatus = &ServiceAvailability{ - ServiceName: serviceName, - ServiceStatus: grpc_health_v1.HealthCheckResponse_NOT_SERVING.String(), - } - availabilityStatuses[i] = availabilityStatus - } else { - s.logger.Info("service status", "service", serviceName, "status", response.GetStatus().String()) - availabilityStatus = &ServiceAvailability{ - ServiceName: serviceName, - ServiceStatus: response.GetStatus().String(), - } - availabilityStatuses[i] = availabilityStatus - } - } - return availabilityStatuses, nil -} - -func NewEigenDAServiceHealthCheck(grpcConnection GRPCConn, disperserHostName, churnerHostName string) EigenDAGRPCServiceChecker { - - // Create Pre-configured connections to the services - // Saves from having to create new connection on each request - - disperserConn, err := grpcConnection.Dial(disperserHostName, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) - - if err != nil { - return nil - } - - churnerConn, err := grpcConnection.Dial(churnerHostName, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) - - if err != nil { - return nil - } - - return &EigenDAServiceAvailabilityCheck{ - disperserConn: disperserConn, - churnerConn: churnerConn, - } -} - -// Create Connection to the service -func (rc *GRPCDialerSkipTLS) Dial(serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { - // Create client options with timeout - opts = append(opts, grpc.WithConnectParams(grpc.ConnectParams{ - MinConnectTimeout: 10 * time.Second, - })) - - return grpc.NewClient(serviceName, opts...) -} - -// CheckServiceHealth matches the HealthCheckService interface -func (sac *EigenDAServiceAvailabilityCheck) CheckHealth(ctx context.Context, serviceName string) (*grpc_health_v1.HealthCheckResponse, error) { - serviceName = strings.ToLower(serviceName) // Normalize service name to lower case. - - var client grpc_health_v1.HealthClient - - switch serviceName { - case "disperser": - - if sac.disperserConn == nil { - return nil, fmt.Errorf("disperser connection is nil") - } - client = grpc_health_v1.NewHealthClient(sac.disperserConn) - case "churner": - - if sac.churnerConn == nil { - return nil, fmt.Errorf("churner connection is nil") - } - client = grpc_health_v1.NewHealthClient(sac.churnerConn) - default: - return nil, fmt.Errorf("unsupported service: %s", serviceName) - } - - return client.Check(ctx, &grpc_health_v1.HealthCheckRequest{}) -} - -// Close Open connections -func (sac *EigenDAServiceAvailabilityCheck) CloseConnections() error { - if sac.disperserConn != nil { - core.CloseLogOnError(sac.disperserConn, "disperser connection", nil) - } - if sac.churnerConn != nil { - core.CloseLogOnError(sac.churnerConn, "churner connection", nil) - } - - return nil -} diff --git a/disperser/dataapi/http_service_availability_handler.go b/disperser/dataapi/http_service_availability_handler.go deleted file mode 100644 index 3f04651093..0000000000 --- a/disperser/dataapi/http_service_availability_handler.go +++ /dev/null @@ -1,52 +0,0 @@ -package dataapi - -import ( - "context" - "net/http" - - "github.com/Layr-Labs/eigenda/core" -) - -// Simple struct with a Service Name and its HealthEndPt. -type HttpServiceAvailabilityCheck struct { - ServiceName string - HealthEndPt string -} - -type HttpServiceAvailability struct{} - -func (s *server) getServiceHealth(ctx context.Context, services []HttpServiceAvailabilityCheck) ([]*ServiceAvailability, error) { - - availabilityStatuses := make([]*ServiceAvailability, len(services)) - for i, service := range services { - var availabilityStatus *ServiceAvailability - s.logger.Info("checking service health", "service", service.ServiceName) - - resp, err := s.eigenDAHttpServiceChecker.CheckHealth(service.HealthEndPt) - if err != nil { - s.logger.Error("Error querying service health:", "err", err) - } - - availabilityStatus = &ServiceAvailability{ - ServiceName: service.ServiceName, - ServiceStatus: resp, - } - availabilityStatuses[i] = availabilityStatus - } - return availabilityStatuses, nil -} - -// ServiceAvailability represents the status of a service. -func (sa *HttpServiceAvailability) CheckHealth(endpt string) (string, error) { - resp, err := http.Get(endpt) - if err != nil { - return "UNKNOWN", err - } - defer core.CloseLogOnError(resp.Body, "httpServiceAvailability response body", nil) - - if resp.StatusCode == http.StatusOK { - return "SERVING", nil - } - - return "NOT_SERVING", nil -} diff --git a/disperser/dataapi/metrics.go b/disperser/dataapi/metrics.go index 1a741b1f02..2e157986d9 100644 --- a/disperser/dataapi/metrics.go +++ b/disperser/dataapi/metrics.go @@ -6,8 +6,6 @@ import ( "net/http" "time" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/common/blobstore" "github.com/Layr-Labs/eigenda/disperser/common/semver" commonv2 "github.com/Layr-Labs/eigenda/disperser/common/v2" blobstorev2 "github.com/Layr-Labs/eigenda/disperser/common/v2/blobstore" @@ -43,7 +41,12 @@ type Metrics struct { logger logging.Logger } -func NewMetrics(serverVersion uint, reg *prometheus.Registry, blobMetadataStore interface{}, httpPort string, logger logging.Logger) *Metrics { +func NewMetrics( + reg *prometheus.Registry, + blobMetadataStore blobstorev2.MetadataStore, + httpPort string, + logger logging.Logger, +) *Metrics { namespace := "eigenda_dataapi" if reg == nil { panic("registry must not be nil") @@ -51,23 +54,8 @@ func NewMetrics(serverVersion uint, reg *prometheus.Registry, blobMetadataStore reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) reg.MustRegister(collectors.NewGoCollector()) - switch serverVersion { - case 1: - if store, ok := blobMetadataStore.(*blobstore.BlobMetadataStore); ok { - reg.MustRegister(NewDynamoDBCollector(store, logger)) - } else { - // Skip registering metrics if the store is not a blobstore.BlobMetadataStore - logger.Warn("blobMetadataStore is not a blobstore.BlobMetadataStore") - } - case 2: - if store, ok := blobMetadataStore.(blobstorev2.MetadataStore); ok { - reg.MustRegister(NewBlobMetadataStoreV2Collector(store, reg, logger)) - } else { - // Skip registering metrics if the store is not a blobstorev2.MetadataStore - logger.Warn("blobMetadataStore is not a blobstorev2.MetadataStore") - } - default: - panic(fmt.Sprintf("unsupported server version %d", serverVersion)) + if blobMetadataStore != nil { + reg.MustRegister(NewBlobMetadataStoreV2Collector(blobMetadataStore, reg, logger)) } metrics := &Metrics{ NumRequests: promauto.With(reg).NewCounterVec( @@ -241,43 +229,6 @@ func (g *Metrics) Start(ctx context.Context) { }() } -type DynamoDBCollector struct { - blobMetadataStore *blobstore.BlobMetadataStore - blobStatusMetric *prometheus.Desc - logger logging.Logger -} - -func NewDynamoDBCollector(blobMetadataStore *blobstore.BlobMetadataStore, logger logging.Logger) *DynamoDBCollector { - return &DynamoDBCollector{ - blobMetadataStore: blobMetadataStore, - blobStatusMetric: prometheus.NewDesc("dynamodb_blob_metadata_status_count", - "Number of blobs with specific status in DynamoDB", - []string{"status"}, - nil, - ), - logger: logger, - } -} - -func (collector *DynamoDBCollector) Describe(ch chan<- *prometheus.Desc) { - ch <- collector.blobStatusMetric -} - -func (collector *DynamoDBCollector) Collect(ch chan<- prometheus.Metric) { - count, err := collector.blobMetadataStore.GetBlobMetadataCountByStatus(context.Background(), disperser.Processing) - if err != nil { - collector.logger.Error("failed to get count of blob metadata by status", "err", err) - return - } - - ch <- prometheus.MustNewConstMetric( - collector.blobStatusMetric, - prometheus.GaugeValue, - float64(count), - disperser.Processing.String(), - ) -} - // BlobStatusMetrics holds the metrics for a specific blob status type BlobStatusMetrics struct { gauge prometheus.Gauge diff --git a/disperser/dataapi/metrics_handler.go b/disperser/dataapi/metrics_handler.go index 59b1023cce..8c9f0ed273 100644 --- a/disperser/dataapi/metrics_handler.go +++ b/disperser/dataapi/metrics_handler.go @@ -2,7 +2,6 @@ package dataapi import ( "context" - "errors" "time" ) @@ -15,24 +14,17 @@ const ( type MetricsHandler struct { // For accessing metrics info promClient PrometheusClient - version DataApiVersion } -func NewMetricsHandler(promClient PrometheusClient, version DataApiVersion) *MetricsHandler { +func NewMetricsHandler(promClient PrometheusClient) *MetricsHandler { return &MetricsHandler{ promClient: promClient, - version: version, } } func (mh *MetricsHandler) GetCompleteBlobSize(ctx context.Context, startTime int64, endTime int64) (*PrometheusResult, error) { - var result *PrometheusResult - var err error - if mh.version == V1 { - result, err = mh.promClient.QueryDisperserBlobSizeBytesPerSecond(ctx, time.Unix(startTime, 0), time.Unix(endTime, 0)) - } else { - result, err = mh.promClient.QueryDisperserBlobSizeBytesPerSecondV2(ctx, time.Unix(startTime, 0), time.Unix(endTime, 0)) - } + result, err := mh.promClient.QueryDisperserBlobSizeBytesPerSecondV2( + ctx, time.Unix(startTime, 0), time.Unix(endTime, 0)) if err != nil { return nil, err } @@ -40,13 +32,8 @@ func (mh *MetricsHandler) GetCompleteBlobSize(ctx context.Context, startTime int } func (mh *MetricsHandler) GetAvgThroughput(ctx context.Context, startTime int64, endTime int64) (float64, error) { - var result *PrometheusResult - var err error - if mh.version == V1 { - result, err = mh.promClient.QueryDisperserBlobSizeBytesPerSecond(ctx, time.Unix(startTime, 0), time.Unix(endTime, 0)) - } else { - result, err = mh.promClient.QueryDisperserBlobSizeBytesPerSecondV2(ctx, time.Unix(startTime, 0), time.Unix(endTime, 0)) - } + result, err := mh.promClient.QueryDisperserBlobSizeBytesPerSecondV2( + ctx, time.Unix(startTime, 0), time.Unix(endTime, 0)) if err != nil { return 0, err } @@ -60,10 +47,6 @@ func (mh *MetricsHandler) GetAvgThroughput(ctx context.Context, startTime int64, } func (mh *MetricsHandler) GetQuorumSigningRateTimeseries(ctx context.Context, startTime time.Time, endTime time.Time, quorumID uint8) (*PrometheusResult, error) { - if mh.version != V2 { - return nil, errors.New("only V2 signing rate fetch is supported") - } - result, err := mh.promClient.QueryQuorumNetworkSigningRateV2(ctx, startTime, endTime, quorumID) if err != nil { return nil, err @@ -81,15 +64,8 @@ func (mh *MetricsHandler) GetThroughputTimeseries(ctx context.Context, startTime // Adjust start time to account for rate interval skipping adjustedStartTime := startTime - int64(throughputRateSecs) - var result *PrometheusResult - var err error - if mh.version == V1 { - result, err = mh.promClient.QueryDisperserAvgThroughputBlobSizeBytes( - ctx, time.Unix(adjustedStartTime, 0), time.Unix(endTime, 0), throughputRateSecs) - } else { - result, err = mh.promClient.QueryDisperserAvgThroughputBlobSizeBytesV2( - ctx, time.Unix(adjustedStartTime, 0), time.Unix(endTime, 0), throughputRateSecs) - } + result, err := mh.promClient.QueryDisperserAvgThroughputBlobSizeBytesV2( + ctx, time.Unix(adjustedStartTime, 0), time.Unix(endTime, 0), throughputRateSecs) if err != nil { return nil, err diff --git a/disperser/dataapi/metrics_handlers.go b/disperser/dataapi/metrics_handlers.go deleted file mode 100644 index 5a20e81159..0000000000 --- a/disperser/dataapi/metrics_handlers.go +++ /dev/null @@ -1,121 +0,0 @@ -package dataapi - -import ( - "context" - "errors" - "fmt" - "math/big" - - "github.com/Layr-Labs/eigenda/core" -) - -func (s *server) getMetric(ctx context.Context, startTime int64, endTime int64) (*Metric, error) { - blockNumber, err := s.transactor.GetCurrentBlockNumber(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get current block number: %w", err) - } - quorumCount, err := s.transactor.GetQuorumCount(ctx, blockNumber) - if err != nil { - return nil, fmt.Errorf("failed to get quorum count: %w", err) - } - // assume quorum IDs are consequent integers starting from 0 - quorumIDs := make([]core.QuorumID, quorumCount) - for i := 0; i < int(quorumCount); i++ { - quorumIDs[i] = core.QuorumID(i) - } - - operatorState, err := s.chainState.GetOperatorState(ctx, uint(blockNumber), quorumIDs) - if err != nil { - return nil, err - } - if len(operatorState.Operators) != int(quorumCount) { - return nil, fmt.Errorf("requesting for %d quorums (quorumID=%v), but got %v", quorumCount, quorumIDs, operatorState.Operators) - } - totalStakePerQuorum := map[core.QuorumID]*big.Int{} - for quorumID, opInfoByID := range operatorState.Operators { - for _, opInfo := range opInfoByID { - if s, ok := totalStakePerQuorum[quorumID]; !ok { - totalStakePerQuorum[quorumID] = new(big.Int).Set(opInfo.Stake) - } else { - s.Add(s, opInfo.Stake) - } - } - } - - throughput, err := s.metricsHandler.GetAvgThroughput(ctx, startTime, endTime) - if err != nil { - return nil, err - } - - costInGas, err := s.calculateTotalCostGasUsed(ctx) - if err != nil { - return nil, err - } - - return &Metric{ - Throughput: throughput, - CostInGas: costInGas, - TotalStake: totalStakePerQuorum[0], - TotalStakePerQuorum: totalStakePerQuorum, - }, nil -} - -func (s *server) calculateTotalCostGasUsed(ctx context.Context) (float64, error) { - batches, err := s.subgraphClient.QueryBatchesWithLimit(ctx, 1, 0) - if err != nil { - return 0, err - } - - if len(batches) == 0 { - return 0, nil - } - - var ( - totalBlobSize uint - totalGasUsed float64 - batch = batches[0] - ) - - if batch == nil { - return 0, errors.New("error the latest batch is not valid") - } - - batchHeaderHash, err := ConvertHexadecimalToBytes(batch.BatchHeaderHash) - if err != nil { - s.logger.Error("Failed to convert BatchHeaderHash to hex string: ", "batchHeaderHash", batch.BatchHeaderHash, "err", err) - return 0, err - } - - metadatas, err := s.blobstore.GetAllBlobMetadataByBatch(ctx, batchHeaderHash) - if err != nil { - s.logger.Error("Failed to get all blob metadata by batch: ", "batchHeaderHash", batchHeaderHash, "err", err) - return 0, err - } - - for _, metadata := range metadatas { - totalBlobSize += metadata.RequestMetadata.BlobSize - } - - if uint64(totalBlobSize) > 0 { - totalGasUsed = float64(batch.GasFees.GasUsed) / float64(totalBlobSize) - } - return totalGasUsed, nil -} - -func (s *server) getNonSigners(ctx context.Context, intervalSeconds int64) (*[]NonSigner, error) { - nonSigners, err := s.subgraphClient.QueryBatchNonSigningOperatorIdsInInterval(ctx, intervalSeconds) - if err != nil { - return nil, err - } - - nonSignersObj := make([]NonSigner, 0) - for nonSigner, nonSigningAmount := range nonSigners { - s.logger.Info("NonSigner", "nonSigner", nonSigner, "nonSigningAmount", nonSigningAmount) - nonSignersObj = append(nonSignersObj, NonSigner{ - OperatorId: nonSigner, - Count: nonSigningAmount, - }) - } - - return &nonSignersObj, nil -} diff --git a/disperser/dataapi/nonsigner_handler.go b/disperser/dataapi/nonsigner_handler.go deleted file mode 100644 index 609c629dd4..0000000000 --- a/disperser/dataapi/nonsigner_handler.go +++ /dev/null @@ -1,221 +0,0 @@ -package dataapi - -import ( - "context" - "fmt" - "math/big" - "sort" - "strconv" - "strings" - - "github.com/Layr-Labs/eigenda/core" -) - -func (s *server) getOperatorNonsigningRate(ctx context.Context, startTime, endTime int64, liveOnly bool) (*OperatorsNonsigningPercentage, error) { - batches, err := s.subgraphClient.QueryBatchNonSigningInfoInInterval(ctx, startTime, endTime) - if err != nil { - return nil, err - } - if len(batches) == 0 { - return &OperatorsNonsigningPercentage{}, nil - } - - // Get the block interval of interest [startBlock, endBlock]. - startBlock := batches[0].ReferenceBlockNumber - endBlock := batches[0].ReferenceBlockNumber - for i := range batches { - if startBlock > batches[i].ReferenceBlockNumber { - startBlock = batches[i].ReferenceBlockNumber - } - if endBlock < batches[i].ReferenceBlockNumber { - endBlock = batches[i].ReferenceBlockNumber - } - } - - // Get the nonsigner (in operatorId) list. - nonsigners, err := getNonSigners(batches) - if err != nil { - return nil, err - } - if len(nonsigners) == 0 { - return &OperatorsNonsigningPercentage{}, nil - } - - // Get the address for the nonsigners (from their operatorIDs). - // nonsignerAddresses[i] is the address for nonsigners[i]. - nonsignerAddresses, err := s.transactor.BatchOperatorIDToAddress(ctx, nonsigners) - if err != nil { - return nil, err - } - - // Create a mapping from address to operatorID. - operatorList := NewOperatorList() - for i := range nonsigners { - addr := strings.ToLower(nonsignerAddresses[i].Hex()) - operatorList.Add(nonsigners[i], addr) - } - - operatorQuorumEvents, err := s.operatorHandler.subgraphClient.QueryOperatorQuorumEvent(ctx, startBlock+1, endBlock) - if err != nil { - return nil, err - } - - // Create operators' quorum intervals. - operatorQuorumIntervals, quorumIDs, err := s.operatorHandler.CreateOperatorQuorumIntervals(ctx, operatorList, operatorQuorumEvents, startBlock, endBlock) - if err != nil { - return nil, err - } - - // Compute num batches failed, where numFailed[op][q] is the number of batches - // failed to sign for operator "op" and quorum "q". - numFailed := computeNumFailed(batches, operatorQuorumIntervals) - - // Compute num batches responsible, where numResponsible[op][q] is the number of batches - // that operator "op" and quorum "q" are responsible for. - numResponsible := computeNumResponsible(batches, operatorQuorumIntervals) - - state, err := s.chainState.GetOperatorState(ctx, uint(endBlock), quorumIDs) - if err != nil { - return nil, err - } - - // Compute the nonsigning rate for each pair. - nonsignerMetrics := make([]*OperatorNonsigningPercentageMetrics, 0) - for op, val := range numResponsible { - for q, totalCount := range val { - if totalCount == 0 { - continue - } - if unsignedCount, ok := numFailed[op][q]; ok { - ps := fmt.Sprintf("%.2f", (float64(unsignedCount)/float64(totalCount))*100) - pf, err := strconv.ParseFloat(ps, 64) - if err != nil { - return nil, err - } - - opID, err := core.OperatorIDFromHex(op) - if err != nil { - return nil, err - } - - stakePercentage := float64(0) - if stake, ok := state.Operators[q][opID]; ok { - totalStake := new(big.Float).SetInt(state.Totals[q].Stake) - stakePercentage, _ = new(big.Float).Quo( - new(big.Float).SetInt(stake.Stake), - totalStake).Float64() - } else if liveOnly { - // Operator "opID" isn't live at "endBlock", skip it. - continue - } - - addr, exist := operatorList.GetAddress(op) - if !exist { - // This should never happen, but we don't fail the entire request, just - // mark error for the address field. - addr = "Unexpected internal error" - } - nonsignerMetric := OperatorNonsigningPercentageMetrics{ - OperatorId: fmt.Sprintf("0x%s", op), - OperatorAddress: addr, - QuorumId: q, - TotalUnsignedBatches: unsignedCount, - TotalBatches: totalCount, - Percentage: pf, - StakePercentage: 100 * stakePercentage, - } - nonsignerMetrics = append(nonsignerMetrics, &nonsignerMetric) - } - } - } - - // Sort by descending order of nonsigning rate. - sort.Slice(nonsignerMetrics, func(i, j int) bool { - if nonsignerMetrics[i].Percentage == nonsignerMetrics[j].Percentage { - if nonsignerMetrics[i].OperatorId == nonsignerMetrics[j].OperatorId { - return nonsignerMetrics[i].QuorumId < nonsignerMetrics[j].QuorumId - } - return nonsignerMetrics[i].OperatorId < nonsignerMetrics[j].OperatorId - } - return nonsignerMetrics[i].Percentage > nonsignerMetrics[j].Percentage - }) - - return &OperatorsNonsigningPercentage{ - Meta: Meta{ - Size: len(nonsignerMetrics), - }, - Data: nonsignerMetrics, - }, nil -} - -func getNonSigners(batches []*BatchNonSigningInfo) ([]core.OperatorID, error) { - nonsignerSet := map[string]struct{}{} - for _, b := range batches { - for _, op := range b.NonSigners { - nonsignerSet[op] = struct{}{} - } - } - nonsigners := make([]core.OperatorID, 0) - for op := range nonsignerSet { - id, err := core.OperatorIDFromHex(op) - if err != nil { - return nil, err - } - nonsigners = append(nonsigners, id) - } - sort.Slice(nonsigners, func(i, j int) bool { - for k := range nonsigners[i] { - if nonsigners[i][k] != nonsigners[j][k] { - return nonsigners[i][k] < nonsigners[j][k] - } - } - return false - }) - return nonsigners, nil -} - -func computeNumFailed(batches []*BatchNonSigningInfo, operatorQuorumIntervals OperatorQuorumIntervals) map[string]map[uint8]int { - numFailed := make(map[string]map[uint8]int) - for _, b := range batches { - for _, op := range b.NonSigners { - op := op[2:] - // Note: avg number of quorums per operator is a small number, so use brute - // force here (otherwise, we can create a map to make it more efficient) - for _, operatorQuorum := range operatorQuorumIntervals.GetQuorums(op, b.ReferenceBlockNumber) { - for _, batchQuorum := range b.QuorumNumbers { - if operatorQuorum == batchQuorum { - if _, ok := numFailed[op]; !ok { - numFailed[op] = make(map[uint8]int) - } - numFailed[op][operatorQuorum]++ - break - } - } - } - } - } - return numFailed -} - -func computeNumResponsible(batches []*BatchNonSigningInfo, operatorQuorumIntervals OperatorQuorumIntervals) map[string]map[uint8]int { - // Create quorumBatches, where quorumBatches[q].AccuBatches is the total number of - // batches in block interval [startBlock, b] for quorum "q". - quorumBatches := CreatQuorumBatches(CreateQuorumBatchMap(batches)) - - numResponsible := make(map[string]map[uint8]int) - for op, val := range operatorQuorumIntervals { - for q, intervals := range val { - numBatches := 0 - if _, ok := quorumBatches[q]; ok { - for _, interval := range intervals { - numBatches = numBatches + ComputeNumBatches(quorumBatches[q], interval.StartBlock, interval.EndBlock) - } - } - if _, ok := numResponsible[op]; !ok { - numResponsible[op] = make(map[uint8]int) - } - numResponsible[op][q] = numBatches - } - } - return numResponsible -} diff --git a/disperser/dataapi/prometheus_client.go b/disperser/dataapi/prometheus_client.go index e0fa706d8d..cb475abf17 100644 --- a/disperser/dataapi/prometheus_client.go +++ b/disperser/dataapi/prometheus_client.go @@ -21,8 +21,6 @@ const ( type ( PrometheusClient interface { - QueryDisperserBlobSizeBytesPerSecond(ctx context.Context, start time.Time, end time.Time) (*PrometheusResult, error) - QueryDisperserAvgThroughputBlobSizeBytes(ctx context.Context, start time.Time, end time.Time, windowSizeInSec uint16) (*PrometheusResult, error) QueryDisperserBlobSizeBytesPerSecondV2(ctx context.Context, start time.Time, end time.Time) (*PrometheusResult, error) QueryDisperserAvgThroughputBlobSizeBytesV2(ctx context.Context, start time.Time, end time.Time, windowSizeInSec uint16) (*PrometheusResult, error) QueryQuorumNetworkSigningRateV2(ctx context.Context, start time.Time, end time.Time, quorum uint8) (*PrometheusResult, error) @@ -49,21 +47,11 @@ func NewPrometheusClient(api prometheus.Api, cluster string) *prometheusClient { return &prometheusClient{api: api, cluster: cluster} } -func (pc *prometheusClient) QueryDisperserBlobSizeBytesPerSecond(ctx context.Context, start time.Time, end time.Time) (*PrometheusResult, error) { - query := fmt.Sprintf("eigenda_batcher_blobs_total{state=\"confirmed\",data=\"size\",cluster=\"%s\"}", pc.cluster) - return pc.queryRange(ctx, query, start, end) -} - func (pc *prometheusClient) QueryDisperserBlobSizeBytesPerSecondV2(ctx context.Context, start time.Time, end time.Time) (*PrometheusResult, error) { query := fmt.Sprintf("eigenda_dispatcher_completed_blobs_total{state=\"complete\",data=\"size\",cluster=\"%s\"}", pc.cluster) return pc.queryRange(ctx, query, start, end) } -func (pc *prometheusClient) QueryDisperserAvgThroughputBlobSizeBytes(ctx context.Context, start time.Time, end time.Time, throughputRateSecs uint16) (*PrometheusResult, error) { - query := fmt.Sprintf("avg_over_time( sum by (job) (rate(eigenda_batcher_blobs_total{state=\"confirmed\",data=\"size\",cluster=\"%s\"}[%ds])) [9m:])", pc.cluster, throughputRateSecs) - return pc.queryRange(ctx, query, start, end) -} - func (pc *prometheusClient) QueryDisperserAvgThroughputBlobSizeBytesV2(ctx context.Context, start time.Time, end time.Time, throughputRateSecs uint16) (*PrometheusResult, error) { query := fmt.Sprintf("avg_over_time( sum by (job) (rate(eigenda_dispatcher_completed_blobs_total{state=\"complete\",data=\"size\",cluster=\"%s\"}[%ds])) [9m:])", pc.cluster, throughputRateSecs) return pc.queryRange(ctx, query, start, end) diff --git a/disperser/dataapi/queried_operators_handlers.go b/disperser/dataapi/queried_operators_handlers.go index 06f4c28e56..7c34208e5d 100644 --- a/disperser/dataapi/queried_operators_handlers.go +++ b/disperser/dataapi/queried_operators_handlers.go @@ -1,227 +1,13 @@ package dataapi import ( - "context" - "math/big" "net" - "sort" - "strings" "time" "github.com/Layr-Labs/eigenda/core" "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/gammazero/workerpool" ) -type OperatorOnlineStatus struct { - OperatorInfo *Operator - IndexedOperatorInfo *core.IndexedOperatorInfo - OperatorProcessError string -} - -var ( - // TODO: Poolsize should be configurable - // Observe performance and tune accordingly - poolSize = 50 - operatorOnlineStatusresultsChan chan *QueriedStateOperatorMetadata -) - -// Function to get registered operators for given number of days -// Queries subgraph for deregistered operators -// Process operator online status -// Returns list of Operators with their online status, socket address and block number they deregistered -func (s *server) getDeregisteredOperatorForDays(ctx context.Context, days int32) ([]*QueriedStateOperatorMetadata, error) { - // Track time taken to get deregistered operators - startTime := time.Now() - - indexedDeregisteredOperatorState, err := s.subgraphClient.QueryIndexedOperatorsWithStateForTimeWindow(ctx, days, Deregistered) - if err != nil { - return nil, err - } - - // Convert the map to a slice. - operators := indexedDeregisteredOperatorState.Operators - - operatorOnlineStatusresultsChan = make(chan *QueriedStateOperatorMetadata, len(operators)) - processOperatorOnlineCheck(indexedDeregisteredOperatorState, operatorOnlineStatusresultsChan, s.logger) - - // Collect results of work done - DeregisteredOperatorMetadata := make([]*QueriedStateOperatorMetadata, 0, len(operators)) - for range operators { - metadata := <-operatorOnlineStatusresultsChan - DeregisteredOperatorMetadata = append(DeregisteredOperatorMetadata, metadata) - } - - // Log the time taken - s.logger.Info("Time taken to get deregistered operators for days", "duration", time.Since(startTime)) - sort.Slice(DeregisteredOperatorMetadata, func(i, j int) bool { - return DeregisteredOperatorMetadata[i].BlockNumber < DeregisteredOperatorMetadata[j].BlockNumber - }) - - return DeregisteredOperatorMetadata, nil -} - -// Function to get registered operators for given number of days -// Queries subgraph for registered operators -// Process operator online status -// Returns list of Operators with their online status, socket address and block number they registered -func (s *server) getRegisteredOperatorForDays(ctx context.Context, days int32) ([]*QueriedStateOperatorMetadata, error) { - // Track time taken to get registered operators - startTime := time.Now() - - indexedRegisteredOperatorState, err := s.subgraphClient.QueryIndexedOperatorsWithStateForTimeWindow(ctx, days, Registered) - if err != nil { - return nil, err - } - - // Convert the map to a slice. - operators := indexedRegisteredOperatorState.Operators - - operatorOnlineStatusresultsChan = make(chan *QueriedStateOperatorMetadata, len(operators)) - processOperatorOnlineCheck(indexedRegisteredOperatorState, operatorOnlineStatusresultsChan, s.logger) - - // Collect results of work done - RegisteredOperatorMetadata := make([]*QueriedStateOperatorMetadata, 0, len(operators)) - for range operators { - metadata := <-operatorOnlineStatusresultsChan - RegisteredOperatorMetadata = append(RegisteredOperatorMetadata, metadata) - } - - // Log the time taken - s.logger.Info("Time taken to get registered operators for days", "duration", time.Since(startTime)) - sort.Slice(RegisteredOperatorMetadata, func(i, j int) bool { - return RegisteredOperatorMetadata[i].BlockNumber < RegisteredOperatorMetadata[j].BlockNumber - }) - - return RegisteredOperatorMetadata, nil -} - -// Function to get operator ejection over last N days -// Returns list of Ejections with operatorId, quorum, block number, txn and timestamp if ejection -func (s *server) getOperatorEjections(ctx context.Context, days int32, operatorId string, first uint, skip uint) ([]*QueriedOperatorEjections, error) { - startTime := time.Now() - - operatorEjections, err := s.subgraphClient.QueryOperatorEjectionsForTimeWindow(ctx, days, operatorId, first, skip) - if err != nil { - return nil, err - } - - // create a sorted slice from the set of quorums - quorumSet := make(map[uint8]struct{}) - for _, ejection := range operatorEjections { - quorumSet[ejection.Quorum] = struct{}{} - } - quorums := make([]uint8, 0, len(quorumSet)) - for quorum := range quorumSet { - quorums = append(quorums, quorum) - } - sort.Slice(quorums, func(i, j int) bool { - return quorums[i] < quorums[j] - }) - - stateCache := make(map[uint64]*core.OperatorState) - ejectedOperatorIds := make(map[core.OperatorID]struct{}) - for _, ejection := range operatorEjections { - previouseBlock := ejection.BlockNumber - 1 - if _, exists := stateCache[previouseBlock]; !exists { - state, err := s.chainState.GetOperatorState(context.Background(), uint(previouseBlock), quorums) - if err != nil { - return nil, err - } - stateCache[previouseBlock] = state - } - - // construct a set of ejected operator ids for later batch address lookup - opID, err := core.OperatorIDFromHex(ejection.OperatorId) - if err != nil { - return nil, err - } - ejectedOperatorIds[opID] = struct{}{} - } - - // resolve operator id to operator addresses mapping - operatorIDs := make([]core.OperatorID, 0, len(ejectedOperatorIds)) - for opID := range ejectedOperatorIds { - operatorIDs = append(operatorIDs, opID) - } - operatorAddresses, err := s.transactor.BatchOperatorIDToAddress(ctx, operatorIDs) - if err != nil { - return nil, err - } - operatorIdToAddress := make(map[string]string) - for i := range operatorAddresses { - operatorIdToAddress["0x"+operatorIDs[i].Hex()] = strings.ToLower(operatorAddresses[i].Hex()) - } - - for _, ejection := range operatorEjections { - state := stateCache[ejection.BlockNumber-1] - opID, err := core.OperatorIDFromHex(ejection.OperatorId) - if err != nil { - return nil, err - } - - stakePercentage := float64(0) - if stake, ok := state.Operators[ejection.Quorum][opID]; ok { - totalStake := new(big.Float).SetInt(state.Totals[ejection.Quorum].Stake) - operatorStake := new(big.Float).SetInt(stake.Stake) - stakePercentage, _ = new(big.Float).Mul(big.NewFloat(100), new(big.Float).Quo(operatorStake, totalStake)).Float64() - } - ejection.StakePercentage = stakePercentage - ejection.OperatorAddress = operatorIdToAddress[ejection.OperatorId] - } - - s.logger.Info("Get operator ejections", "days", days, "operatorId", operatorId, "len", len(operatorEjections), "duration", time.Since(startTime)) - return operatorEjections, nil -} - -func processOperatorOnlineCheck(queriedOperatorsInfo *IndexedQueriedOperatorInfo, operatorOnlineStatusresultsChan chan<- *QueriedStateOperatorMetadata, logger logging.Logger) { - operators := queriedOperatorsInfo.Operators - wp := workerpool.New(poolSize) - - for _, operatorInfo := range operators { - operatorStatus := OperatorOnlineStatus{ - OperatorInfo: operatorInfo.Metadata, - IndexedOperatorInfo: operatorInfo.IndexedOperatorInfo, - OperatorProcessError: operatorInfo.OperatorProcessError, - } - - // Submit each operator status check to the worker pool - wp.Submit(func() { - checkIsOnlineAndProcessOperator(operatorStatus, operatorOnlineStatusresultsChan, logger) - }) - } - - wp.StopWait() // Wait for all submitted tasks to complete and stop the pool -} - -func checkIsOnlineAndProcessOperator(operatorStatus OperatorOnlineStatus, operatorOnlineStatusresultsChan chan<- *QueriedStateOperatorMetadata, logger logging.Logger) { - var isOnline bool - var socket string - if operatorStatus.IndexedOperatorInfo != nil { - socket = core.OperatorSocket(operatorStatus.IndexedOperatorInfo.Socket).GetV1RetrievalSocket() - isOnline = checkIsOperatorPortOpen(socket, 10, logger) - } - - // Log the online status - if isOnline { - logger.Debug("Operator is online", "operatorInfo", operatorStatus.IndexedOperatorInfo, "socket", socket) - } else { - logger.Debug("Operator is offline", "operatorInfo", operatorStatus.IndexedOperatorInfo, "socket", socket) - } - - // Create the metadata regardless of online status - metadata := &QueriedStateOperatorMetadata{ - OperatorId: string(operatorStatus.OperatorInfo.OperatorId[:]), - BlockNumber: uint(operatorStatus.OperatorInfo.BlockNumber), - Socket: socket, - IsOnline: isOnline, - OperatorProcessError: operatorStatus.OperatorProcessError, - } - - // Send the metadata to the results channel - operatorOnlineStatusresultsChan <- metadata -} - // Check that the socketString is invalid or unspecified (private IPs are allowed) func ValidOperatorIP(address string, logger logging.Logger) bool { host, _, err := net.SplitHostPort(address) diff --git a/disperser/dataapi/server.go b/disperser/dataapi/server.go index 194b153f99..62eec89bc1 100644 --- a/disperser/dataapi/server.go +++ b/disperser/dataapi/server.go @@ -1,97 +1,10 @@ package dataapi import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "math" - "math/big" - "net/http" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - "time" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/Layr-Labs/eigensdk-go/logging" - "google.golang.org/grpc/health/grpc_health_v1" - - "github.com/Layr-Labs/eigenda/disperser" "github.com/Layr-Labs/eigenda/disperser/common/semver" - docsv1 "github.com/Layr-Labs/eigenda/disperser/dataapi/docs/v1" - "github.com/gin-contrib/cors" - "github.com/gin-contrib/logger" - "github.com/gin-gonic/gin" - swaggerfiles "github.com/swaggo/files" // swagger embed files - ginswagger "github.com/swaggo/gin-swagger" // gin-swagger middleware -) - -const ( - maxWorkerPoolLimit = 10 - maxQueryBatchesLimit = 2 - - cacheControlParam = "Cache-Control" - - // Cache control for responses. - // The time unit is second for max age. - maxOperatorsNonsigningPercentageAge = 10 - maxOperatorPortCheckAge = 60 - maxNonSignerAge = 10 - maxDeregisteredOperatorAge = 10 - maxEjectedOperatorAge = 10 - maxThroughputAge = 10 - maxMetricAage = 10 - maxFeedBlobsAge = 10 - maxFeedBlobAge = 300 // this is completely static - maxDisperserAvailabilityAge = 3 - maxChurnerAvailabilityAge = 3 - maxBatcherAvailabilityAge = 3 - maxOperatorsStakeAge = 300 // not expect the stake change to happen frequently ) -var errNotFound = errors.New("not found") - -type EigenDAGRPCServiceChecker interface { - CheckHealth(ctx context.Context, serviceName string) (*grpc_health_v1.HealthCheckResponse, error) - CloseConnections() error -} - -type EigenDAHttpServiceChecker interface { - CheckHealth(serviceName string) (string, error) -} - type ( - BlobMetadataResponse struct { - BlobKey string `json:"blob_key"` - BatchHeaderHash string `json:"batch_header_hash"` - BlobIndex uint32 `json:"blob_index"` - SignatoryRecordHash string `json:"signatory_record_hash"` - ReferenceBlockNumber uint32 `json:"reference_block_number"` - BatchRoot string `json:"batch_root"` - BlobInclusionProof string `json:"blob_inclusion_proof"` - BlobCommitment *encoding.BlobCommitments `json:"blob_commitment"` - BatchId uint32 `json:"batch_id"` - ConfirmationBlockNumber uint32 `json:"confirmation_block_number"` - ConfirmationTxnHash string `json:"confirmation_txn_hash"` - Fee string `json:"fee"` - SecurityParams []*core.SecurityParam `json:"security_params"` - RequestAt uint64 `json:"requested_at"` - BlobStatus disperser.BlobStatus `json:"blob_status"` - } - - Metric struct { - Throughput float64 `json:"throughput"` - CostInGas float64 `json:"cost_in_gas"` - // deprecated: use TotalStakePerQuorum instead. Remove when the frontend is updated. - TotalStake *big.Int `json:"total_stake"` - TotalStakePerQuorum map[core.QuorumID]*big.Int `json:"total_stake_per_quorum"` - } - Throughput struct { Throughput float64 `json:"throughput"` Timestamp uint64 `json:"timestamp"` @@ -102,11 +15,6 @@ type ( NextToken string `json:"next_token,omitempty"` } - BlobsResponse struct { - Meta Meta `json:"meta"` - Data []*BlobMetadataResponse `json:"data"` - } - OperatorNonsigningPercentageMetrics struct { OperatorId string `json:"operator_id"` OperatorAddress string `json:"operator_address"` @@ -136,19 +44,6 @@ type ( StakeRankedOperators map[string][]*OperatorStake `json:"stake_ranked_operators"` } - QueriedStateOperatorMetadata struct { - OperatorId string `json:"operator_id"` - BlockNumber uint `json:"block_number"` - Socket string `json:"socket"` - IsOnline bool `json:"is_online"` - OperatorProcessError string `json:"operator_process_error"` - } - - QueriedStateOperatorsResponse struct { - Meta Meta `json:"meta"` - Data []*QueriedStateOperatorMetadata `json:"data"` - } - QueriedOperatorEjections struct { OperatorId string `json:"operator_id"` OperatorAddress string `json:"operator_address"` @@ -158,23 +53,6 @@ type ( TransactionHash string `json:"transaction_hash"` StakePercentage float64 `json:"stake_percentage"` } - QueriedOperatorEjectionsResponse struct { - Ejections []*QueriedOperatorEjections `json:"ejections"` - } - - ServiceAvailability struct { - ServiceName string `json:"service_name"` - ServiceStatus string `json:"service_status"` - } - - ServiceAvailabilityResponse struct { - Meta Meta `json:"meta"` - Data []*ServiceAvailability `json:"data"` - } - - OperatorPortCheckRequest struct { - OperatorId string `json:"operator_id"` - } OperatorLiveness struct { OperatorId string `json:"operator_id"` @@ -202,1000 +80,9 @@ type ( ErrorResponse struct { Error string `json:"error"` } - - server struct { - serverMode string - socketAddr string - allowOrigins []string - logger logging.Logger - blobstore disperser.BlobStore - promClient PrometheusClient - subgraphClient SubgraphClient - transactor core.Reader - chainState core.ChainState - indexedChainState core.IndexedChainState - - metrics *Metrics - disperserHostName string - churnerHostName string - batcherHealthEndpt string - eigenDAGRPCServiceChecker EigenDAGRPCServiceChecker - eigenDAHttpServiceChecker EigenDAHttpServiceChecker - - operatorHandler *OperatorHandler - metricsHandler *MetricsHandler - } ) type ServerInterface interface { Start() error Shutdown() error } - -func NewServer( - config Config, - blobstore disperser.BlobStore, - promClient PrometheusClient, - subgraphClient SubgraphClient, - transactor core.Reader, - chainState core.ChainState, - indexedChainState core.IndexedChainState, - logger logging.Logger, - metrics *Metrics, - grpcConn GRPCConn, - eigenDAGRPCServiceChecker EigenDAGRPCServiceChecker, - eigenDAHttpServiceChecker EigenDAHttpServiceChecker, - -) (*server, error) { - // Initialize the health checker service for EigenDA services - if grpcConn == nil { - grpcConn = &GRPCDialerSkipTLS{} - } - - if eigenDAGRPCServiceChecker == nil { - eigenDAGRPCServiceChecker = NewEigenDAServiceHealthCheck(grpcConn, config.DisperserHostname, config.ChurnerHostname) - } - - if eigenDAHttpServiceChecker == nil { - eigenDAHttpServiceChecker = &HttpServiceAvailability{} - } - - l := logger.With("component", "DataAPIServer") - - operatorHandler, err := NewOperatorHandler(logger, metrics, transactor, chainState, indexedChainState, subgraphClient) - if err != nil { - return nil, fmt.Errorf("failed to create operatorHandler: %w", err) - } - - return &server{ - logger: l, - serverMode: config.ServerMode, - socketAddr: config.SocketAddr, - allowOrigins: config.AllowOrigins, - blobstore: blobstore, - promClient: promClient, - subgraphClient: subgraphClient, - transactor: transactor, - chainState: chainState, - indexedChainState: indexedChainState, - metrics: metrics, - disperserHostName: config.DisperserHostname, - churnerHostName: config.ChurnerHostname, - batcherHealthEndpt: config.BatcherHealthEndpt, - eigenDAGRPCServiceChecker: eigenDAGRPCServiceChecker, - eigenDAHttpServiceChecker: eigenDAHttpServiceChecker, - operatorHandler: operatorHandler, - metricsHandler: NewMetricsHandler(promClient, V1), - }, nil -} - -func (s *server) Start() error { - if s.serverMode == gin.ReleaseMode { - // optimize performance and disable debug features. - gin.SetMode(gin.ReleaseMode) - } - - router := gin.New() - basePath := "/api/v1" - docsv1.SwaggerInfoV1.BasePath = basePath - docsv1.SwaggerInfoV1.Host = os.Getenv("SWAGGER_HOST") - v1 := router.Group(basePath) - { - feed := v1.Group("/feed") - { - feed.GET("/blobs", s.FetchBlobsHandler) - feed.GET("/blobs/:blob_key", s.FetchBlobHandler) - feed.GET("/batches/:batch_header_hash/blobs", s.FetchBlobsFromBatchHeaderHash) - } - operatorsInfo := v1.Group("/operators-info") - { - operatorsInfo.GET("/deregistered-operators", s.FetchDeregisteredOperators) - operatorsInfo.GET("/operator-ejections", s.FetchOperatorEjections) - operatorsInfo.GET("/registered-operators", s.FetchRegisteredOperators) - operatorsInfo.GET("/port-check", s.OperatorPortCheck) - operatorsInfo.GET("/semver-scan", s.SemverScan) - operatorsInfo.GET("/operators-stake", s.OperatorsStake) - } - metrics := v1.Group("/metrics") - { - metrics.GET("/", s.FetchMetricsHandler) - metrics.GET("/throughput", s.FetchMetricsThroughputHandler) - metrics.GET("/non-signers", s.FetchNonSigners) - metrics.GET("/operator-nonsigning-percentage", s.FetchOperatorsNonsigningPercentageHandler) - metrics.GET("/disperser-service-availability", s.FetchDisperserServiceAvailability) - metrics.GET("/churner-service-availability", s.FetchChurnerServiceAvailability) - metrics.GET("/batcher-service-availability", s.FetchBatcherAvailability) - } - swagger := v1.Group("/swagger") - { - swagger.GET("/*any", ginswagger.WrapHandler(swaggerfiles.Handler, ginswagger.InstanceName("V1"), ginswagger.URL("/api/v1/swagger/doc.json"))) - } - } - - router.GET("/", func(g *gin.Context) { - g.JSON(http.StatusAccepted, gin.H{"status": "OK"}) - }) - - router.Use(logger.SetLogger( - logger.WithSkipPath([]string{"/"}), - )) - - config := cors.DefaultConfig() - config.AllowOrigins = s.allowOrigins - config.AllowCredentials = true - config.AllowMethods = []string{"GET", "POST", "HEAD", "OPTIONS"} - - if s.serverMode != gin.ReleaseMode { - config.AllowOrigins = []string{"*"} - } - router.Use(cors.New(config)) - - srv := &http.Server{ - Addr: s.socketAddr, - Handler: router, - ReadTimeout: 5 * time.Second, - ReadHeaderTimeout: 5 * time.Second, - WriteTimeout: 20 * time.Second, - IdleTimeout: 120 * time.Second, - } - - errChan := run(s.logger, srv) - return <-errChan -} - -func (s *server) Shutdown() error { - - if s.eigenDAGRPCServiceChecker != nil { - err := s.eigenDAGRPCServiceChecker.CloseConnections() - - if err != nil { - s.logger.Error("Failed to close connections", "error", err) - return err - } - } - - return nil -} - -// FetchBlobHandler godoc -// -// @Summary Fetch blob metadata by blob key -// @Tags Feed -// @Produce json -// @Param blob_key path string true "Blob Key" -// @Success 200 {object} BlobMetadataResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /feed/blobs/{blob_key} [get] -func (s *server) FetchBlobHandler(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchBlob", time.Since(handlerStart)) - }() - - blobKey := c.Param("blob_key") - - metadata, err := s.getBlob(c.Request.Context(), blobKey) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlob") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchBlob") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxFeedBlobAge)) - c.JSON(http.StatusOK, metadata) -} - -// FetchBlobsFromBatchHeaderHash godoc -// -// @Summary Fetch blob metadata by batch header hash -// @Tags Feed -// @Produce json -// @Param batch_header_hash path string true "Batch Header Hash" -// @Param limit query int false "Limit [default: 10]" -// @Param next_token query string false "Next page token" -// @Success 200 {object} BlobsResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /feed/batches/{batch_header_hash}/blobs [get] -func (s *server) FetchBlobsFromBatchHeaderHash(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchBlobsFromBatchHeaderHash", time.Since(handlerStart)) - }() - - batchHeaderHash := c.Param("batch_header_hash") - batchHeaderHashBytes, err := ConvertHexadecimalToBytes([]byte(batchHeaderHash)) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("invalid batch header hash")) - return - } - - limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("invalid limit parameter")) - return - } - if limit <= 0 || limit > 1000 { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("limit must be between 0 and 1000")) - return - } - - var exclusiveStartKey *disperser.BatchIndexExclusiveStartKey - nextToken := c.Query("next_token") - if nextToken != "" { - exclusiveStartKey, err = decodeNextToken(nextToken) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("invalid next_token")) - return - } - } - - metadatas, newExclusiveStartKey, err := s.getBlobsFromBatchHeaderHash(c.Request.Context(), batchHeaderHashBytes, limit, exclusiveStartKey) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, err) - return - } - - var nextPageToken string - if newExclusiveStartKey != nil { - nextPageToken, err = encodeNextToken(newExclusiveStartKey) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("failed to generate next page token")) - return - } - } - - s.metrics.IncrementSuccessfulRequestNum("FetchBlobsFromBatchHeaderHash") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxFeedBlobAge)) - c.JSON(http.StatusOK, BlobsResponse{ - Meta: Meta{ - Size: len(metadatas), - NextToken: nextPageToken, - }, - Data: metadatas, - }) -} - -func decodeNextToken(token string) (*disperser.BatchIndexExclusiveStartKey, error) { - // Decode the base64 string - decodedBytes, err := base64.URLEncoding.DecodeString(token) - if err != nil { - return nil, fmt.Errorf("failed to decode token: %w", err) - } - - // Unmarshal the JSON into a BatchIndexExclusiveStartKey - var key disperser.BatchIndexExclusiveStartKey - err = json.Unmarshal(decodedBytes, &key) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal token: %w", err) - } - - return &key, nil -} - -func encodeNextToken(key *disperser.BatchIndexExclusiveStartKey) (string, error) { - // Marshal the key to JSON - jsonBytes, err := json.Marshal(key) - if err != nil { - return "", fmt.Errorf("failed to marshal key: %w", err) - } - - // Encode the JSON as a base64 string - token := base64.URLEncoding.EncodeToString(jsonBytes) - - return token, nil -} - -// FetchBlobsHandler godoc -// -// @Summary Fetch blobs metadata list -// @Tags Feed -// @Produce json -// @Param limit query int false "Limit [default: 10]" -// @Success 200 {object} BlobsResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /feed/blobs [get] -func (s *server) FetchBlobsHandler(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchBlobs", time.Since(handlerStart)) - }() - - limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("invalid limit parameter")) - return - } - if limit <= 0 { - s.metrics.IncrementFailedRequestNum("FetchBlobsFromBatchHeaderHash") - errorResponse(c, fmt.Errorf("limit must be greater than 0")) - return - } - - metadatas, err := s.getBlobs(c.Request.Context(), limit) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBlobs") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchBlobs") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxFeedBlobsAge)) - c.JSON(http.StatusOK, BlobsResponse{ - Meta: Meta{ - Size: len(metadatas), - }, - Data: metadatas, - }) -} - -// FetchMetricsHandler godoc -// -// @Summary Fetch metrics -// @Tags Metrics -// @Produce json -// @Param start query int false "Start unix timestamp [default: 1 hour ago]" -// @Param end query int false "End unix timestamp [default: unix time now]" -// @Param limit query int false "Limit [default: 10]" -// @Success 200 {object} Metric -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics [get] -func (s *server) FetchMetricsHandler(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchMetrics", time.Since(handlerStart)) - }() - - now := time.Now() - start, err := strconv.ParseInt(c.DefaultQuery("start", "0"), 10, 64) - if err != nil || start == 0 { - start = now.Add(-time.Hour * 1).Unix() - } - - end, err := strconv.ParseInt(c.DefaultQuery("end", "0"), 10, 64) - if err != nil || end == 0 { - end = now.Unix() - } - - metric, err := s.getMetric(c.Request.Context(), start, end) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchMetrics") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchMetrics") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxMetricAage)) - c.JSON(http.StatusOK, metric) -} - -// FetchMetricsThroughputHandler godoc -// -// @Summary Fetch throughput time series -// @Tags Metrics -// @Produce json -// @Param start query int false "Start unix timestamp [default: 1 hour ago]" -// @Param end query int false "End unix timestamp [default: unix time now]" -// @Success 200 {object} []Throughput -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics/throughput [get] -func (s *server) FetchMetricsThroughputHandler(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchMetricsTroughput", time.Since(handlerStart)) - }() - - now := time.Now() - start, err := strconv.ParseInt(c.DefaultQuery("start", "0"), 10, 64) - if err != nil || start == 0 { - start = now.Add(-time.Hour * 1).Unix() - } - - end, err := strconv.ParseInt(c.DefaultQuery("end", "0"), 10, 64) - if err != nil || end == 0 { - end = now.Unix() - } - - ths, err := s.metricsHandler.GetThroughputTimeseries(c.Request.Context(), start, end) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchMetricsTroughput") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchMetricsTroughput") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxThroughputAge)) - c.JSON(http.StatusOK, ths) -} - -// FetchNonSigners godoc -// -// @Summary Fetch non signers -// @Tags Metrics -// @Produce json -// @Param interval query int false "Interval to query for non signers in seconds [default: 3600]" -// @Success 200 {object} []NonSigner -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics/non-signers [get] -func (s *server) FetchNonSigners(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchNonSigners", time.Since(handlerStart)) - }() - - interval, err := strconv.ParseInt(c.DefaultQuery("interval", "3600"), 10, 64) - if err != nil || interval == 0 { - interval = 3600 - } - metric, err := s.getNonSigners(c.Request.Context(), interval) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchNonSigners") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchNonSigners") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxNonSignerAge)) - c.JSON(http.StatusOK, metric) -} - -// FetchOperatorsNonsigningPercentageHandler godoc -// -// @Summary Fetch operators non signing percentage -// @Tags Metrics -// @Produce json -// @Param interval query int false "Interval to query for operators nonsigning percentage [default: 3600]" -// @Param end query string false "End time (2006-01-02T15:04:05Z) to query for operators nonsigning percentage [default: now]" -// @Param live_only query string false "Whether return only live nonsigners [default: true]" -// @Success 200 {object} OperatorsNonsigningPercentage -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics/operator-nonsigning-percentage [get] -func (s *server) FetchOperatorsNonsigningPercentageHandler(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchOperatorsNonsigningPercentageHandler", time.Since(handlerStart)) - }() - - endTime := time.Now() - if c.Query("end") != "" { - - var err error - endTime, err = time.Parse("2006-01-02T15:04:05Z", c.Query("end")) - if err != nil { - errorResponse(c, err) - return - } - } - - interval, err := strconv.ParseInt(c.DefaultQuery("interval", "3600"), 10, 64) - if err != nil || interval == 0 { - interval = 3600 - } - - liveOnly := "true" - if c.Query("live_only") != "" { - liveOnly = c.Query("live_only") - if liveOnly != "true" && liveOnly != "false" { - errorResponse(c, errors.New("the live_only param must be \"true\" or \"false\"")) - return - } - } - - startTime := endTime.Add(-time.Duration(interval) * time.Second) - - metric, err := s.getOperatorNonsigningRate(c.Request.Context(), startTime.Unix(), endTime.Unix(), liveOnly == "true") - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchOperatorsNonsigningPercentageHandler") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchOperatorsNonsigningPercentageHandler") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxOperatorsNonsigningPercentageAge)) - c.JSON(http.StatusOK, metric) -} - -// OperatorsStake godoc -// -// @Summary Operator stake distribution query -// @Tags OperatorsStake -// @Produce json -// @Param operator_id query string true "Operator ID" -// @Success 200 {object} OperatorsStakeResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /operators-info/operators-stake [get] -func (s *server) OperatorsStake(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("OperatorsStake", time.Since(handlerStart)) - }() - - operatorId := c.DefaultQuery("operator_id", "") - s.logger.Info("getting operators stake distribution", "operatorId", operatorId) - - operatorsStakeResponse, err := s.operatorHandler.GetOperatorsStake(c.Request.Context(), operatorId) - if err != nil { - s.metrics.IncrementFailedRequestNum("OperatorsStake") - errorResponse(c, fmt.Errorf("failed to get operator stake: %w", err)) - return - } - - s.metrics.IncrementSuccessfulRequestNum("OperatorsStake") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxOperatorsStakeAge)) - c.JSON(http.StatusOK, operatorsStakeResponse) -} - -// FetchDeregisteredOperators godoc -// -// @Summary Fetch list of operators that have been deregistered for days. Days is a query parameter with a default value of 14 and max value of 30. -// @Tags OperatorsInfo -// @Produce json -// @Success 200 {object} QueriedStateOperatorsResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /operators-info/deregistered-operators [get] -func (s *server) FetchDeregisteredOperators(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchDeregisteredOperators", time.Since(handlerStart)) - }() - - // Get query parameters - // Default Value 14 days - days := c.DefaultQuery("days", "14") // If not specified, defaults to 14 - - // Convert days to integer - daysInt, err := strconv.Atoi(days) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'days' parameter"}) - return - } - - if daysInt > 30 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'days' parameter. Max value is 30"}) - return - } - - operatorMetadatas, err := s.getDeregisteredOperatorForDays(c.Request.Context(), int32(daysInt)) - if err != nil { - s.logger.Error("Failed to fetch deregistered operators", "error", err) - s.metrics.IncrementFailedRequestNum("FetchDeregisteredOperators") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchDeregisteredOperators") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxDeregisteredOperatorAge)) - c.JSON(http.StatusOK, QueriedStateOperatorsResponse{ - Meta: Meta{ - Size: len(operatorMetadatas), - }, - Data: operatorMetadatas, - }) -} - -// FetchRegisteredOperators godoc -// -// @Summary Fetch list of operators that have been registered for days. Days is a query parameter with a default value of 14 and max value of 30. -// @Tags OperatorsInfo -// @Produce json -// @Success 200 {object} QueriedStateOperatorsResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /operators-info/registered-operators [get] -func (s *server) FetchRegisteredOperators(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchRegisteredOperators", time.Since(handlerStart)) - }() - - // Get query parameters - // Default Value 14 days - days := c.DefaultQuery("days", "14") // If not specified, defaults to 14 - - // Convert days to integer - daysInt, err := strconv.Atoi(days) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'days' parameter"}) - return - } - - if daysInt > 30 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'days' parameter. Max value is 30"}) - return - } - - operatorMetadatas, err := s.getRegisteredOperatorForDays(c.Request.Context(), int32(daysInt)) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchRegisteredOperators") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchRegisteredOperators") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxDeregisteredOperatorAge)) - c.JSON(http.StatusOK, QueriedStateOperatorsResponse{ - Meta: Meta{ - Size: len(operatorMetadatas), - }, - Data: operatorMetadatas, - }) -} - -// FetchOperatorEjections godoc -// -// @Summary Fetch list of operator ejections over last N days. -// @Tags OperatorsInfo -// @Produce json -// @Param days query int false "Lookback in days [default: 1]" -// @Param operator_id query string false "Operator ID filter [default: all operators]" -// @Param first query int false "Return first N ejections [default: 1000]" -// @Param skip query int false "Skip first N ejections [default: 0]" -// @Success 200 {object} QueriedOperatorEjectionsResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /operators-info/operator-ejections [get] -func (s *server) FetchOperatorEjections(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchOperatorEjections", time.Since(handlerStart)) - }() - - operatorId := c.DefaultQuery("operator_id", "") // If not specified, defaults to all operators - - days := c.DefaultQuery("days", "1") // If not specified, defaults to 1 - parsedDays, err := strconv.ParseInt(days, 10, 32) - if err != nil || parsedDays < math.MinInt32 || parsedDays > math.MaxInt32 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'days' parameter"}) - return - } - daysInt := int32(parsedDays) - - first := c.DefaultQuery("first", "1000") // If not specified, defaults to 1000 - parsedFirst, err := strconv.ParseInt(first, 10, 32) - if err != nil || parsedFirst < 1 || parsedFirst > 10000 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'first' parameter. Value must be between 1..10000"}) - return - } - firstInt := int32(parsedFirst) - - skip := c.DefaultQuery("skip", "0") // If not specified, defaults to 0 - parsedSkip, err := strconv.ParseInt(skip, 10, 32) - if err != nil || parsedSkip < 0 || parsedSkip > 1000000000 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'skip' parameter. Value must be between 0..1000000000"}) - return - } - skipInt := int32(parsedSkip) - - operatorEjections, err := s.getOperatorEjections(c.Request.Context(), int32(daysInt), operatorId, uint(firstInt), uint(skipInt)) - if err != nil { - s.logger.Error("Failed to fetch ejected operators", "error", err) - s.metrics.IncrementFailedRequestNum("FetchOperatorEjections") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchOperatorEjections") - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxEjectedOperatorAge)) - c.JSON(http.StatusOK, QueriedOperatorEjectionsResponse{ - Ejections: operatorEjections, - }) -} - -// OperatorPortCheck godoc -// -// @Summary Operator v1 node reachability port check -// @Tags OperatorsInfo -// @Produce json -// @Param operator_id query string true "Operator ID" -// @Success 200 {object} OperatorPortCheckResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /operators-info/port-check [get] -func (s *server) OperatorPortCheck(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("OperatorPortCheck", time.Since(handlerStart)) - }() - - operatorId := c.DefaultQuery("operator_id", "") - s.logger.Info("checking operator ports", "operatorId", operatorId) - portCheckResponse, err := s.operatorHandler.ProbeV1OperatorPorts(c.Request.Context(), operatorId) - if err != nil { - if strings.Contains(err.Error(), "not found") { - err = errNotFound - s.logger.Warn("operator not found", "operatorId", operatorId) - s.metrics.IncrementNotFoundRequestNum("OperatorPortCheck") - } else { - s.logger.Error("operator port check failed", "error", err) - s.metrics.IncrementFailedRequestNum("OperatorPortCheck") - } - errorResponse(c, err) - return - } - - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxOperatorPortCheckAge)) - c.JSON(http.StatusOK, portCheckResponse) -} - -// Semver scan godoc -// -// @Summary Active operator semver scan -// @Tags OperatorsInfo -// @Produce json -// @Success 200 {object} SemverReportResponse -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /operators-info/semver-scan [get] -func (s *server) SemverScan(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("SemverScan", time.Since(handlerStart)) - }() - - report, err := s.operatorHandler.ScanOperatorsHostInfo(c.Request.Context()) - if err != nil { - s.logger.Error("failed to scan operators host info", "error", err) - s.metrics.IncrementFailedRequestNum("SemverScan") - errorResponse(c, err) - } - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxOperatorPortCheckAge)) - c.JSON(http.StatusOK, report) -} - -// FetchDisperserServiceAvailability godoc -// -// @Summary Get status of EigenDA Disperser service. -// @Tags ServiceAvailability -// @Produce json -// @Success 200 {object} ServiceAvailabilityResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics/disperser-service-availability [get] -func (s *server) FetchDisperserServiceAvailability(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchDisperserServiceAvailability", time.Since(handlerStart)) - }() - - // Check Disperser - services := []string{"Disperser"} - - s.logger.Info("Getting service availability for", "services", strings.Join(services, ", ")) - - availabilityStatuses, err := s.getServiceAvailability(c.Request.Context(), services) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchDisperserServiceAvailability") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchDisperserServiceAvailability") - - // Set the status code to 503 if any of the services are not serving - availabilityStatus := http.StatusOK - for _, status := range availabilityStatuses { - if status.ServiceStatus == "NOT_SERVING" { - availabilityStatus = http.StatusServiceUnavailable - break - } - - if status.ServiceStatus == "UNKNOWN" { - availabilityStatus = http.StatusInternalServerError - break - } - - } - - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxDisperserAvailabilityAge)) - c.JSON(availabilityStatus, ServiceAvailabilityResponse{ - Meta: Meta{ - Size: len(availabilityStatuses), - }, - Data: availabilityStatuses, - }) -} - -// FetchChurnerServiceAvailability godoc -// -// @Summary Get status of EigenDA churner service. -// @Tags Churner ServiceAvailability -// @Produce json -// @Success 200 {object} ServiceAvailabilityResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics/churner-service-availability [get] -func (s *server) FetchChurnerServiceAvailability(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchChurnerServiceAvailability", time.Since(handlerStart)) - }() - - // Check Disperser - services := []string{"Churner"} - - s.logger.Info("Getting service availability for", "services", strings.Join(services, ", ")) - - availabilityStatuses, err := s.getServiceAvailability(c.Request.Context(), services) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchChurnerServiceAvailability") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchChurnerServiceAvailability") - - // Set the status code to 503 if any of the services are not serving - availabilityStatus := http.StatusOK - for _, status := range availabilityStatuses { - if status.ServiceStatus == "NOT_SERVING" { - availabilityStatus = http.StatusServiceUnavailable - break - } - - if status.ServiceStatus == "UNKNOWN" { - availabilityStatus = http.StatusInternalServerError - break - } - - } - - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxChurnerAvailabilityAge)) - c.JSON(availabilityStatus, ServiceAvailabilityResponse{ - Meta: Meta{ - Size: len(availabilityStatuses), - }, - Data: availabilityStatuses, - }) -} - -// FetchBatcherAvailability godoc -// -// @Summary Get status of EigenDA batcher. -// @Tags Batcher Availability -// @Produce json -// @Success 200 {object} ServiceAvailabilityResponse -// @Failure 400 {object} ErrorResponse "error: Bad request" -// @Failure 404 {object} ErrorResponse "error: Not found" -// @Failure 500 {object} ErrorResponse "error: Server error" -// @Router /metrics/batcher-service-availability [get] -func (s *server) FetchBatcherAvailability(c *gin.Context) { - handlerStart := time.Now() - defer func() { - s.metrics.ObserveLatency("FetchBatcherAvailability", time.Since(handlerStart)) - }() - - // Check Batcher - services := []HttpServiceAvailabilityCheck{{"Batcher", s.batcherHealthEndpt}} - - s.logger.Info("Getting service availability for", "service", services[0].ServiceName, "endpoint", services[0].HealthEndPt) - - availabilityStatuses, err := s.getServiceHealth(c.Request.Context(), services) - if err != nil { - s.metrics.IncrementFailedRequestNum("FetchBatcherAvailability") - errorResponse(c, err) - return - } - - s.metrics.IncrementSuccessfulRequestNum("FetchBatcherAvailability") - - // Set the status code to 503 if any of the services are not serving - availabilityStatus := http.StatusOK - for _, status := range availabilityStatuses { - if status.ServiceStatus == "NOT_SERVING" { - availabilityStatus = http.StatusServiceUnavailable - break - } - - if status.ServiceStatus == "UNKNOWN" { - availabilityStatus = http.StatusInternalServerError - break - } - - } - - c.Writer.Header().Set(cacheControlParam, fmt.Sprintf("max-age=%d", maxBatcherAvailabilityAge)) - c.JSON(availabilityStatus, ServiceAvailabilityResponse{ - Meta: Meta{ - Size: len(availabilityStatuses), - }, - Data: availabilityStatuses, - }) -} - -func errorResponse(c *gin.Context, err error) { - _ = c.Error(err) - var code int - switch { - case errors.Is(err, errNotFound): - code = http.StatusNotFound - default: - code = http.StatusInternalServerError - } - c.JSON(code, ErrorResponse{ - Error: err.Error(), - }) -} - -func run(logger logging.Logger, httpServer *http.Server) <-chan error { - errChan := make(chan error, 1) - ctx, stop := signal.NotifyContext( - context.Background(), - os.Interrupt, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - go func() { - <-ctx.Done() - - logger.Info("shutdown signal received") - - defer func() { - stop() - close(errChan) - }() - - if err := httpServer.Shutdown(context.Background()); err != nil { - errChan <- err - } - logger.Info("shutdown completed") - }() - - go func() { - logger.Info("server running", "addr", httpServer.Addr) - if err := httpServer.ListenAndServe(); err != nil { - errChan <- err - } - }() - - return errChan -} diff --git a/disperser/dataapi/server_test.go b/disperser/dataapi/server_test.go deleted file mode 100644 index bb7dba6adc..0000000000 --- a/disperser/dataapi/server_test.go +++ /dev/null @@ -1,1602 +0,0 @@ -package dataapi_test - -import ( - "context" - _ "embed" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "log" - "math/big" - "net" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/core" - coremock "github.com/Layr-Labs/eigenda/core/mock" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/Layr-Labs/eigenda/disperser/common/inmem" - "github.com/Layr-Labs/eigenda/disperser/dataapi" - prommock "github.com/Layr-Labs/eigenda/disperser/dataapi/prometheus/mock" - "github.com/Layr-Labs/eigenda/disperser/dataapi/subgraph" - subgraphmock "github.com/Layr-Labs/eigenda/disperser/dataapi/subgraph/mock" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/Layr-Labs/eigenda/test" - "github.com/consensys/gnark-crypto/ecc/bn254/fp" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/gin-gonic/gin" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/common/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/health/grpc_health_v1" -) - -var ( - //go:embed testdata/prometheus-response-sample.json - mockPrometheusResponse string - //go:embed testdata/prometheus-resp-avg-throughput.json - mockPrometheusRespAvgThroughput string - - expectedBlobCommitment *encoding.BlobCommitments - mockLogger = test.GetLogger() - blobstore = inmem.NewBlobStore() - mockPrometheusApi = &prommock.MockPrometheusApi{} - prometheusClient = dataapi.NewPrometheusClient(mockPrometheusApi, "test-cluster") - mockSubgraphApi = &subgraphmock.MockSubgraphApi{} - subgraphClient = dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger) - - config = dataapi.Config{ServerMode: "test", SocketAddr: ":8080", AllowOrigins: []string{"*"}, DisperserHostname: "localhost:32007", ChurnerHostname: "localhost:32009"} - - serverVersion = uint(1) - mockTx = &coremock.MockWriter{} - metrics = dataapi.NewMetrics(serverVersion, prometheus.NewRegistry(), nil, "9001", mockLogger) - opId0, _ = core.OperatorIDFromHex("e22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311") - opId1, _ = core.OperatorIDFromHex("e23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312") - mockChainState, _ = coremock.NewChainDataMock(map[uint8]map[core.OperatorID]int{ - 0: { - opId0: 1, - opId1: 1, - }, - 1: { - opId0: 1, - opId1: 3, - }, - }) - mockIndexedChainState, _ = coremock.MakeChainDataMock(map[uint8]int{ - 0: 10, - 1: 10, - 2: 10, - }) - _ = mockTx.On("GetCurrentBlockNumber").Return(uint32(1), nil) - _ = mockTx.On("GetQuorumCount").Return(uint8(2), nil) - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, subgraphClient, mockTx, mockChainState, mockIndexedChainState, mockLogger, dataapi.NewMetrics(serverVersion, prometheus.NewRegistry(), nil, "9001", mockLogger), &MockGRPCConnection{}, nil, nil) - expectedRequestedAt = uint64(5567830000000000000) - expectedDataLength = uint32(32) - expectedBatchId = uint32(99) - expectedBatchRoot = []byte("hello") - expectedReferenceBlockNumber = uint32(132) - expectedConfirmationBlockNumber = uint32(150) - expectedSignatoryRecordHash = [32]byte{0} - expectedFee = []byte{0} - expectedInclusionProof = []byte{1, 2, 3, 4, 5} - gettysburgAddressBytes = []byte("Fourscore and seven years ago our fathers brought forth, on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal. Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived, and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting-place for those who here gave their lives, that that nation might live. It is altogether fitting and proper that we should do this. But, in a larger sense, we cannot dedicate, we cannot consecrate—we cannot hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they here gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.") -) - -type MockSubgraphClient struct { - mock.Mock -} - -type MockGRPCConnection struct{} - -type MockHttpClient struct { - ShouldSucceed bool -} - -func (mc *MockGRPCConnection) Dial(serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { - // Here, return a mock connection. How you implement this depends on your testing framework - // and what aspects of the gRPC connection you wish to mock. - // For a simple approach, you might not even need to return a real *grpc.ClientConn - // but rather a mock or stub that satisfies the interface. - return &grpc.ClientConn{}, nil // Simplified, consider using a more sophisticated mock. -} - -type MockGRPNilConnection struct{} - -func (mc *MockGRPNilConnection) Dial(serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { - // Here, return a mock connection. How you implement this depends on your testing framework - // and what aspects of the gRPC connection you wish to mock. - // For a simple approach, you might not even need to return a real *grpc.ClientConn - // but rather a mock or stub that satisfies the interface. - return nil, nil // Simplified, consider using a more sophisticated mock. -} - -type MockHealthCheckService struct { - ResponseMap map[string]*grpc_health_v1.HealthCheckResponse -} - -func NewMockHealthCheckService() *MockHealthCheckService { - return &MockHealthCheckService{ - ResponseMap: make(map[string]*grpc_health_v1.HealthCheckResponse), - } -} - -func (m *MockHealthCheckService) CheckHealth(ctx context.Context, serviceName string) (*grpc_health_v1.HealthCheckResponse, error) { - response, exists := m.ResponseMap[serviceName] - if !exists { - // Simulate an unsupported service error or return a default response - return nil, fmt.Errorf("unsupported service: %s", serviceName) - } - return response, nil -} - -func (m *MockHealthCheckService) CloseConnections() error { - // Close any open connections or resources - return nil -} - -func (m *MockHealthCheckService) AddResponse(serviceName string, response *grpc_health_v1.HealthCheckResponse) { - m.ResponseMap[serviceName] = response -} - -func (c *MockHttpClient) CheckHealth(url string) (string, error) { - // Simulate success or failure based on the ShouldSucceed flag - - if c.ShouldSucceed { - return "SERVING", nil - } - - return "NOT_SERVING", nil -} - -func TestFetchBlobHandler(t *testing.T) { - r := setUpRouter() - - blob := makeTestBlob(0, 80) - key := queueBlob(t, &blob, blobstore) - expectedBatchHeaderHash := [32]byte{1, 2, 3} - expectedBlobIndex := uint32(1) - markBlobConfirmed(t, &blob, key, expectedBlobIndex, expectedBatchHeaderHash, blobstore) - blobKey := key.String() - r.GET("/v1/feed/blobs/:blob_key", testDataApiServer.FetchBlobHandler) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/feed/blobs/"+blobKey, nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.BlobMetadataResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, hex.EncodeToString(expectedBatchHeaderHash[:]), response.BatchHeaderHash) - assert.Equal(t, expectedBlobIndex, uint32(response.BlobIndex)) - assert.Equal(t, hex.EncodeToString(expectedSignatoryRecordHash[:]), response.SignatoryRecordHash) - assert.Equal(t, expectedReferenceBlockNumber, uint32(response.ReferenceBlockNumber)) - assert.Equal(t, hex.EncodeToString(expectedBatchRoot), response.BatchRoot) - assert.Equal(t, hex.EncodeToString(expectedInclusionProof), response.BlobInclusionProof) - assert.Equal(t, expectedBlobCommitment, response.BlobCommitment) - assert.Equal(t, expectedBatchId, uint32(response.BatchId)) - assert.Equal(t, expectedConfirmationBlockNumber, uint32(response.ConfirmationBlockNumber)) - assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000123", response.ConfirmationTxnHash) - assert.Equal(t, hex.EncodeToString(expectedFee), response.Fee) - assert.Equal(t, blob.RequestHeader.SecurityParams, response.SecurityParams) - assert.Equal(t, uint64(5567830000), response.RequestAt) -} - -func TestFetchBlobsHandler(t *testing.T) { - r := setUpRouter() - blob := makeTestBlob(0, 10) - - for _, batch := range subgraphBatches { - var ( - key = queueBlob(t, &blob, blobstore) - ) - // Convert the string to a byte slice - batchHeaderHashBytes := []byte(batch.BatchHeaderHash) - batchHeaderHash, err := dataapi.ConvertHexadecimalToBytes(batchHeaderHashBytes) - assert.NoError(t, err) - markBlobConfirmed(t, &blob, key, 1, batchHeaderHash, blobstore) - } - - mockSubgraphApi.On("QueryBatches").Return(subgraphBatches, nil) - - r.GET("/v1/feed/blobs", testDataApiServer.FetchBlobsHandler) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/feed/blobs?limit=2", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.BlobsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) -} - -func TestFetchBlobsFromBatchHeaderHash(t *testing.T) { - r := setUpRouter() - - batchHeaderHash := "6E2EFA6EB7AE40CE7A65B465679DE5649F994296D18C075CF2C490564BBF7CA5" - batchHeaderHashBytes, err := dataapi.ConvertHexadecimalToBytes([]byte(batchHeaderHash)) - assert.NoError(t, err) - - blob1 := makeTestBlob(0, 80) - key1 := queueBlob(t, &blob1, blobstore) - - blob2 := makeTestBlob(0, 80) - key2 := queueBlob(t, &blob2, blobstore) - - markBlobConfirmed(t, &blob1, key1, 1, batchHeaderHashBytes, blobstore) - markBlobConfirmed(t, &blob2, key2, 2, batchHeaderHashBytes, blobstore) - - r.GET("/v1/feed/batches/:batch_header_hash/blobs", testDataApiServer.FetchBlobsFromBatchHeaderHash) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/feed/batches/"+batchHeaderHash+"/blobs?limit=1", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.BlobsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, hex.EncodeToString(batchHeaderHashBytes[:]), response.Data[0].BatchHeaderHash) - assert.Equal(t, uint32(1), uint32(response.Data[0].BlobIndex)) - - // With the next_token query parameter set, the response should contain the next token - w = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/v1/feed/batches/"+batchHeaderHash+"/blobs?limit=1&next_token="+response.Meta.NextToken, nil) - r.ServeHTTP(w, req) - - res = w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err = io.ReadAll(res.Body) - assert.NoError(t, err) - - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, hex.EncodeToString(batchHeaderHashBytes[:]), response.Data[0].BatchHeaderHash) - assert.Equal(t, uint32(2), uint32(response.Data[0].BlobIndex)) - - // With the next_token query parameter set to an invalid value, the response should contain an error - w = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/v1/feed/batches/"+batchHeaderHash+"/blobs?limit=1&next_token=invalid", nil) - r.ServeHTTP(w, req) - - res = w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err = io.ReadAll(res.Body) - assert.NoError(t, err) - - var errorResponse dataapi.ErrorResponse - err = json.Unmarshal(data, &errorResponse) - assert.NoError(t, err) - - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) - assert.Equal(t, "invalid next_token", errorResponse.Error) - - // Fetch both blobs when no limit is set - w = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/v1/feed/batches/"+batchHeaderHash+"/blobs", nil) - r.ServeHTTP(w, req) - - res = w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err = io.ReadAll(res.Body) - assert.NoError(t, err) - - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - - // When the batch header hash is invalid, the response should contain an error - w = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/v1/feed/batches/invalid/blobs", nil) - r.ServeHTTP(w, req) - - res = w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err = io.ReadAll(res.Body) - assert.NoError(t, err) - - err = json.Unmarshal(data, &errorResponse) - assert.NoError(t, err) - - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) - assert.Equal(t, "invalid batch header hash", errorResponse.Error) -} - -func TestFetchMetricsHandler(t *testing.T) { - r := setUpRouter() - - blob := makeTestBlob(0, 10) - for _, batch := range subgraphBatches { - var ( - key = queueBlob(t, &blob, blobstore) - ) - - batchHeaderHashBytes := []byte(batch.BatchHeaderHash) - batchHeaderHash, err := dataapi.ConvertHexadecimalToBytes(batchHeaderHashBytes) - assert.NoError(t, err) - - markBlobConfirmed(t, &blob, key, 1, batchHeaderHash, blobstore) - } - - s := new(model.SampleStream) - err := s.UnmarshalJSON([]byte(mockPrometheusResponse)) - assert.NoError(t, err) - - matrix := make(model.Matrix, 0) - matrix = append(matrix, s) - mockTx.On("GetCurrentBlockNumber").Return(uint32(1), nil) - mockTx.On("GetQuorumCount").Return(uint8(2), nil) - mockSubgraphApi.On("QueryBatches").Return(subgraphBatches, nil) - mockPrometheusApi.On("QueryRange").Return(matrix, nil, nil).Once() - - r.GET("/v1/metrics", testDataApiServer.FetchMetricsHandler) - - req := httptest.NewRequest(http.MethodGet, "/v1/metrics", nil) - req.Close = true - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.Metric - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 16555.555555555555, response.Throughput) - assert.Equal(t, float64(85.14485344239945), response.CostInGas) - assert.Equal(t, big.NewInt(2), response.TotalStake) - assert.Len(t, response.TotalStakePerQuorum, 2) - assert.Equal(t, big.NewInt(2), response.TotalStakePerQuorum[0]) - assert.Equal(t, big.NewInt(4), response.TotalStakePerQuorum[1]) -} - -func TestFetchMetricsThroughputHandler(t *testing.T) { - r := setUpRouter() - - s := new(model.SampleStream) - err := s.UnmarshalJSON([]byte(mockPrometheusRespAvgThroughput)) - assert.NoError(t, err) - - matrix := make(model.Matrix, 0) - matrix = append(matrix, s) - mockPrometheusApi.On("QueryRange").Return(matrix, nil, nil).Once() - - r.GET("/v1/metrics/throughput", testDataApiServer.FetchMetricsThroughputHandler) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/metrics/throughput", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response []*dataapi.Throughput - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - var totalThroughput float64 - for _, v := range response { - totalThroughput += v.Throughput - } - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 3361, len(response)) - assert.Equal(t, float64(12000), response[0].Throughput) - assert.Equal(t, uint64(1701292920), response[0].Timestamp) - assert.Equal(t, float64(3.503022666666651e+07), totalThroughput) -} - -func TestFetchUnsignedBatchesHandler(t *testing.T) { - r := setUpRouter() - - stopTime := time.Now().UTC() - interval := 3600 - startTime := stopTime.Add(-time.Duration(interval) * time.Second) - - mockSubgraphApi.On("QueryBatchNonSigningInfo", startTime.Unix(), stopTime.Unix()).Return(batchNonSigningInfo, nil) - addr1 := gethcommon.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa") - addr2 := gethcommon.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") - mockTx.On("BatchOperatorIDToAddress").Return([]gethcommon.Address{addr1, addr2}, nil) - mockTx.On("GetQuorumBitmapForOperatorsAtBlockNumber").Return([]*big.Int{big.NewInt(3), big.NewInt(0)}, nil) - mockSubgraphApi.On("QueryOperatorAddedToQuorum").Return(operatorAddedToQuorum, nil) - mockSubgraphApi.On("QueryOperatorRemovedFromQuorum").Return(operatorRemovedFromQuorum, nil) - - r.GET("/v1/metrics/operator-nonsigning-percentage", testDataApiServer.FetchOperatorsNonsigningPercentageHandler) - - w := httptest.NewRecorder() - reqStr := fmt.Sprintf("/v1/metrics/operator-nonsigning-percentage?interval=%v&end=%s", interval, stopTime.Format("2006-01-02T15:04:05Z")) - req := httptest.NewRequest(http.MethodGet, reqStr, nil) - ctxWithDeadline, cancel := context.WithTimeout(req.Context(), 500*time.Microsecond) - defer cancel() - - req = req.WithContext(ctxWithDeadline) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.OperatorsNonsigningPercentage - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - responseData := response.Data[0] - operatorId := responseData.OperatorId - assert.Equal(t, 1, responseData.TotalBatches) - assert.Equal(t, 1, responseData.TotalUnsignedBatches) - assert.Equal(t, uint8(0), responseData.QuorumId) - assert.Equal(t, float64(100), responseData.Percentage) - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operatorId) - assert.Equal(t, float64(50), responseData.StakePercentage) - - responseData = response.Data[1] - operatorId = responseData.OperatorId - assert.Equal(t, 2, responseData.TotalBatches) - assert.Equal(t, 2, responseData.TotalUnsignedBatches) - assert.Equal(t, uint8(1), responseData.QuorumId) - assert.Equal(t, float64(100), responseData.Percentage) - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operatorId) - assert.Equal(t, float64(25), responseData.StakePercentage) -} - -func TestPortCheckIpValidation(t *testing.T) { - assert.Equal(t, false, dataapi.ValidOperatorIP("", mockLogger)) - assert.Equal(t, false, dataapi.ValidOperatorIP("0.0.0.0:32005", mockLogger)) - assert.Equal(t, true, dataapi.ValidOperatorIP("10.0.0.1:32005", mockLogger)) - assert.Equal(t, false, dataapi.ValidOperatorIP("::ffff:192.0.2.1:32005", mockLogger)) - assert.Equal(t, false, dataapi.ValidOperatorIP("google.com", mockLogger)) - assert.Equal(t, true, dataapi.ValidOperatorIP("localhost:32005", mockLogger)) - assert.Equal(t, true, dataapi.ValidOperatorIP("127.0.0.1:32005", mockLogger)) - assert.Equal(t, true, dataapi.ValidOperatorIP("23.93.76.1:32005", mockLogger)) - assert.Equal(t, true, dataapi.ValidOperatorIP("google.com:32005", mockLogger)) - assert.Equal(t, true, dataapi.ValidOperatorIP("[2606:4700:4400::ac40:98f1]:32005", mockLogger)) - assert.Equal(t, false, dataapi.ValidOperatorIP("2606:4700:4400::ac40:98f1:32005", mockLogger)) -} - -func TestPortCheck(t *testing.T) { - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil - r := setUpRouter() - operator_id := "0xa96bfb4a7ca981ad365220f336dc5a3de0816ebd5130b79bbc85aca94bc9b6ab" - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(operatorInfo, nil) - r.GET("/v1/operators-info/port-check", testDataApiServer.OperatorPortCheck) - w := httptest.NewRecorder() - reqStr := fmt.Sprintf("/v1/operators-info/port-check?operator_id=%v", operator_id) - req := httptest.NewRequest(http.MethodGet, reqStr, nil) - ctxWithDeadline, cancel := context.WithTimeout(req.Context(), 500*time.Microsecond) - defer cancel() - req = req.WithContext(ctxWithDeadline) - r.ServeHTTP(w, req) - assert.Equal(t, w.Code, http.StatusOK) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.OperatorPortCheckResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, "23.93.76.1:32005", response.DispersalSocket) - assert.Equal(t, false, response.DispersalOnline) - assert.Equal(t, "23.93.76.1:32006", response.RetrievalSocket) - assert.Equal(t, false, response.RetrievalOnline) - - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestCheckBatcherHealthExpectServing(t *testing.T) { - r := setUpRouter() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, &MockHttpClient{ShouldSucceed: true}) - - r.GET("/v1/metrics/batcher-service-availability", testDataApiServer.FetchBatcherAvailability) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/metrics/batcher-service-availability", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.ServiceAvailabilityResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - fmt.Printf("Response: %v\n", response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - serviceData := response.Data[0] - assert.Equal(t, "Batcher", serviceData.ServiceName) - assert.Equal(t, "SERVING", serviceData.ServiceStatus) -} - -func TestCheckBatcherHealthExpectNotServing(t *testing.T) { - r := setUpRouter() - - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, &MockHttpClient{ShouldSucceed: false}) - - r.GET("/v1/metrics/batcher-service-availability", testDataApiServer.FetchBatcherAvailability) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/metrics/batcher-service-availability", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.ServiceAvailabilityResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - fmt.Printf("Response: %v\n", response) - - assert.Equal(t, http.StatusServiceUnavailable, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - serviceData := response.Data[0] - assert.Equal(t, "Batcher", serviceData.ServiceName) - assert.Equal(t, "NOT_SERVING", serviceData.ServiceStatus) -} - -func TestFetchDisperserServiceAvailabilityHandler(t *testing.T) { - r := setUpRouter() - - mockHealthCheckService := NewMockHealthCheckService() - mockHealthCheckService.AddResponse("Disperser", &grpc_health_v1.HealthCheckResponse{ - Status: grpc_health_v1.HealthCheckResponse_SERVING, - }) - - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, mockHealthCheckService, nil) - - r.GET("/v1/metrics/disperser-service-availability", testDataApiServer.FetchDisperserServiceAvailability) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/metrics/disperser-service-availability", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.ServiceAvailabilityResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - fmt.Printf("Response: %v\n", response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - serviceData := response.Data[0] - assert.Equal(t, "Disperser", serviceData.ServiceName) - assert.Equal(t, grpc_health_v1.HealthCheckResponse_SERVING.String(), serviceData.ServiceStatus) -} - -func TestChurnerServiceAvailabilityHandler(t *testing.T) { - r := setUpRouter() - - mockHealthCheckService := NewMockHealthCheckService() - mockHealthCheckService.AddResponse("Churner", &grpc_health_v1.HealthCheckResponse{ - Status: grpc_health_v1.HealthCheckResponse_SERVING, - }) - - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, mockHealthCheckService, nil) - - r.GET("/v1/metrics/churner-service-availability", testDataApiServer.FetchChurnerServiceAvailability) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/metrics/churner-service-availability", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.ServiceAvailabilityResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - fmt.Printf("Response: %v\n", response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - serviceData := response.Data[0] - assert.Equal(t, "Churner", serviceData.ServiceName) - assert.Equal(t, grpc_health_v1.HealthCheckResponse_SERVING.String(), serviceData.ServiceStatus) -} - -func TestFetchDeregisteredOperatorNoSocketInfoOneOperatorHandler(t *testing.T) { - - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil - - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfoNoSocketInfo - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregistered, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfoNoSocketInfo, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", response.Data[0].OperatorId) - assert.Equal(t, "failed to convert operator info gql to indexed operator info at blocknumber: 22 for operator 0x3078653232646165313261303037346632306238666339366130343839333736", response.Data[0].OperatorProcessError) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredMultipleOperatorsOneWithNoSocketInfoHandler(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfoNoSocketInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphTwoOperatorsDeregistered, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfoNoSocketInfo, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - // Start test server for Operator - closeServer, err := startTestGRPCServer("localhost:32009") // Let the OS assign a free port - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer() // Ensure the server is closed after the test - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - operator1Data := response.Data[0] - operator2Data := response.Data[1] - - responseJson := string(data) - fmt.Printf("Response: %v\n", responseJson) - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - assert.Equal(t, "failed to convert operator info gql to indexed operator info at blocknumber: 22 for operator 0x3078653232646165313261303037346632306238666339366130343839333736", operator1Data.OperatorProcessError) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, true, operator2Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorInfoInvalidTimeStampHandler(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfoInvalidTimeStamp - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregisteredInvalidTimeStamp, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 0, response.Meta.Size) - assert.Equal(t, 0, len(response.Data)) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorInfoInvalidTimeStampTwoOperatorsHandler(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfoInvalidTimeStamp - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregisteredInvalidTimeStampTwoOperator, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - operator1Data := response.Data[0] - - responseJson := string(data) - fmt.Printf("Response: %v\n", responseJson) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator1Data.OperatorId) - assert.Equal(t, uint(24), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchMetricsDeregisteredOperatorHandler(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphTwoOperatorsDeregistered, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - // Start the test server for Operator 2 - closeServer, err := startTestGRPCServer("localhost:32009") - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer() - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - operator1Data := response.Data[0] - operator2Data := response.Data[1] - - responseJson := string(data) - fmt.Printf("Response: %v\n", responseJson) - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32007", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, true, operator2Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorOffline(t *testing.T) { - r := setUpRouter() - - indexedOperatorState := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorState[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregistered, nil) - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil) - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorState, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - - for _, data := range response.Data { - fmt.Printf("Data: %v\n", data) - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", data.OperatorId) - assert.Equal(t, uint(22), data.BlockNumber) - assert.Equal(t, "localhost:32007", data.Socket) - } - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorsWithoutDaysQueryParam(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphTwoOperatorsDeregistered, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators/", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators/", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - operator1Data := response.Data[0] - operator2Data := response.Data[1] - fmt.Printf("Operator1Data: %v\n", operator1Data) - fmt.Printf("Operator2Data: %v\n", operator2Data) - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32007", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, false, operator2Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorInvalidDaysQueryParam(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregistered, nil) - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil) - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=ten", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - fmt.Printf("Response: %v\n", res) - - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - - // Assert the response body - var responseBody map[string]string - err := json.Unmarshal(w.Body.Bytes(), &responseBody) - if err != nil { - t.Fatalf("Error unmarshaling response body: %v", err) - } - expectedErrorMessage := "Invalid 'days' parameter" - assert.Equal(t, expectedErrorMessage, responseBody["error"]) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorQueryDaysGreaterThan30(t *testing.T) { - r := setUpRouter() - - indexedOperatorState := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorState[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregistered, nil) - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil) - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorState, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=31", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - fmt.Printf("Response: %v\n", res) - - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - - // Assert the response body - var responseBody map[string]string - err := json.Unmarshal(w.Body.Bytes(), &responseBody) - if err != nil { - t.Fatalf("Error unmarshaling response body: %v", err) - } - expectedErrorMessage := "Invalid 'days' parameter. Max value is 30" - assert.Equal(t, expectedErrorMessage, responseBody["error"]) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorsMultipleOffline(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphTwoOperatorsDeregistered, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - fmt.Printf("Response: %v\n", response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - operator1Data := response.Data[0] - operator2Data := response.Data[1] - fmt.Printf("Operator1Data: %v\n", operator1Data) - fmt.Printf("Operator2Data: %v\n", operator2Data) - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32007", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, false, operator2Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorOnline(t *testing.T) { - r := setUpRouter() - - indexedOperatorState := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorState[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorDeregistered, nil) - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil) - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorState, nil) - - // Start test server for Operator - closeServer, err := startTestGRPCServer("localhost:32007") // Let the OS assign a free port - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer() // Ensure the server is closed after the test - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - assert.Equal(t, true, response.Data[0].IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorsMultipleOfflineOnline(t *testing.T) { - // Skipping this test as reported being flaky but could not reproduce it locally - t.Skip("Skipping testing in CI environment") - - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphTwoOperatorsDeregistered, nil) - - // Set up the mock calls for the two operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - // Start the test server for Operator 2 - closeServer, err := startTestGRPCServer("localhost:32009") - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer() - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - operator1Data := response.Data[0] - operator2Data := response.Data[1] - - responseJson := string(data) - fmt.Printf("Response: %v\n", responseJson) - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32007", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, true, operator2Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorsMultipleOnline(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphTwoOperatorsDeregistered, nil) - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - // Start test server for Operator 1 - closeServer1, err := startTestGRPCServer("localhost:32007") // Let the OS assign a free port - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer1() // Ensure the server is closed after the test - - // Start test server for Operator 2 - closeServer2, err := startTestGRPCServer("localhost:32009") // Let the OS assign a free port - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer2() // Ensure the server is closed after the test - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 2, response.Meta.Size) - assert.Equal(t, 2, len(response.Data)) - - operator1Data := response.Data[0] - operator2Data := response.Data[1] - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32007", operator1Data.Socket) - assert.Equal(t, true, operator1Data.IsOnline) - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, true, operator2Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchDeregisteredOperatorsMultipleOfflineSameBlock(t *testing.T) { - r := setUpRouter() - - indexedOperatorStates := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorStates[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - indexedOperatorStates[core.OperatorID{1}] = subgraphDeregisteredOperatorInfo2 - indexedOperatorStates[core.OperatorID{2}] = subgraphDeregisteredOperatorInfo3 - - mockSubgraphApi.On("QueryDeregisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphThreeOperatorsDeregistered, nil) - - // Set up the mock calls for the three operators - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo2, nil).Once() - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo3, nil).Once() - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorStates, nil) - - r.GET("/v1/operators-info/deregistered-operators", testDataApiServer.FetchDeregisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/deregistered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 3, response.Meta.Size) - assert.Equal(t, 3, len(response.Data)) - - operator1Data := response.Data[0] - - assert.Equal(t, "0xe22dae12a0074f20b8fc96a0489376db34075e545ef60c4845d264a732568311", operator1Data.OperatorId) - assert.Equal(t, uint(22), operator1Data.BlockNumber) - assert.Equal(t, "localhost:32007", operator1Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - operator2Data := getOperatorData(response.Data, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312") - operator3Data := getOperatorData(response.Data, "0xe24cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568313") - - assert.Equal(t, "0xe23cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568312", operator2Data.OperatorId) - assert.Equal(t, uint(24), operator2Data.BlockNumber) - assert.Equal(t, "localhost:32009", operator2Data.Socket) - assert.Equal(t, false, operator1Data.IsOnline) - - assert.Equal(t, "0xe24cae12a0074f20b8fc96a0489376db34075e545ef60c4845d264b732568313", operator3Data.OperatorId) - assert.Equal(t, uint(24), operator3Data.BlockNumber) - assert.Equal(t, "localhost:32011", operator3Data.Socket) - assert.Equal(t, false, operator3Data.IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func TestFetchRegisteredOperatorOnline(t *testing.T) { - r := setUpRouter() - - indexedOperatorState := make(map[core.OperatorID]*subgraph.OperatorInfo) - indexedOperatorState[core.OperatorID{0}] = subgraphDeregisteredOperatorInfo - mockSubgraphApi.On("QueryRegisteredOperatorsGreaterThanBlockTimestamp").Return(subgraphOperatorRegistered, nil) - mockSubgraphApi.On("QueryOperatorInfoByOperatorIdAtBlockNumber").Return(subgraphIndexedOperatorInfo1, nil) - testDataApiServer, _ = dataapi.NewServer(config, blobstore, prometheusClient, dataapi.NewSubgraphClient(mockSubgraphApi, mockLogger), mockTx, mockChainState, mockIndexedChainState, mockLogger, metrics, &MockGRPCConnection{}, nil, nil) - - mockSubgraphApi.On("QueryIndexedOperatorsWithStateForTimeWindow").Return(indexedOperatorState, nil) - - // Start test server for Operator - closeServer, err := startTestGRPCServer("localhost:32007") // Let the OS assign a free port - if err != nil { - t.Fatalf("Failed to start test server: %v", err) - } - defer closeServer() // Ensure the server is closed after the test - - r.GET("/v1/operators-info/registered-operators", testDataApiServer.FetchRegisteredOperators) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/v1/operators-info/registered-operators?days=14", nil) - r.ServeHTTP(w, req) - - res := w.Result() - defer core.CloseLogOnError(res.Body, "response body", mockLogger) - - data, err := io.ReadAll(res.Body) - assert.NoError(t, err) - - var response dataapi.QueriedStateOperatorsResponse - err = json.Unmarshal(data, &response) - assert.NoError(t, err) - assert.NotNil(t, response) - - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, 1, response.Meta.Size) - assert.Equal(t, 1, len(response.Data)) - assert.Equal(t, true, response.Data[0].IsOnline) - - // Reset the mock - mockSubgraphApi.ExpectedCalls = nil - mockSubgraphApi.Calls = nil -} - -func setUpRouter() *gin.Engine { - return gin.Default() -} - -func queueBlob(t *testing.T, blob *core.Blob, queue disperser.BlobStore) disperser.BlobKey { - t.Helper() - ctx := t.Context() - key, err := queue.StoreBlob(ctx, blob, expectedRequestedAt) - require.NoError(t, err) - return key -} - -func markBlobConfirmed(t *testing.T, blob *core.Blob, key disperser.BlobKey, blobIndex uint32, batchHeaderHash [32]byte, queue disperser.BlobStore) { - t.Helper() - ctx := t.Context() - // simulate blob confirmation - var commitX, commitY fp.Element - _, err := commitX.SetString("21661178944771197726808973281966770251114553549453983978976194544185382599016") - require.NoError(t, err) - _, err = commitY.SetString("9207254729396071334325696286939045899948985698134704137261649190717970615186") - require.NoError(t, err) - commitment := &encoding.G1Commitment{ - X: commitX, - Y: commitY, - } - - confirmationInfo := &disperser.ConfirmationInfo{ - BatchHeaderHash: batchHeaderHash, - BlobIndex: blobIndex, - SignatoryRecordHash: expectedSignatoryRecordHash, - ReferenceBlockNumber: expectedReferenceBlockNumber, - BatchRoot: expectedBatchRoot, - BlobInclusionProof: expectedInclusionProof, - BlobCommitment: &encoding.BlobCommitments{ - Commitment: commitment, - Length: expectedDataLength, - }, - BatchID: expectedBatchId, - ConfirmationTxnHash: gethcommon.HexToHash("0x123"), - ConfirmationBlockNumber: expectedConfirmationBlockNumber, - Fee: expectedFee, - } - metadata := &disperser.BlobMetadata{ - BlobHash: key.BlobHash, - MetadataHash: key.MetadataHash, - BlobStatus: disperser.Confirmed, - Expiry: 0, - NumRetries: 0, - RequestMetadata: &disperser.RequestMetadata{ - BlobRequestHeader: core.BlobRequestHeader{ - SecurityParams: blob.RequestHeader.SecurityParams, - }, - RequestedAt: expectedRequestedAt, - BlobSize: uint(len(blob.Data)), - }, - } - - expectedBlobCommitment = confirmationInfo.BlobCommitment - updated, err := queue.MarkBlobConfirmed(ctx, metadata, confirmationInfo) - require.NoError(t, err) - require.Equal(t, disperser.Confirmed, updated.BlobStatus) -} - -func makeTestBlob(quorumID core.QuorumID, adversityThreshold uint8) core.Blob { - blob := core.Blob{ - RequestHeader: core.BlobRequestHeader{ - SecurityParams: []*core.SecurityParam{ - { - QuorumID: quorumID, - AdversaryThreshold: adversityThreshold, - }, - }, - }, - Data: gettysburgAddressBytes, - } - return blob -} - -// startTestGRPCServer starts a gRPC server on a specified address. -// It returns a function to stop the server. -func startTestGRPCServer(address string) (stopFunc func(), err error) { - lis, err := net.Listen("tcp", address) - if err != nil { - return nil, err - } - - grpcServer := grpc.NewServer() - - stopFunc = func() { - grpcServer.Stop() - core.CloseLogOnError(lis, "listener", nil) - } - - go func() { - if err := grpcServer.Serve(lis); err != nil { - log.Fatalf("Failed to serve: %v", err) - } - }() - - return stopFunc, nil -} - -// Helper to get OperatorData from response -func getOperatorData(operatorMetadtas []*dataapi.QueriedStateOperatorMetadata, operatorId string) dataapi.QueriedStateOperatorMetadata { - - for _, operatorMetadata := range operatorMetadtas { - if operatorMetadata.OperatorId == operatorId { - return *operatorMetadata - } - } - return dataapi.QueriedStateOperatorMetadata{} - -} diff --git a/disperser/dataapi/v2/feed_cache_test.go b/disperser/dataapi/v2/feed_cache_test.go index 0b570df5ce..f6ac686763 100644 --- a/disperser/dataapi/v2/feed_cache_test.go +++ b/disperser/dataapi/v2/feed_cache_test.go @@ -96,7 +96,7 @@ func setupTestCache(maxItems int) (*v2.FeedCache[testItem], *testFetcher, time.T maxItems, fetcher.fetch, timestampFn, - dataapi.NewMetrics(uint(2), prometheus.NewRegistry(), nil, "9001", test.GetLogger()).BatchFeedCacheMetrics, + dataapi.NewMetrics(prometheus.NewRegistry(), nil, "9001", test.GetLogger()).BatchFeedCacheMetrics, ) return cache, fetcher, baseTime diff --git a/disperser/dataapi/v2/server_v2.go b/disperser/dataapi/v2/server_v2.go index 6f5f6f7985..faf93929ce 100644 --- a/disperser/dataapi/v2/server_v2.go +++ b/disperser/dataapi/v2/server_v2.go @@ -189,7 +189,7 @@ func NewServerV2( indexedChainState: indexedChainState, metrics: metrics, operatorHandler: operatorHandler, - metricsHandler: dataapi.NewMetricsHandler(promClient, dataapi.V2), + metricsHandler: dataapi.NewMetricsHandler(promClient), batchFeedCache: batchFeedCache, blobMetadataCache: blobMetadataCache, blobAttestationInfoCache: blobAttestationInfoCache, diff --git a/disperser/dataapi/v2/server_v2_test.go b/disperser/dataapi/v2/server_v2_test.go index 9827b4c053..f2f13e67c4 100644 --- a/disperser/dataapi/v2/server_v2_test.go +++ b/disperser/dataapi/v2/server_v2_test.go @@ -73,7 +73,6 @@ var ( dynamoClient dynamodb.Client - serverVersion = uint(2) mockPrometheusApi = &prommock.MockPrometheusApi{} prometheusClient = dataapi.NewPrometheusClient(mockPrometheusApi, "test-cluster") mockSubgraphApi = &subgraphmock.MockSubgraphApi{} @@ -198,7 +197,7 @@ func setup(_ *testing.M) { mockTx.On("GetCurrentBlockNumber").Return(uint32(1), nil) mockTx.On("GetQuorumCount").Return(uint8(2), nil) - metrics := dataapi.NewMetrics(serverVersion, prometheus.NewRegistry(), blobMetadataStore, "9001", logger) + metrics := dataapi.NewMetrics(prometheus.NewRegistry(), blobMetadataStore, "9001", logger) testDataApiServerV2, err = serverv2.NewServerV2( config, blobMetadataStore, prometheusClient, subgraphClient, mockTx, mockChainState, mockIndexedChainState, logger, metrics) @@ -1516,7 +1515,7 @@ func TestFetchBatchFeed(t *testing.T) { testDataApiServerV2, err := serverv2.NewServerV2( config, blobMetadataStore, prometheusClient, subgraphClient, mockTx, mockChainState, mockIndexedChainState, logger, - dataapi.NewMetrics(serverVersion, prometheus.NewRegistry(), nil, "9001", logger)) + dataapi.NewMetrics(prometheus.NewRegistry(), nil, "9001", logger)) require.NoError(t, err) r.GET("/v2/batches/feed", testDataApiServerV2.FetchBatchFeed) @@ -1996,7 +1995,7 @@ func TestFetchOperatorSigningInfo(t *testing.T) { testDataApiServerV2, err := serverv2.NewServerV2( config, blobMetadataStore, prometheusClient, subgraphClient, mockTx, mockChainState, mockIndexedChainState, logger, - dataapi.NewMetrics(serverVersion, prometheus.NewRegistry(), nil, "9001", logger)) + dataapi.NewMetrics(prometheus.NewRegistry(), nil, "9001", logger)) require.NoError(t, err) r.GET("/v2/operators/signing-info", testDataApiServerV2.FetchOperatorSigningInfo) @@ -2295,7 +2294,7 @@ func TestCheckOperatorsLivenessLegacyV1SocketRegistration(t *testing.T) { testDataApiServerV2, err := serverv2.NewServerV2( config, blobMetadataStore, prometheusClient, subgraphClient, mockTx, mockChainState, mockIcs, logger, - dataapi.NewMetrics(serverVersion, prometheus.NewRegistry(), nil, "9001", logger)) + dataapi.NewMetrics(prometheus.NewRegistry(), nil, "9001", logger)) require.NoError(t, err) r.GET("/v2/operators/liveness", testDataApiServerV2.CheckOperatorsLiveness) diff --git a/disperser/disperser.go b/disperser/disperser.go deleted file mode 100644 index a50107c038..0000000000 --- a/disperser/disperser.go +++ /dev/null @@ -1,246 +0,0 @@ -package disperser - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/encoding" - - disperser_rpc "github.com/Layr-Labs/eigenda/api/grpc/disperser" - gcommon "github.com/ethereum/go-ethereum/common" -) - -// BlobStatus represents the status of a blob. -// The status of a blob is updated as the blob is processed by the disperser. -// The status of a blob can be queried by the client using the GetBlobStatus API. -// Intermediate states are states that the blob can be in while being processed, and it can be updated to a differet state: -// - PROCESSING -// - DISPERSING -// - CONFIRMED -// Terminal states are states that will not be updated to a different state: -// - FAILED -// - FINALIZED -// - INSUFFICIENT_SIGNATURES -// -// Note: this docstring and the enum ones below are copied from the disperser.proto, -// which is the source of truth for BlobStatus. -type BlobStatus uint - -// WARNING: THESE VALUES BECOME PART OF PERSISTENT SYSTEM STATE; -// ALWAYS INSERT NEW ENUM VALUES AS THE LAST ELEMENT TO MAINTAIN COMPATIBILITY -const ( - // PROCESSING means that the blob is currently being processed by the disperser - Processing BlobStatus = iota - // CONFIRMED means that the blob has been dispersed to DA Nodes and the dispersed - // batch containing the blob has been confirmed onchain - Confirmed - // FAILED means that the blob has failed permanently (for reasons other than insufficient - // signatures, which is a separate state). This status is somewhat of a catch-all category, - // containg (but not necessarily exclusively as errors can be added in the future): - // - blob has expired - // - internal logic error while requesting encoding - // - blob retry has exceeded its limit while waiting for blob finalization after confirmation. - // Most likely triggered by a chain reorg: see https://github.com/Layr-Labs/eigenda/blob/master/disperser/batcher/finalizer.go#L179-L189. - Failed - // FINALIZED means that the block containing the blob's confirmation transaction has been finalized on Ethereum - Finalized - // INSUFFICIENT_SIGNATURES means that the confirmation threshold for the blob was not met - // for at least one quorum. - InsufficientSignatures - // The DISPERSING state is comprised of two separate phases: - // - Dispersing to DA nodes and collecting signature - // - Submitting the transaction on chain and waiting for tx receipt - Dispersing -) - -var enumStrings = map[BlobStatus]string{ - Processing: "Processing", - Confirmed: "Confirmed", - Failed: "Failed", - Finalized: "Finalized", - InsufficientSignatures: "InsufficientSignatures", - Dispersing: "Dispersing", -} - -func (bs BlobStatus) String() string { - if str, ok := enumStrings[bs]; ok { - return str - } - return "Unknown value" -} - -type BlobHash = string -type MetadataHash = string - -type BlobKey struct { - BlobHash BlobHash - MetadataHash MetadataHash -} - -func (mk BlobKey) String() string { - return fmt.Sprintf("%s-%s", mk.BlobHash, mk.MetadataHash) -} - -func ParseBlobKey(key string) (BlobKey, error) { - parts := strings.Split(key, "-") - if len(parts) != 2 { - return BlobKey{}, fmt.Errorf("invalid metadata key: %s", key) - } - return BlobKey{ - BlobHash: parts[0], - MetadataHash: parts[1], - }, nil -} - -type BlobMetadata struct { - BlobHash BlobHash `json:"blob_hash"` - MetadataHash MetadataHash `json:"metadata_hash"` - BlobStatus BlobStatus `json:"blob_status"` - // Expiry is unix epoch time in seconds at which the blob will expire - Expiry uint64 `json:"expiry"` - // NumRetries is the number of times the blob has been retried - // After few failed attempts, the blob will be marked as failed - NumRetries uint `json:"num_retries"` - // RequestMetadata is the request metadata of the blob when it was requested - // This field is omitted when marshalling to DynamoDB attributevalue as this field will be flattened - RequestMetadata *RequestMetadata `json:"request_metadata" dynamodbav:"-"` - // ConfirmationInfo is the confirmation metadata of the blob when it was confirmed - // This field is nil if the blob has not been confirmed - // This field is omitted when marshalling to DynamoDB attributevalue as this field will be flattened - ConfirmationInfo *ConfirmationInfo `json:"blob_confirmation_info" dynamodbav:"-"` -} - -func (m *BlobMetadata) GetBlobKey() BlobKey { - return BlobKey{ - BlobHash: m.BlobHash, - MetadataHash: m.MetadataHash, - } -} - -func (m *BlobMetadata) IsConfirmed() (bool, error) { - if m.BlobStatus != Confirmed && m.BlobStatus != Finalized { - return false, nil - } - - if m.ConfirmationInfo == nil { - return false, fmt.Errorf("blob status is confirmed but missing confirmation info: %s", m.GetBlobKey().String()) - } - return true, nil -} - -type RequestMetadata struct { - core.BlobRequestHeader - BlobSize uint `json:"blob_size"` - RequestedAt uint64 `json:"requested_at"` -} - -type ConfirmationInfo struct { - BatchHeaderHash [32]byte `json:"batch_header_hash"` - BlobIndex uint32 `json:"blob_index"` - BlobCount uint32 `json:"blob_count"` - SignatoryRecordHash [32]byte `json:"signatory_record_hash"` - ReferenceBlockNumber uint32 `json:"reference_block_number"` - BatchRoot []byte `json:"batch_root"` - BlobInclusionProof []byte `json:"blob_inclusion_proof"` - BlobCommitment *encoding.BlobCommitments `json:"blob_commitment"` - BatchID uint32 `json:"batch_id"` - ConfirmationTxnHash gcommon.Hash `json:"confirmation_txn_hash"` - ConfirmationBlockNumber uint32 `json:"confirmation_block_number"` - Fee []byte `json:"fee"` - QuorumResults map[core.QuorumID]*core.QuorumResult `json:"quorum_results"` - BlobQuorumInfos []*core.BlobQuorumInfo `json:"blob_quorum_infos"` -} - -type BlobStoreExclusiveStartKey struct { - BlobHash BlobHash - MetadataHash MetadataHash - BlobStatus int32 // BlobStatus is an integer - Expiry int64 // Expiry is epoch time in seconds -} - -type BatchIndexExclusiveStartKey struct { - BlobHash BlobHash - MetadataHash MetadataHash - BatchHeaderHash []byte - BlobIndex uint32 -} - -type BlobStore interface { - // StoreBlob adds a blob to the queue and returns a key that can be used to retrieve the blob later - StoreBlob(ctx context.Context, blob *core.Blob, requestedAt uint64) (BlobKey, error) - // GetBlobContent retrieves a blob's content - GetBlobContent(ctx context.Context, blobHash BlobHash) ([]byte, error) - // MarkBlobConfirmed updates blob metadata to Confirmed status with confirmation info - // Returns the updated metadata and error - MarkBlobConfirmed(ctx context.Context, existingMetadata *BlobMetadata, confirmationInfo *ConfirmationInfo) (*BlobMetadata, error) - // MarkBlobDispersing updates blob metadata to Dispersing status - MarkBlobDispersing(ctx context.Context, blobKey BlobKey) error - // MarkBlobInsufficientSignatures updates blob metadata to InsufficientSignatures status with confirmation info - // Returns the updated metadata and error - MarkBlobInsufficientSignatures(ctx context.Context, existingMetadata *BlobMetadata, confirmationInfo *ConfirmationInfo) (*BlobMetadata, error) - // MarkBlobFinalized marks a blob as finalized - MarkBlobFinalized(ctx context.Context, blobKey BlobKey) error - // MarkBlobProcessing marks a blob as processing - MarkBlobProcessing(ctx context.Context, blobKey BlobKey) error - // MarkBlobFailed marks a blob as failed - MarkBlobFailed(ctx context.Context, blobKey BlobKey) error - // IncrementBlobRetryCount increments the retry count of a blob - IncrementBlobRetryCount(ctx context.Context, existingMetadata *BlobMetadata) error - // UpdateConfirmationBlockNumber updates the confirmation block number of a blob - UpdateConfirmationBlockNumber(ctx context.Context, existingMetadata *BlobMetadata, confirmationBlockNumber uint32) error - // GetBlobsByMetadata retrieves a list of blobs given a list of metadata - GetBlobsByMetadata(ctx context.Context, metadata []*BlobMetadata) (map[BlobKey]*core.Blob, error) - // GetBlobMetadataByStatus returns a list of blob metadata for blobs with the given status - GetBlobMetadataByStatus(ctx context.Context, blobStatus BlobStatus) ([]*BlobMetadata, error) - // GetMetadataInBatch returns the metadata in a given batch at given index. - GetMetadataInBatch(ctx context.Context, batchHeaderHash [32]byte, blobIndex uint32) (*BlobMetadata, error) - // GetBlobMetadataByStatusWithPagination returns a list of blob metadata for blobs with the given status - // Results are limited to the given limit and the pagination token is returned - GetBlobMetadataByStatusWithPagination(ctx context.Context, blobStatus BlobStatus, limit int32, exclusiveStartKey *BlobStoreExclusiveStartKey) ([]*BlobMetadata, *BlobStoreExclusiveStartKey, error) - // GetAllBlobMetadataByBatch returns the metadata of all the blobs in the batch. - GetAllBlobMetadataByBatch(ctx context.Context, batchHeaderHash [32]byte) ([]*BlobMetadata, error) - // GetAllBlobMetadataByBatchWithPagination returns all the blobs in the batch using pagination - GetAllBlobMetadataByBatchWithPagination(ctx context.Context, batchHeaderHash [32]byte, limit int32, exclusiveStartKey *BatchIndexExclusiveStartKey) ([]*BlobMetadata, *BatchIndexExclusiveStartKey, error) - // GetBlobMetadata returns a blob metadata given a metadata key - GetBlobMetadata(ctx context.Context, blobKey BlobKey) (*BlobMetadata, error) - // GetBulkBlobMetadata returns a list of blob metadata given a list of blob keys - GetBulkBlobMetadata(ctx context.Context, blobKeys []BlobKey) ([]*BlobMetadata, error) - // HandleBlobFailure handles a blob failure by either incrementing the retry count or marking the blob as failed - // Returns a boolean indicating whether the blob should be retried and an error - HandleBlobFailure(ctx context.Context, metadata *BlobMetadata, maxRetry uint) (bool, error) -} - -type Dispatcher interface { - DisperseBatch(context.Context, *core.IndexedOperatorState, []core.EncodedBlob, *core.BatchHeader) chan core.SigningMessage -} - -func FromBlobStatusProto(status disperser_rpc.BlobStatus) (*BlobStatus, error) { - var res BlobStatus - switch status { - case disperser_rpc.BlobStatus_UNKNOWN: - return nil, errors.New("unexpected blob status BlobStatus_UNKNOWN") - case disperser_rpc.BlobStatus_PROCESSING: - res = Processing - return &res, nil - case disperser_rpc.BlobStatus_CONFIRMED: - res = Confirmed - return &res, nil - case disperser_rpc.BlobStatus_FAILED: - res = Failed - return &res, nil - case disperser_rpc.BlobStatus_FINALIZED: - res = Finalized - return &res, nil - case disperser_rpc.BlobStatus_INSUFFICIENT_SIGNATURES: - res = InsufficientSignatures - return &res, nil - case disperser_rpc.BlobStatus_DISPERSING: - res = Dispersing - return &res, nil - } - - return nil, fmt.Errorf("unknown blob status: %v", status) -} diff --git a/disperser/mock/dispatcher.go b/disperser/mock/dispatcher.go deleted file mode 100644 index 3cbbf7a14d..0000000000 --- a/disperser/mock/dispatcher.go +++ /dev/null @@ -1,66 +0,0 @@ -package mock - -import ( - "context" - "errors" - - "github.com/Layr-Labs/eigenda/core" - coremock "github.com/Layr-Labs/eigenda/core/mock" - "github.com/Layr-Labs/eigenda/disperser" - "github.com/stretchr/testify/mock" -) - -type Dispatcher struct { - mock.Mock - state *coremock.PrivateOperatorState -} - -var _ disperser.Dispatcher = (*Dispatcher)(nil) - -func NewDispatcher(state *coremock.PrivateOperatorState) *Dispatcher { - return &Dispatcher{ - state: state, - } -} - -func (d *Dispatcher) DisperseBatch(ctx context.Context, state *core.IndexedOperatorState, blobs []core.EncodedBlob, header *core.BatchHeader) chan core.SigningMessage { - args := d.Called() - var nonSigners map[core.OperatorID]struct{} - if args.Get(0) != nil { - nonSigners = args.Get(0).(map[core.OperatorID]struct{}) - } - update := make(chan core.SigningMessage) - message, err := header.GetBatchHeaderHash() - if err != nil { - for id := range d.state.PrivateOperators { - update <- core.SigningMessage{ - Signature: nil, - ValidatorId: id, - Err: err, - } - } - } - - go func() { - for id := range state.IndexedOperators { - info := d.state.PrivateOperators[id] - if _, ok := nonSigners[id]; ok { - update <- core.SigningMessage{ - Signature: nil, - ValidatorId: id, - Err: errors.New("not a signer"), - } - } else { - sig := info.KeyPair.SignMessage(message) - - update <- core.SigningMessage{ - Signature: sig, - ValidatorId: id, - Err: nil, - } - } - } - }() - - return update -} diff --git a/docker-bake.hcl b/docker-bake.hcl index e8343e81da..01dd76df2d 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -44,7 +44,6 @@ group "default" { group "all" { targets = [ "node-group", - "batcher", "disperser", "encoder", "retriever", @@ -69,7 +68,6 @@ group "node-group" { group "internal-release" { targets = [ "node-internal", - "batcher-internal", "disperser-internal", "encoder-internal", "encoder-icicle-internal", @@ -86,22 +84,6 @@ group "internal-release" { } # DISPERSER TARGETS -target "batcher" { - context = "." - dockerfile = "./Dockerfile" - target = "batcher" - tags = ["${REGISTRY}/${REPO}/batcher:${BUILD_TAG}"] -} - -target "batcher-internal" { - inherits = ["batcher"] - tags = [ - "${REGISTRY}/eigenda-batcher:${BUILD_TAG}", - "${REGISTRY}/eigenda-batcher:${GIT_SHA}", - "${REGISTRY}/eigenda-batcher:sha-${GIT_SHORT_SHA}" - ] -} - target "disperser" { context = "." dockerfile = "./Dockerfile" diff --git a/inabox/deploy/cmd/main.go b/inabox/deploy/cmd/main.go index d56a819bfb..79174d9c66 100644 --- a/inabox/deploy/cmd/main.go +++ b/inabox/deploy/cmd/main.go @@ -22,7 +22,6 @@ var ( rootPathFlagName = "root-path" localstackPortFlagName = "localstack-port" - metadataTableName = "test-BlobMetadata" bucketTableName = "test-BucketStore" metadataTableNameV2 = "test-BlobMetadata-v2" @@ -216,7 +215,6 @@ func startLocalstack(ctx *cli.Context, config *deploy.Config) error { deployConfig := testbed.DeployResourcesConfig{ LocalStackEndpoint: localstackContainer.Endpoint(), - MetadataTableName: metadataTableName, BucketTableName: bucketTableName, V2MetadataTableName: metadataTableNameV2, AWSConfig: localstackContainer.GetAWSClientConfig(), diff --git a/inabox/deploy/codegen/main.go b/inabox/deploy/codegen/main.go index ffed292c98..5451aae5c4 100644 --- a/inabox/deploy/codegen/main.go +++ b/inabox/deploy/codegen/main.go @@ -9,7 +9,6 @@ import ( proxy "github.com/Layr-Labs/eigenda/api/proxy/config" dis "github.com/Layr-Labs/eigenda/disperser/cmd/apiserver/flags" - bat "github.com/Layr-Labs/eigenda/disperser/cmd/batcher/flags" controller "github.com/Layr-Labs/eigenda/disperser/cmd/controller/flags" enc "github.com/Layr-Labs/eigenda/disperser/cmd/encoder/flags" opr "github.com/Layr-Labs/eigenda/node/flags" @@ -187,7 +186,6 @@ func main() { ` configs += genVars("DisperserVars", getFlags(dis.Flags)) - configs += genVars("BatcherVars", getFlags(bat.Flags)) configs += genVars("EncoderVars", getFlags(enc.Flags)) configs += genVars("OperatorVars", getFlags(opr.Flags)) configs += genVars("RetrieverVars", getFlags(retriever.Flags)) diff --git a/inabox/deploy/config.go b/inabox/deploy/config.go index c918df778f..331fcfe1e0 100644 --- a/inabox/deploy/config.go +++ b/inabox/deploy/config.go @@ -219,7 +219,6 @@ func (env *Config) generateDisperserV2Vars(ind int, logPath, dbPath, grpcPort st DISPERSER_SERVER_EIGENDA_DIRECTORY: env.EigenDA.EigenDADirectory, DISPERSER_SERVER_BLS_OPERATOR_STATE_RETRIVER: env.EigenDA.OperatorStateRetriever, DISPERSER_SERVER_EIGENDA_SERVICE_MANAGER: env.EigenDA.ServiceManager, - DISPERSER_SERVER_DISPERSER_VERSION: "2", DISPERSER_SERVER_ENABLE_PAYMENT_METERER: "true", DISPERSER_SERVER_RESERVED_ONLY: "false", @@ -239,43 +238,6 @@ func (env *Config) generateDisperserV2Vars(ind int, logPath, dbPath, grpcPort st return v } -// Generates batcher .env -func (env *Config) generateBatcherVars(ind int, key, graphUrl, logPath string) BatcherVars { - v := BatcherVars{ - BATCHER_LOG_FORMAT: "text", - BATCHER_S3_BUCKET_NAME: "test-eigenda-blobstore", - BATCHER_DYNAMODB_TABLE_NAME: "test-BlobMetadata", - BATCHER_OBJECT_STORAGE_BACKEND: "s3", - BATCHER_ENABLE_METRICS: "true", - BATCHER_METRICS_HTTP_PORT: "9094", - BATCHER_PULL_INTERVAL: "5s", - BATCHER_EIGENDA_DIRECTORY: env.EigenDA.EigenDADirectory, - BATCHER_BLS_OPERATOR_STATE_RETRIVER: env.EigenDA.OperatorStateRetriever, - BATCHER_EIGENDA_SERVICE_MANAGER: env.EigenDA.ServiceManager, - BATCHER_SRS_ORDER: "300000", - BATCHER_CHAIN_RPC: "", - BATCHER_PRIVATE_KEY: key[2:], - BATCHER_GRAPH_URL: graphUrl, - BATCHER_USE_GRAPH: "true", - BATCHER_BATCH_SIZE_LIMIT: "10240", // 10 GiB - BATCHER_INDEXER_PULL_INTERVAL: "1s", - BATCHER_AWS_REGION: "", - BATCHER_AWS_ACCESS_KEY_ID: "", - BATCHER_AWS_SECRET_ACCESS_KEY: "", - BATCHER_AWS_ENDPOINT_URL: "", - BATCHER_FINALIZER_INTERVAL: "6m", - BATCHER_ENCODING_REQUEST_QUEUE_SIZE: "500", - BATCHER_NUM_CONFIRMATIONS: "0", - BATCHER_MAX_BLOBS_TO_FETCH_FROM_STORE: "100", - BATCHER_FINALIZATION_BLOCK_DELAY: "0", - BATCHER_KMS_KEY_DISABLE: "true", - } - - env.applyDefaults(&v, "BATCHER", "batcher", ind) - - return v -} - func (env *Config) generateEncoderVars(ind int, grpcPort string) EncoderVars { v := EncoderVars{ DISPERSER_ENCODER_LOG_FORMAT: "text", @@ -388,10 +350,6 @@ func (env *Config) generateProxyVars(ind int) ProxyVars { EIGENDA_PROXY_EIGENDA_V2_DISPERSER_RPC: "localhost:32005", EIGENDA_PROXY_EIGENDA_V2_EIGENDA_DIRECTORY: env.EigenDA.EigenDADirectory, EIGENDA_PROXY_EIGENDA_V2_GRPC_DISABLE_TLS: "true", - // SRS paths - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G1_PATH: "../resources/srs/g1.point", - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G2_PATH: "../resources/srs/g2.point", - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G2_TRAILING_PATH: "../resources/srs/g2.trailing.point", } env.applyDefaults(&v, "EIGENDA_PROXY", "proxy", ind) return v @@ -641,20 +599,6 @@ func (env *Config) GenerateAllVariables() error { env.Operators = append(env.Operators, operatorConfig) } - // Batcher - name = "batcher0" - logPath, _, _, envFile = env.getPaths(name) - key, _, err := env.getKey(name) - if err != nil { - return fmt.Errorf("failed to get key for %s: %w", name, err) - } - - batcherConfig := env.generateBatcherVars(0, key, graphUrl, logPath) - if err := writeEnv(batcherConfig.getEnvMap(), envFile); err != nil { - return fmt.Errorf("failed to write env file: %w", err) - } - env.Batcher = append(env.Batcher, batcherConfig) - // Encoders // TODO: Add more encoders name = "enc0" @@ -705,7 +649,7 @@ func (env *Config) GenerateAllVariables() error { } name = "retriever0" - key, _, err = env.getKey(name) + key, _, err := env.getKey(name) if err != nil { return fmt.Errorf("failed to get key for %s: %w", name, err) } diff --git a/inabox/deploy/config_types.go b/inabox/deploy/config_types.go index 816f988643..3d673ae355 100644 --- a/inabox/deploy/config_types.go +++ b/inabox/deploy/config_types.go @@ -116,7 +116,6 @@ type Config struct { Churner ChurnerVars Dispersers []DisperserVars - Batcher []BatcherVars Encoder []EncoderVars Operators []OperatorVars Stakers []Staker diff --git a/inabox/deploy/env_vars.go b/inabox/deploy/env_vars.go index cf6e1ae2a5..689b19c827 100644 --- a/inabox/deploy/env_vars.go +++ b/inabox/deploy/env_vars.go @@ -23,8 +23,6 @@ type DisperserVars struct { DISPERSER_SERVER_OCI_NAMESPACE string - DISPERSER_SERVER_DISPERSER_VERSION string - DISPERSER_SERVER_METRICS_HTTP_PORT string DISPERSER_SERVER_ENABLE_METRICS string @@ -171,137 +169,6 @@ func (vars DisperserVars) getEnvMap() map[string]string { return envMap } -type BatcherVars struct { - BATCHER_S3_BUCKET_NAME string - - BATCHER_DYNAMODB_TABLE_NAME string - - BATCHER_PULL_INTERVAL string - - BATCHER_ENCODER_ADDRESS string - - BATCHER_ENABLE_METRICS string - - BATCHER_BATCH_SIZE_LIMIT string - - BATCHER_USE_GRAPH string - - BATCHER_SRS_ORDER string - - BATCHER_METRICS_HTTP_PORT string - - BATCHER_INDEXER_DATA_DIR string - - BATCHER_ENCODING_TIMEOUT string - - BATCHER_ATTESTATION_TIMEOUT string - - BATCHER_BATCH_ATTESTATION_TIMEOUT string - - BATCHER_CHAIN_READ_TIMEOUT string - - BATCHER_CHAIN_WRITE_TIMEOUT string - - BATCHER_CHAIN_STATE_TIMEOUT string - - BATCHER_TRANSACTION_BROADCAST_TIMEOUT string - - BATCHER_NUM_CONNECTIONS string - - BATCHER_FINALIZER_INTERVAL string - - BATCHER_FINALIZER_POOL_SIZE string - - BATCHER_ENCODING_REQUEST_QUEUE_SIZE string - - BATCHER_MAX_NUM_RETRIES_PER_BLOB string - - BATCHER_TARGET_NUM_CHUNKS string - - BATCHER_MAX_BLOBS_TO_FETCH_FROM_STORE string - - BATCHER_FINALIZATION_BLOCK_DELAY string - - BATCHER_MAX_NODE_CONNECTIONS string - - BATCHER_MAX_NUM_RETRIES_PER_DISPERSAL string - - BATCHER_ENABLE_GNARK_BUNDLE_ENCODING string - - BATCHER_BLS_OPERATOR_STATE_RETRIVER string - - BATCHER_EIGENDA_SERVICE_MANAGER string - - BATCHER_EIGENDA_DIRECTORY string - - BATCHER_OBJECT_STORAGE_BACKEND string - - BATCHER_OCI_REGION string - - BATCHER_OCI_COMPARTMENT_ID string - - BATCHER_OCI_NAMESPACE string - - BATCHER_CHAIN_RPC string - - BATCHER_CHAIN_RPC_FALLBACK string - - BATCHER_PRIVATE_KEY string - - BATCHER_NUM_CONFIRMATIONS string - - BATCHER_NUM_RETRIES string - - BATCHER_RETRY_DELAY_INCREMENT string - - BATCHER_LOG_LEVEL string - - BATCHER_LOG_PATH string - - BATCHER_LOG_FORMAT string - - BATCHER_INDEXER_PULL_INTERVAL string - - BATCHER_AWS_REGION string - - BATCHER_AWS_ACCESS_KEY_ID string - - BATCHER_AWS_SECRET_ACCESS_KEY string - - BATCHER_AWS_ENDPOINT_URL string - - BATCHER_FRAGMENT_PREFIX_CHARS string - - BATCHER_FRAGMENT_PARALLELISM_FACTOR string - - BATCHER_FRAGMENT_PARALLELISM_CONSTANT string - - BATCHER_FRAGMENT_READ_TIMEOUT string - - BATCHER_FRAGMENT_WRITE_TIMEOUT string - - BATCHER_GRAPH_URL string - - BATCHER_GRAPH_BACKOFF string - - BATCHER_GRAPH_MAX_RETRIES string - - BATCHER_KMS_KEY_ID string - - BATCHER_KMS_KEY_REGION string - - BATCHER_KMS_KEY_DISABLE string -} - -func (vars BatcherVars) getEnvMap() map[string]string { - v := reflect.ValueOf(vars) - envMap := make(map[string]string) - for i := 0; i < v.NumField(); i++ { - envMap[v.Type().Field(i).Name] = v.Field(i).String() - } - return envMap -} - type EncoderVars struct { DISPERSER_ENCODER_GRPC_PORT string @@ -510,6 +377,10 @@ type OperatorVars struct { NODE_STORE_CHUNKS_REQUEST_MAX_FUTURE_AGE string + NODE_DISPERSER_RATE_LIMIT_PER_SECOND string + + NODE_DISPERSER_RATE_LIMIT_BURST string + NODE_LEVELDB_DISABLE_SEEKS_COMPACTION_V1 string NODE_LEVELDB_ENABLE_SYNC_WRITES_V1 string @@ -562,10 +433,18 @@ type OperatorVars struct { NODE_PAYMENT_VAULT_UPDATE_INTERVAL string + NODE_ON_DEMAND_METER_REFRESH_INTERVAL string + + NODE_ON_DEMAND_METER_FUZZ_FACTOR string + NODE_ENABLE_PER_ACCOUNT_PAYMENT_METRICS string NODE_OVERRIDE_V2_TTL string + NODE_ENFORCE_SINGLE_BLOB_BATCHES string + + NODE_DEPRECATE_V1 string + NODE_G1_PATH string NODE_G2_PATH string @@ -1093,36 +972,6 @@ type ProxyVars struct { EIGENDA_PROXY_LOG_COLOR string - EIGENDA_PROXY_EIGENDA_DISPERSER_RPC string - - EIGENDA_PROXY_EIGENDA_RESPONSE_TIMEOUT string - - EIGENDA_PROXY_EIGENDA_CONFIRMATION_TIMEOUT string - - EIGENDA_PROXY_EIGENDA_STATUS_QUERY_TIMEOUT string - - EIGENDA_PROXY_EIGENDA_STATUS_QUERY_INTERVAL string - - EIGENDA_PROXY_EIGENDA_GRPC_DISABLE_TLS string - - EIGENDA_PROXY_EIGENDA_CUSTOM_QUORUM_IDS string - - EIGENDA_PROXY_EIGENDA_SIGNER_PRIVATE_KEY_HEX string - - EIGENDA_PROXY_EIGENDA_PUT_BLOB_ENCODING_VERSION string - - EIGENDA_PROXY_EIGENDA_DISABLE_POINT_VERIFICATION_MODE string - - EIGENDA_PROXY_EIGENDA_CONFIRMATION_DEPTH string - - EIGENDA_PROXY_EIGENDA_ETH_RPC string - - EIGENDA_PROXY_EIGENDA_SERVICE_MANAGER_ADDR string - - EIGENDA_PROXY_EIGENDA_PUT_RETRIES string - - EIGENDA_PROXY_EIGENDA_MAX_BLOB_LENGTH string - EIGENDA_PROXY_EIGENDA_V2_DISPERSER_RPC string EIGENDA_PROXY_EIGENDA_V2_GRPC_DISABLE_TLS string @@ -1205,58 +1054,12 @@ type ProxyVars struct { EIGENDA_PROXY_MEMSTORE_PUT_RETURNS_FAILOVER_ERROR string - EIGENDA_PROXY_EIGENDA_CERT_VERIFICATION_DISABLED string - - EIGENDA_PROXY_EIGENDA_CERT_VERIFIER_V1 string - - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G1_PATH string - - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G2_POWER_OF_2_PATH string - - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G2_PATH string - - EIGENDA_PROXY_EIGENDA_TARGET_KZG_G2_TRAILING_PATH string - - EIGENDA_PROXY_EIGENDA_TARGET_CACHE_PATH string - EIGENDA_PROXY_METRICS_ENABLED string - EIGENDA_PROXY_DISPERSER_RPC string - - EIGENDA_PROXY_STATUS_QUERY_TIMEOUT string - - EIGENDA_PROXY_STATUS_QUERY_INTERVAL string - - EIGENDA_PROXY_GRPC_DISABLE_TLS string - - EIGENDA_PROXY_RESPONSE_TIMEOUT string - - EIGENDA_PROXY_CUSTOM_QUORUM_IDS string - - EIGENDA_PROXY_SIGNER_PRIVATE_KEY_HEX string - - EIGENDA_PROXY_PUT_BLOB_ENCODING_VERSION string - - EIGENDA_PROXY_DISABLE_POINT_VERIFICATION_MODE string - EIGENDA_PROXY_EIGENDA_V2_SERVICE_MANAGER_ADDR string EIGENDA_PROXY_EIGENDA_V2_BLS_OPERATOR_STATE_RETRIEVER_ADDR string - EIGENDA_PROXY_ETH_RPC string - - EIGENDA_PROXY_SERVICE_MANAGER_ADDR string - - EIGENDA_PROXY_ETH_CONFIRMATION_DEPTH string - - EIGENDA_PROXY_TARGET_KZG_G1_PATH string - - EIGENDA_PROXY_TARGET_G2_TAU_PATH string - - EIGENDA_PROXY_TARGET_CACHE_PATH string - - EIGENDA_PROXY_MAX_BLOB_LENGTH string - EIGENDA_PROXY_FALLBACK_TARGETS string EIGENDA_PROXY_CACHE_TARGETS string diff --git a/inabox/templates/testconfig-anvil-nochurner.yaml b/inabox/templates/testconfig-anvil-nochurner.yaml index ed54cfce80..8c2f073814 100644 --- a/inabox/templates/testconfig-anvil-nochurner.yaml +++ b/inabox/templates/testconfig-anvil-nochurner.yaml @@ -27,8 +27,6 @@ privateKeys: ecdsaMap: default: privateKey: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - batcher0: - privateKey: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d services: counts: diff --git a/inabox/templates/testconfig-anvil.yaml b/inabox/templates/testconfig-anvil.yaml index 6e04dc716e..e07f8adc9e 100644 --- a/inabox/templates/testconfig-anvil.yaml +++ b/inabox/templates/testconfig-anvil.yaml @@ -25,8 +25,6 @@ privateKeys: ecdsaMap: default: privateKey: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - batcher0: - privateKey: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d services: counts: diff --git a/inabox/tests/setup_disperser_harness.go b/inabox/tests/setup_disperser_harness.go index 44a66023d9..e3af991f71 100644 --- a/inabox/tests/setup_disperser_harness.go +++ b/inabox/tests/setup_disperser_harness.go @@ -159,7 +159,6 @@ func setupLocalStackResources( deployConfig := testbed.DeployResourcesConfig{ LocalStackEndpoint: localstackContainer.Endpoint(), BlobStoreBucketName: config.S3BucketName, - MetadataTableName: config.MetadataTableName, BucketTableName: config.BucketTableName, V2MetadataTableName: config.MetadataTableNameV2, AWSConfig: localstackContainer.GetAWSClientConfig(), diff --git a/node/grpc/server_load_test.go b/node/grpc/server_load_test.go deleted file mode 100644 index 7899f31dc6..0000000000 --- a/node/grpc/server_load_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package grpc_test - -import ( - "context" - "crypto/rand" - "fmt" - "testing" - "time" - - "github.com/Layr-Labs/eigenda/core" - "github.com/Layr-Labs/eigenda/disperser/batcher" - dispatcher "github.com/Layr-Labs/eigenda/disperser/batcher/grpc" - "github.com/Layr-Labs/eigenda/encoding" - "github.com/Layr-Labs/eigenda/encoding/codec" - "github.com/stretchr/testify/assert" -) - -func makeBatch( - t *testing.T, blobSize uint32, numBlobs int, advThreshold, quorumThreshold int, refBlockNumber uint, -) (*core.BatchHeader, map[core.OperatorID][]*core.EncodedBlobMessage) { - p, _, err := makeTestComponents() - assert.NoError(t, err) - asn := &core.StdAssignmentCoordinator{} - - blobHeaders := make([]*core.BlobHeader, numBlobs) - blobChunks := make([][]*encoding.Frame, numBlobs) - blobMessagesByOp := make(map[core.OperatorID][]*core.EncodedBlobMessage) - for i := 0; i < numBlobs; i++ { - // create data - ranData := make([]byte, blobSize) - _, err := rand.Read(ranData) - assert.NoError(t, err) - - data := codec.ConvertByPaddingEmptyByte(ranData) - data = data[:blobSize] - - operatorState, err := chainState.GetOperatorState(context.Background(), 0, []core.QuorumID{0}) - assert.NoError(t, err) - - chunkLength, err := asn.CalculateChunkLength(operatorState, uint(encoding.GetBlobLength(blobSize)), 0, - &core.SecurityParam{ - QuorumID: 0, - AdversaryThreshold: uint8(advThreshold), - ConfirmationThreshold: uint8(quorumThreshold), - }) - assert.NoError(t, err) - - blobQuorumInfo := &core.BlobQuorumInfo{ - SecurityParam: core.SecurityParam{ - QuorumID: 0, - AdversaryThreshold: uint8(advThreshold), - ConfirmationThreshold: uint8(quorumThreshold), - }, - ChunkLength: chunkLength, - } - - // encode data - - assignments, info, err := asn.GetAssignments(operatorState, uint(encoding.GetBlobLength(blobSize)), blobQuorumInfo) - assert.NoError(t, err) - quorumInfo := batcher.QuorumInfo{ - Assignments: assignments, - Info: info, - QuantizationFactor: batcher.QuantizationFactor, - } - - params := encoding.ParamsFromMins(uint64(chunkLength), quorumInfo.Info.TotalChunks) - t.Logf("Encoding params: ChunkLength: %d, NumChunks: %d", params.ChunkLength, params.NumChunks) - commits, chunks, err := p.EncodeAndProve(data, params) - assert.NoError(t, err) - blobChunks[i] = chunks - - chunkBytes := make([][]byte, len(chunks)) - for _, c := range chunks { - serialized, err := c.SerializeGob() - assert.NotNil(t, err) - chunkBytes = append(chunkBytes, serialized) - } - - // populate blob header - blobHeaders[i] = &core.BlobHeader{ - BlobCommitments: commits, - QuorumInfos: []*core.BlobQuorumInfo{blobQuorumInfo}, - } - - // populate blob messages - for opID, assignment := range quorumInfo.Assignments { - blobMessagesByOp[opID] = append(blobMessagesByOp[opID], &core.EncodedBlobMessage{ - BlobHeader: blobHeaders[i], - EncodedBundles: make(core.EncodedBundles), - }) - blobMessagesByOp[opID][i].EncodedBundles[0].Format = core.GobChunkEncodingFormat - blobMessagesByOp[opID][i].EncodedBundles[0].ChunkLen = int(params.ChunkLength) - blobMessagesByOp[opID][i].EncodedBundles[0].Chunks = append(blobMessagesByOp[opID][i].EncodedBundles[0].Chunks, chunkBytes[assignment.StartIndex:assignment.StartIndex+assignment.NumChunks]...) - } - } - - batchHeader := &core.BatchHeader{ - ReferenceBlockNumber: refBlockNumber, - BatchRoot: [32]byte{}, - } - _, err = batchHeader.SetBatchRoot(blobHeaders) - assert.NoError(t, err) - return batchHeader, blobMessagesByOp -} - -func TestStoreChunks(t *testing.T) { - t.Skip("Skipping TestStoreChunks as it's meant to be tested manually to measure performance") - - server := newTestServer(t, false) - // 50 X 200 KiB blobs - batchHeader, blobMessagesByOp := makeBatch(t, 200*1024, 50, 80, 100, 1) - numTotalChunks := 0 - for i := range blobMessagesByOp[opID] { - numTotalChunks += len(blobMessagesByOp[opID][i].EncodedBundles[0].Chunks) - } - t.Logf("Batch numTotalChunks: %d", numTotalChunks) - req, totalSize, err := dispatcher.GetStoreChunksRequest(blobMessagesByOp[opID], batchHeader, false) - fmt.Println("totalSize", totalSize) - assert.NoError(t, err) - assert.Equal(t, int64(26214400), totalSize) - - timer := time.Now() - reply, err := server.StoreChunks(context.Background(), req) - t.Log("StoreChunks took", time.Since(timer)) - assert.NoError(t, err) - assert.NotNil(t, reply.GetSignature()) -} diff --git a/test/testbed/deploy_localstack_resources.go b/test/testbed/deploy_localstack_resources.go index d8e7bf34b1..ac0ada21ee 100644 --- a/test/testbed/deploy_localstack_resources.go +++ b/test/testbed/deploy_localstack_resources.go @@ -10,7 +10,6 @@ import ( test_utils "github.com/Layr-Labs/eigenda/common/aws/dynamodb/utils" "github.com/Layr-Labs/eigenda/common/store" "github.com/Layr-Labs/eigenda/core/meterer" - "github.com/Layr-Labs/eigenda/disperser/common/blobstore" blobstorev2 "github.com/Layr-Labs/eigenda/disperser/common/v2/blobstore" "github.com/Layr-Labs/eigensdk-go/logging" awssdk "github.com/aws/aws-sdk-go-v2/aws" @@ -28,9 +27,6 @@ type DeployResourcesConfig struct { // Required: AWS client config AWSConfig aws.ClientConfig - // Optional: Metadata table name, defaults to "test-eigenda-blobmetadata" - MetadataTableName string - // Optional: Bucket table name, defaults to "test-eigenda-bucket" BucketTableName string @@ -70,9 +66,6 @@ func DeployResources(ctx context.Context, config DeployResourcesConfig) error { if config.V2PaymentPrefix == "" { config.V2PaymentPrefix = "e2e_v2_" } - if config.MetadataTableName == "" { - config.MetadataTableName = "test-eigenda-blobmetadata" - } if config.BucketTableName == "" { config.BucketTableName = "test-eigenda-bucket" } @@ -94,16 +87,6 @@ func DeployResources(ctx context.Context, config DeployResourcesConfig) error { return fmt.Errorf("failed to create S3 bucket: %w", err) } - // Create metadata table - if config.MetadataTableName != "" { - _, err := test_utils.CreateTable(ctx, cfg, config.MetadataTableName, - blobstore.GenerateTableSchema(config.MetadataTableName, 10, 10)) - if err != nil { - return fmt.Errorf("failed to create metadata table %s: %w", config.MetadataTableName, err) - } - logger.Info("Created metadata table", "table", config.MetadataTableName) - } - // Create bucket table if config.BucketTableName != "" { _, err := test_utils.CreateTable(ctx, cfg, config.BucketTableName,