From 5f38d4268869fb1602a55963c014a8ac5e3bfd64 Mon Sep 17 00:00:00 2001 From: Jian Xiao <99709935+jianoaix@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:43:14 -0800 Subject: [PATCH] Provide signer and nonsigner operator IDs in blob attestation-info API (#1266) --- disperser/dataapi/docs/v2/V2_docs.go | 30 +++++- disperser/dataapi/docs/v2/V2_swagger.json | 30 +++++- disperser/dataapi/docs/v2/V2_swagger.yaml | 21 +++- disperser/dataapi/v2/blobs.go | 54 +++++++++- disperser/dataapi/v2/server_v2.go | 7 +- disperser/dataapi/v2/server_v2_test.go | 122 ++++++++++++++++++++-- 6 files changed, 247 insertions(+), 17 deletions(-) diff --git a/disperser/dataapi/docs/v2/V2_docs.go b/disperser/dataapi/docs/v2/V2_docs.go index 05e3ab757e..f3debb0727 100644 --- a/disperser/dataapi/docs/v2/V2_docs.go +++ b/disperser/dataapi/docs/v2/V2_docs.go @@ -979,6 +979,32 @@ const docTemplateV2 = `{ } } }, + "v2.AttestationInfo": { + "type": "object", + "properties": { + "attestation": { + "$ref": "#/definitions/github_com_Layr-Labs_eigenda_core_v2.Attestation" + }, + "nonsigning_operator_ids": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "signing_operator_ids": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "v2.BatchFeedResponse": { "type": "object", "properties": { @@ -1039,8 +1065,8 @@ const docTemplateV2 = `{ "v2.BlobAttestationInfoResponse": { "type": "object", "properties": { - "attestation": { - "$ref": "#/definitions/github_com_Layr-Labs_eigenda_core_v2.Attestation" + "attestation_info": { + "$ref": "#/definitions/v2.AttestationInfo" }, "batch_header_hash": { "type": "string" diff --git a/disperser/dataapi/docs/v2/V2_swagger.json b/disperser/dataapi/docs/v2/V2_swagger.json index 8b394d8d6c..dd31b2366f 100644 --- a/disperser/dataapi/docs/v2/V2_swagger.json +++ b/disperser/dataapi/docs/v2/V2_swagger.json @@ -976,6 +976,32 @@ } } }, + "v2.AttestationInfo": { + "type": "object", + "properties": { + "attestation": { + "$ref": "#/definitions/github_com_Layr-Labs_eigenda_core_v2.Attestation" + }, + "nonsigning_operator_ids": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "signing_operator_ids": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "v2.BatchFeedResponse": { "type": "object", "properties": { @@ -1036,8 +1062,8 @@ "v2.BlobAttestationInfoResponse": { "type": "object", "properties": { - "attestation": { - "$ref": "#/definitions/github_com_Layr-Labs_eigenda_core_v2.Attestation" + "attestation_info": { + "$ref": "#/definitions/v2.AttestationInfo" }, "batch_header_hash": { "type": "string" diff --git a/disperser/dataapi/docs/v2/V2_swagger.yaml b/disperser/dataapi/docs/v2/V2_swagger.yaml index 1b79045be8..98d1450d2a 100644 --- a/disperser/dataapi/docs/v2/V2_swagger.yaml +++ b/disperser/dataapi/docs/v2/V2_swagger.yaml @@ -227,6 +227,23 @@ definitions: type: number type: object type: object + v2.AttestationInfo: + properties: + attestation: + $ref: '#/definitions/github_com_Layr-Labs_eigenda_core_v2.Attestation' + nonsigning_operator_ids: + additionalProperties: + items: + type: string + type: array + type: object + signing_operator_ids: + additionalProperties: + items: + type: string + type: array + type: object + type: object v2.BatchFeedResponse: properties: batches: @@ -266,8 +283,8 @@ definitions: type: object v2.BlobAttestationInfoResponse: properties: - attestation: - $ref: '#/definitions/github_com_Layr-Labs_eigenda_core_v2.Attestation' + attestation_info: + $ref: '#/definitions/v2.AttestationInfo' batch_header_hash: type: string blob_inclusion_info: diff --git a/disperser/dataapi/v2/blobs.go b/disperser/dataapi/v2/blobs.go index e54d8d68eb..e9d666597b 100644 --- a/disperser/dataapi/v2/blobs.go +++ b/disperser/dataapi/v2/blobs.go @@ -7,6 +7,7 @@ import ( "strconv" "time" + "github.com/Layr-Labs/eigenda/core" corev2 "github.com/Layr-Labs/eigenda/core/v2" "github.com/Layr-Labs/eigenda/disperser/common/v2/blobstore" "github.com/gin-gonic/gin" @@ -225,6 +226,8 @@ func (s *ServerV2) FetchBlobCertificate(c *gin.Context) { // @Router /blobs/{blob_key}/attestation-info [get] func (s *ServerV2) FetchBlobAttestationInfo(c *gin.Context) { handlerStart := time.Now() + + ctx := c.Request.Context() blobKey, err := corev2.HexToBlobKey(c.Param("blob_key")) if err != nil { s.metrics.IncrementInvalidArgRequestNum("FetchBlobAttestationInfo") @@ -246,11 +249,60 @@ func (s *ServerV2) FetchBlobAttestationInfo(c *gin.Context) { return } + // Get quorums that this blob was dispersed to + metadata, err := s.blobMetadataStore.GetBlobMetadata(ctx, blobKey) + if err != nil { + s.metrics.IncrementFailedRequestNum("FetchBlobAttestationInfo") + errorResponse(c, fmt.Errorf("failed to fetch blob metadata: %w", err)) + return + } + blobQuorums := make(map[uint8]struct{}, 0) + for _, q := range metadata.BlobHeader.QuorumNumbers { + blobQuorums[q] = struct{}{} + } + + // Get all nonsigners (of the batch that this blob is part of) + nonsigners := make(map[core.OperatorID]struct{}, 0) + for i := 0; i < len(attestationInfo.Attestation.NonSignerPubKeys); i++ { + opId := attestationInfo.Attestation.NonSignerPubKeys[i].GetOperatorID() + nonsigners[opId] = struct{}{} + } + + // Get all operators at the reference block number + rbn := attestationInfo.Attestation.ReferenceBlockNumber + operatorsByQuorum, err := s.chainReader.GetOperatorStakesForQuorums(ctx, attestationInfo.Attestation.QuorumNumbers, uint32(rbn)) + if err != nil { + s.metrics.IncrementFailedRequestNum("FetchBlobAttestationInfo") + errorResponse(c, fmt.Errorf("failed to fetch operators at reference block number: %w", err)) + return + } + + // Compute the signers and nonsigners for the blob, for each quorum that the blob was dispersed to + signerIds := make(map[uint8][]string, 0) + nonsignerIds := make(map[uint8][]string, 0) + for q, innerMap := range operatorsByQuorum { + // Make sure the blob was dispersed to the quorum + if _, exist := blobQuorums[q]; !exist { + continue + } + for _, op := range innerMap { + if _, exist := nonsigners[op.OperatorID]; exist { + nonsignerIds[q] = append(nonsignerIds[q], op.OperatorID.Hex()) + } else { + signerIds[q] = append(signerIds[q], op.OperatorID.Hex()) + } + } + } + response := &BlobAttestationInfoResponse{ BlobKey: blobKey.Hex(), BatchHeaderHash: hex.EncodeToString(batchHeaderHash[:]), InclusionInfo: attestationInfo.InclusionInfo, - Attestation: attestationInfo.Attestation, + AttestationInfo: &AttestationInfo{ + Attestation: attestationInfo.Attestation, + SigningOperatorIds: signerIds, + NonsigningOperatorIds: nonsignerIds, + }, } s.metrics.IncrementSuccessfulRequestNum("FetchBlobAttestationInfo") diff --git a/disperser/dataapi/v2/server_v2.go b/disperser/dataapi/v2/server_v2.go index e4867c8901..0ca3ff10c1 100644 --- a/disperser/dataapi/v2/server_v2.go +++ b/disperser/dataapi/v2/server_v2.go @@ -71,11 +71,16 @@ type ( Certificate *corev2.BlobCertificate `json:"blob_certificate"` } + AttestationInfo struct { + Attestation *corev2.Attestation `json:"attestation"` + NonsigningOperatorIds map[uint8][]string `json:"nonsigning_operator_ids"` + SigningOperatorIds map[uint8][]string `json:"signing_operator_ids"` + } BlobAttestationInfoResponse struct { BlobKey string `json:"blob_key"` BatchHeaderHash string `json:"batch_header_hash"` InclusionInfo *corev2.BlobInclusionInfo `json:"blob_inclusion_info"` - Attestation *corev2.Attestation `json:"attestation"` + AttestationInfo *AttestationInfo `json:"attestation_info"` } BlobInfo struct { diff --git a/disperser/dataapi/v2/server_v2_test.go b/disperser/dataapi/v2/server_v2_test.go index a9961adc91..599e507997 100644 --- a/disperser/dataapi/v2/server_v2_test.go +++ b/disperser/dataapi/v2/server_v2_test.go @@ -411,6 +411,7 @@ func TestFetchBlobFeed(t *testing.T) { // Actually create blobs firstBlobKeys := make([][32]byte, 3) + dynamoKeys := make([]commondynamodb.Key, numBlobs) for i := 0; i < numBlobs; i++ { blobHeader := makeBlobHeaderV2(t) blobKey, err := blobHeader.BlobKey() @@ -435,11 +436,17 @@ func TestFetchBlobFeed(t *testing.T) { } err = blobMetadataStore.PutBlobMetadata(ctx, metadata) require.NoError(t, err) + dynamoKeys[i] = commondynamodb.Key{ + "PK": &types.AttributeValueMemberS{Value: "BlobKey#" + blobKey.Hex()}, + "SK": &types.AttributeValueMemberS{Value: "BlobMetadata"}, + } } sort.Slice(firstBlobKeys, func(i, j int) bool { return bytes.Compare(firstBlobKeys[i][:], firstBlobKeys[j][:]) < 0 }) + defer deleteItems(t, dynamoKeys) + r.GET("/v2/blobs/feed", testDataApiServerV2.FetchBlobFeed) t.Run("invalid params", func(t *testing.T) { @@ -597,12 +604,22 @@ func TestFetchBlobAttestationInfo(t *testing.T) { r := setUpRouter() // Set up blob inclusion info + now := time.Now() blobHeader := makeBlobHeaderV2(t) + metadata := &commonv2.BlobMetadata{ + BlobHeader: blobHeader, + BlobStatus: commonv2.Queued, + Expiry: uint64(now.Add(time.Hour).Unix()), + NumRetries: 0, + UpdatedAt: uint64(now.UnixNano()), + } + err := blobMetadataStore.PutBlobMetadata(context.Background(), metadata) + require.NoError(t, err) blobKey, err := blobHeader.BlobKey() require.NoError(t, err) batchHeader := &corev2.BatchHeader{ BatchRoot: [32]byte{1, 2, 3}, - ReferenceBlockNumber: 100, + ReferenceBlockNumber: 10, } bhh, err := batchHeader.Hash() assert.NoError(t, err) @@ -627,18 +644,23 @@ func TestFetchBlobAttestationInfo(t *testing.T) { require.Equal(t, http.StatusInternalServerError, w.Result().StatusCode) }) + operatorPubKeys := []*core.G1Point{ + core.NewG1Point(big.NewInt(1), big.NewInt(2)), + core.NewG1Point(big.NewInt(3), big.NewInt(4)), + core.NewG1Point(big.NewInt(4), big.NewInt(5)), + core.NewG1Point(big.NewInt(5), big.NewInt(6)), + } + // Set up attestation keyPair, err := core.GenRandomBlsKeys() assert.NoError(t, err) apk := keyPair.GetPubKeyG2() + nonsignerPubKeys := operatorPubKeys[:2] attestation := &corev2.Attestation{ - BatchHeader: batchHeader, - AttestedAt: uint64(time.Now().UnixNano()), - NonSignerPubKeys: []*core.G1Point{ - core.NewG1Point(big.NewInt(1), big.NewInt(2)), - core.NewG1Point(big.NewInt(3), big.NewInt(4)), - }, - APKG2: apk, + BatchHeader: batchHeader, + AttestedAt: uint64(time.Now().UnixNano()), + NonSignerPubKeys: nonsignerPubKeys, + APKG2: apk, QuorumAPKs: map[uint8]*core.G1Point{ 0: core.NewG1Point(big.NewInt(5), big.NewInt(6)), 1: core.NewG1Point(big.NewInt(7), big.NewInt(8)), @@ -655,6 +677,51 @@ func TestFetchBlobAttestationInfo(t *testing.T) { err = blobMetadataStore.PutAttestation(ctx, attestation) assert.NoError(t, err) + operatorStakesByBlock := map[uint32]core.OperatorStakes{ + 10: core.OperatorStakes{ + 0: { + 0: { + OperatorID: operatorPubKeys[0].GetOperatorID(), + Stake: big.NewInt(2), + }, + 1: { + OperatorID: operatorPubKeys[1].GetOperatorID(), + Stake: big.NewInt(2), + }, + 2: { + OperatorID: operatorPubKeys[2].GetOperatorID(), + Stake: big.NewInt(3), + }, + }, + 1: { + 0: { + OperatorID: operatorPubKeys[0].GetOperatorID(), + Stake: big.NewInt(2), + }, + 1: { + OperatorID: operatorPubKeys[2].GetOperatorID(), + Stake: big.NewInt(2), + }, + 2: { + OperatorID: operatorPubKeys[3].GetOperatorID(), + Stake: big.NewInt(2), + }, + }, + 2: { + 1: { + OperatorID: operatorPubKeys[0].GetOperatorID(), + Stake: big.NewInt(2), + }, + }, + }, + } + mockTx.On("GetOperatorStakesForQuorums").Return( + func(quorums []core.QuorumID, blockNum uint32) core.OperatorStakes { + return operatorStakesByBlock[blockNum] + }, + nil, + ) + t.Run("found attestation info", func(t *testing.T) { reqStr := fmt.Sprintf("/v2/blobs/%s/attestation-info", blobKey.Hex()) w := executeRequest(t, r, http.MethodGet, reqStr) @@ -663,9 +730,40 @@ func TestFetchBlobAttestationInfo(t *testing.T) { assert.Equal(t, blobKey.Hex(), response.BlobKey) assert.Equal(t, hex.EncodeToString(bhh[:]), response.BatchHeaderHash) assert.Equal(t, inclusionInfo, response.InclusionInfo) - assert.Equal(t, attestation, response.Attestation) + assert.Equal(t, attestation, response.AttestationInfo.Attestation) + + signers := map[uint8][]string{ + 0: { + operatorPubKeys[2].GetOperatorID().Hex(), + }, + 1: { + operatorPubKeys[2].GetOperatorID().Hex(), + operatorPubKeys[3].GetOperatorID().Hex(), + }, + } + nonsigners := map[uint8][]string{ + 0: { + operatorPubKeys[0].GetOperatorID().Hex(), + operatorPubKeys[1].GetOperatorID().Hex(), + }, + 1: { + operatorPubKeys[0].GetOperatorID().Hex(), + }, + } + for key, expectedSigners := range signers { + actualSigners, exists := response.AttestationInfo.SigningOperatorIds[key] + require.True(t, exists) + assert.ElementsMatch(t, expectedSigners, actualSigners) + } + for key, expectedNonsigners := range nonsigners { + actualNonsigners, exists := response.AttestationInfo.NonsigningOperatorIds[key] + require.True(t, exists) + assert.ElementsMatch(t, expectedNonsigners, actualNonsigners) + } }) + mockTx.ExpectedCalls = nil + mockTx.Calls = nil deleteItems(t, []commondynamodb.Key{ { "PK": &types.AttributeValueMemberS{Value: "BatchHeader#" + hex.EncodeToString(bhh[:])}, @@ -679,6 +777,10 @@ func TestFetchBlobAttestationInfo(t *testing.T) { "PK": &types.AttributeValueMemberS{Value: "BlobKey#" + blobKey.Hex()}, "SK": &types.AttributeValueMemberS{Value: "BatchHeader#" + hex.EncodeToString(bhh[:])}, }, + { + "PK": &types.AttributeValueMemberS{Value: "BlobKey#" + blobKey.Hex()}, + "SK": &types.AttributeValueMemberS{Value: "BlobMetadata"}, + }, }) } @@ -1383,6 +1485,8 @@ func TestFetchOperatorSigningInfo(t *testing.T) { }) }) + mockTx.ExpectedCalls = nil + mockTx.Calls = nil } func TestCheckOperatorsLiveness(t *testing.T) {