From 6ada19985390033f86bd11b1b6a4ae8eb51c5ba1 Mon Sep 17 00:00:00 2001 From: Ian Shim <100327837+ian-shim@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:57:50 -0800 Subject: [PATCH] [v2] Disperser request validation (#915) --- disperser/apiserver/disperse_blob_v2.go | 44 +++++++------ disperser/apiserver/server_v2_test.go | 87 +++++++++++++++++++++++-- disperser/cmd/apiserver/flags/flags.go | 3 +- encoding/data.go | 47 +++++++++++++ 4 files changed, 156 insertions(+), 25 deletions(-) diff --git a/disperser/apiserver/disperse_blob_v2.go b/disperser/apiserver/disperse_blob_v2.go index 304724eb64..5fc160b677 100644 --- a/disperser/apiserver/disperse_blob_v2.go +++ b/disperser/apiserver/disperse_blob_v2.go @@ -64,22 +64,33 @@ func (s *DispersalServerV2) StoreBlob(ctx context.Context, data []byte, blobHead func (s *DispersalServerV2) validateDispersalRequest(req *pb.DisperseBlobRequest) error { data := req.GetData() blobSize := len(data) - if uint64(blobSize) > s.maxNumSymbolsPerBlob*encoding.BYTES_PER_SYMBOL { - return api.NewErrorInvalidArg(fmt.Sprintf("blob size cannot exceed %v bytes", s.maxNumSymbolsPerBlob*encoding.BYTES_PER_SYMBOL)) - } if blobSize == 0 { return api.NewErrorInvalidArg("blob size must be greater than 0") } + blobLength := encoding.GetBlobLengthPowerOf2(uint(blobSize)) + if blobLength > uint(s.maxNumSymbolsPerBlob) { + return api.NewErrorInvalidArg("blob size too big") + } blobHeaderProto := req.GetBlobHeader() if blobHeaderProto.GetCommitment() == nil { return api.NewErrorInvalidArg("blob header must contain commitments") } + if len(blobHeaderProto.GetQuorumNumbers()) == 0 { + return api.NewErrorInvalidArg("blob header must contain at least one quorum number") + } + if len(blobHeaderProto.GetQuorumNumbers()) > int(s.onchainState.QuorumCount) { return api.NewErrorInvalidArg(fmt.Sprintf("too many quorum numbers specified: maximum is %d", s.onchainState.QuorumCount)) } + for _, quorum := range blobHeaderProto.GetQuorumNumbers() { + if quorum > corev2.MaxQuorumID || uint8(quorum) >= s.onchainState.QuorumCount { + return api.NewErrorInvalidArg(fmt.Sprintf("invalid quorum number %d; maximum is %d", quorum, s.onchainState.QuorumCount)) + } + } + // validate every 32 bytes is a valid field element _, err := rs.ToFrArray(data) if err != nil { @@ -87,10 +98,6 @@ func (s *DispersalServerV2) validateDispersalRequest(req *pb.DisperseBlobRequest return api.NewErrorInvalidArg("encountered an error to convert a 32-bytes into a valid field element, please use the correct format where every 32bytes(big-endian) is less than 21888242871839275222246405745257275088548364400416034343698204186575808495617") } - if !containsRequiredQuorum(s.onchainState.RequiredQuorums, blobHeaderProto.GetQuorumNumbers()) { - return api.NewErrorInvalidArg(fmt.Sprintf("request must contain at least one required quorum: %v does not specify any of %v", blobHeaderProto.GetQuorumNumbers(), s.onchainState.RequiredQuorums)) - } - if _, ok := s.onchainState.BlobVersionParameters[corev2.BlobVersion(blobHeaderProto.GetVersion())]; !ok { validVersions := make([]int32, 0, len(s.onchainState.BlobVersionParameters)) for version := range s.onchainState.BlobVersionParameters { @@ -107,18 +114,17 @@ func (s *DispersalServerV2) validateDispersalRequest(req *pb.DisperseBlobRequest return api.NewErrorInvalidArg(fmt.Sprintf("authentication failed: %s", err.Error())) } - // TODO(ian-shim): validate commitment, length is power of 2 and less than maxNumSymbolsPerBlob, payment metadata - - return nil -} + if len(blobHeader.PaymentMetadata.AccountID) == 0 || blobHeader.PaymentMetadata.BinIndex == 0 || blobHeader.PaymentMetadata.CumulativePayment == nil { + return api.NewErrorInvalidArg("invalid payment metadata") + } -func containsRequiredQuorum(requiredQuorums []uint8, quorumNumbers []uint32) bool { - for _, required := range requiredQuorums { - for _, quorum := range quorumNumbers { - if uint8(quorum) == required { - return true - } - } + commitments, err := s.prover.GetCommitmentsForPaddedLength(data) + if err != nil { + return api.NewErrorInternal(fmt.Sprintf("failed to get commitments: %v", err)) + } + if !commitments.Equal(&blobHeader.BlobCommitments) { + return api.NewErrorInvalidArg("invalid blob commitment") } - return false + + return nil } diff --git a/disperser/apiserver/server_v2_test.go b/disperser/apiserver/server_v2_test.go index e70fb3c823..5ef2fe8dac 100644 --- a/disperser/apiserver/server_v2_test.go +++ b/disperser/apiserver/server_v2_test.go @@ -109,7 +109,7 @@ func TestV2DisperseBlobRequestValidation(t *testing.T) { data := make([]byte, 50) _, err := rand.Read(data) assert.NoError(t, err) - + signer := auth.NewLocalBlobRequestSigner(privateKeyHex) data = codec.ConvertByPaddingEmptyByte(data) commitments, err := prover.GetCommitmentsForPaddedLength(data) assert.NoError(t, err) @@ -150,10 +150,10 @@ func TestV2DisperseBlobRequestValidation(t *testing.T) { }) assert.ErrorContains(t, err, "too many quorum numbers specified") - // request without required quorums + // request with invalid quorum invalidReqProto = &pbcommonv2.BlobHeader{ Version: 0, - QuorumNumbers: []uint32{2}, + QuorumNumbers: []uint32{2, 54}, Commitment: commitmentProto, PaymentHeader: &pbcommon.PaymentHeader{ AccountId: accountID, @@ -165,7 +165,7 @@ func TestV2DisperseBlobRequestValidation(t *testing.T) { Data: data, BlobHeader: invalidReqProto, }) - assert.ErrorContains(t, err, "request must contain at least one required quorum") + assert.ErrorContains(t, err, "invalid quorum") // request with invalid blob version invalidReqProto = &pbcommonv2.BlobHeader{ @@ -201,6 +201,83 @@ func TestV2DisperseBlobRequestValidation(t *testing.T) { BlobHeader: invalidReqProto, }) assert.ErrorContains(t, err, "authentication failed") + + // request with invalid payment metadata + invalidReqProto = &pbcommonv2.BlobHeader{ + Version: 0, + QuorumNumbers: []uint32{0, 1}, + Commitment: commitmentProto, + PaymentHeader: &pbcommon.PaymentHeader{ + AccountId: accountID, + BinIndex: 0, + CumulativePayment: big.NewInt(100).Bytes(), + }, + } + blobHeader, err := corev2.BlobHeaderFromProtobuf(invalidReqProto) + assert.NoError(t, err) + sig, err := signer.SignBlobRequest(blobHeader) + assert.NoError(t, err) + invalidReqProto.Signature = sig + + _, err = c.DispersalServerV2.DisperseBlob(context.Background(), &pbv2.DisperseBlobRequest{ + Data: data, + BlobHeader: invalidReqProto, + }) + assert.ErrorContains(t, err, "invalid payment metadata") + + // request with invalid commitment + invalidCommitment := commitmentProto + invalidCommitment.Length = commitmentProto.Length - 1 + invalidReqProto = &pbcommonv2.BlobHeader{ + Version: 0, + QuorumNumbers: []uint32{0, 1}, + Commitment: invalidCommitment, + PaymentHeader: &pbcommon.PaymentHeader{ + AccountId: accountID, + BinIndex: 5, + CumulativePayment: big.NewInt(100).Bytes(), + }, + } + blobHeader, err = corev2.BlobHeaderFromProtobuf(invalidReqProto) + assert.NoError(t, err) + sig, err = signer.SignBlobRequest(blobHeader) + assert.NoError(t, err) + invalidReqProto.Signature = sig + _, err = c.DispersalServerV2.DisperseBlob(context.Background(), &pbv2.DisperseBlobRequest{ + Data: data, + BlobHeader: invalidReqProto, + }) + assert.ErrorContains(t, err, "invalid blob commitment") + + // request with blob size exceeding the limit + data = make([]byte, 321) + _, err = rand.Read(data) + assert.NoError(t, err) + data = codec.ConvertByPaddingEmptyByte(data) + commitments, err = prover.GetCommitmentsForPaddedLength(data) + assert.NoError(t, err) + commitmentProto, err = commitments.ToProtobuf() + assert.NoError(t, err) + validHeader := &pbcommonv2.BlobHeader{ + Version: 0, + QuorumNumbers: []uint32{0, 1}, + Commitment: commitmentProto, + PaymentHeader: &pbcommon.PaymentHeader{ + AccountId: accountID, + BinIndex: 5, + CumulativePayment: big.NewInt(100).Bytes(), + }, + } + blobHeader, err = corev2.BlobHeaderFromProtobuf(validHeader) + assert.NoError(t, err) + sig, err = signer.SignBlobRequest(blobHeader) + assert.NoError(t, err) + validHeader.Signature = sig + _, err = c.DispersalServerV2.DisperseBlob(context.Background(), &pbv2.DisperseBlobRequest{ + Data: data, + BlobHeader: validHeader, + }) + assert.ErrorContains(t, err, "blob size too big") } func TestV2GetBlobStatus(t *testing.T) { @@ -362,7 +439,7 @@ func newTestServerV2(t *testing.T) *testComponents { s := apiserver.NewDispersalServerV2(disperser.ServerConfig{ GrpcPort: "51002", GrpcTimeout: 1 * time.Second, - }, rateConfig, blobStore, blobMetadataStore, chainReader, nil, auth.NewAuthenticator(), prover, 100, time.Hour, logger) + }, rateConfig, blobStore, blobMetadataStore, chainReader, nil, auth.NewAuthenticator(), prover, 10, time.Hour, logger) err = s.RefreshOnchainState(context.Background()) assert.NoError(t, err) diff --git a/disperser/cmd/apiserver/flags/flags.go b/disperser/cmd/apiserver/flags/flags.go index 45eb411339..4bffb6f67e 100644 --- a/disperser/cmd/apiserver/flags/flags.go +++ b/disperser/cmd/apiserver/flags/flags.go @@ -9,6 +9,7 @@ import ( "github.com/Layr-Labs/eigenda/common/geth" "github.com/Layr-Labs/eigenda/common/ratelimit" "github.com/Layr-Labs/eigenda/disperser/apiserver" + "github.com/Layr-Labs/eigenda/encoding" "github.com/Layr-Labs/eigenda/encoding/kzg" "github.com/urfave/cli" ) @@ -150,7 +151,7 @@ var ( MaxNumSymbolsPerBlob = cli.UintFlag{ Name: common.PrefixFlag(FlagPrefix, "max-num-symbols-per-blob"), Usage: "max number of symbols per blob. This flag is only relevant in v2", - Value: 65_536, + Value: 16 * 1024 * 1024 / encoding.BYTES_PER_SYMBOL, // this should allow for 16MiB blobs EnvVar: common.PrefixEnvVar(envVarPrefix, "MAX_NUM_SYMBOLS_PER_BLOB"), Required: false, } diff --git a/encoding/data.go b/encoding/data.go index d74ef8950a..79f9492c50 100644 --- a/encoding/data.go +++ b/encoding/data.go @@ -1,6 +1,8 @@ package encoding import ( + "bytes" + pbcommon "github.com/Layr-Labs/eigenda/api/grpc/common" "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fr" @@ -54,6 +56,51 @@ func (c *BlobCommitments) ToProtobuf() (*pbcommon.BlobCommitment, error) { }, nil } +// Equal checks if two BlobCommitments are equal +func (c *BlobCommitments) Equal(c1 *BlobCommitments) bool { + if c.Length != c1.Length { + return false + } + + cCommitment, err := c.Commitment.Serialize() + if err != nil { + return false + } + c1Commitment, err := c1.Commitment.Serialize() + if err != nil { + return false + } + if !bytes.Equal(cCommitment, c1Commitment) { + return false + } + + cLengthCommitment, err := c.LengthCommitment.Serialize() + if err != nil { + return false + } + c1LengthCommitment, err := c1.LengthCommitment.Serialize() + if err != nil { + return false + } + if !bytes.Equal(cLengthCommitment, c1LengthCommitment) { + return false + } + + cLengthProof, err := c.LengthProof.Serialize() + if err != nil { + return false + } + c1LengthProof, err := c1.LengthProof.Serialize() + if err != nil { + return false + } + if !bytes.Equal(cLengthProof, c1LengthProof) { + return false + } + + return true +} + func BlobCommitmentsFromProtobuf(c *pbcommon.BlobCommitment) (*BlobCommitments, error) { commitment, err := new(G1Commitment).Deserialize(c.Commitment) if err != nil {