diff --git a/api/clients/codecs/polynomial_form.go b/api/clients/codecs/polynomial_form.go new file mode 100644 index 0000000000..5f8f53b267 --- /dev/null +++ b/api/clients/codecs/polynomial_form.go @@ -0,0 +1,11 @@ +package codecs + +// PolynomialForm is an enum that represents the different ways that a blob polynomial may be represented +type PolynomialForm uint + +const ( + // Eval is short for "evaluation form". The field elements represent the evaluation at the polynomial's roots of unity + Eval PolynomialForm = iota + // Coeff is short for "coefficient form". The field elements represent the coefficients of the polynomial + Coeff +) \ No newline at end of file diff --git a/api/clients/v2/config.go b/api/clients/v2/config.go new file mode 100644 index 0000000000..2982fb0c76 --- /dev/null +++ b/api/clients/v2/config.go @@ -0,0 +1,35 @@ +package clients + +import ( + "time" + + "github.com/Layr-Labs/eigenda/api/clients/codecs" +) + +// EigenDAClientConfig contains configuration values for EigenDAClient +type EigenDAClientConfig struct { + // The blob encoding version to use when writing and reading blobs + BlobEncodingVersion codecs.BlobEncodingVersion + + // The Ethereum RPC URL to use for querying the Ethereum blockchain. + EthRpcUrl string + + // The address of the EigenDABlobVerifier contract + EigenDABlobVerifierAddr string + + // BlobPolynomialForm is the form that the blob polynomial is commited to and dispersed in, as well as the form the + // blob polynomial will be received in from the relay. + // + // The chosen form dictates how the KZG commitment made to the blob can be used. If the polynomial is in Coeff form + // when committed to, then it will be possible to open points on the KZG commitment to prove that the field elements + // correspond to the commitment. If the polynomial is in Eval form when committed to, then it will not be possible + // to create a commitment opening: the blob will need to be supplied in its entirety to perform a verification that + // any part of the data matches the KZG commitment. + BlobPolynomialForm codecs.PolynomialForm + + // The timeout duration for relay calls to retrieve blobs. + RelayTimeout time.Duration + + // The timeout duration for contract calls + ContractCallTimeout time.Duration +} diff --git a/api/clients/v2/eigenda_client.go b/api/clients/v2/eigenda_client.go new file mode 100644 index 0000000000..428cfcd015 --- /dev/null +++ b/api/clients/v2/eigenda_client.go @@ -0,0 +1,280 @@ +package clients + +import ( + "context" + "errors" + "fmt" + "math/rand" + + "github.com/Layr-Labs/eigenda/api/clients/codecs" + "github.com/Layr-Labs/eigenda/api/clients/v2/verification" + "github.com/Layr-Labs/eigenda/common/geth" + contractEigenDABlobVerifier "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDABlobVerifier" + core "github.com/Layr-Labs/eigenda/core/v2" + "github.com/Layr-Labs/eigenda/encoding" + "github.com/Layr-Labs/eigensdk-go/logging" + "github.com/consensys/gnark-crypto/ecc/bn254" + gethcommon "github.com/ethereum/go-ethereum/common" +) + +// EigenDAClient provides the ability to get payloads from the relay subsystem, and to send new payloads to the disperser. +// +// This struct is not threadsafe. +type EigenDAClient struct { + log logging.Logger + // doesn't need to be cryptographically secure, as it's only used to distribute load across relays + random *rand.Rand + clientConfig *EigenDAClientConfig + codec codecs.BlobCodec + relayClient RelayClient + g1Srs []bn254.G1Affine + blobVerifier verification.IBlobVerifier +} + +// BuildEigenDAClient builds an EigenDAClient from config structs. +func BuildEigenDAClient( + log logging.Logger, + clientConfig *EigenDAClientConfig, + ethConfig geth.EthClientConfig, + relayClientConfig *RelayClientConfig, + g1Srs []bn254.G1Affine) (*EigenDAClient, error) { + + relayClient, err := NewRelayClient(relayClientConfig, log) + if err != nil { + return nil, fmt.Errorf("new relay client: %w", err) + } + + ethClient, err := geth.NewClient(ethConfig, gethcommon.Address{}, 0, log) + if err != nil { + return nil, fmt.Errorf("new eth client: %w", err) + } + + blobVerifier, err := verification.NewBlobVerifier(ethClient, clientConfig.EigenDABlobVerifierAddr) + if err != nil { + return nil, fmt.Errorf("new blob verifier: %w", err) + } + + codec, err := createCodec(clientConfig) + if err != nil { + return nil, err + } + + return NewEigenDAClient( + log, + rand.New(rand.NewSource(rand.Int63())), + clientConfig, + relayClient, + blobVerifier, + codec, + g1Srs) +} + +// NewEigenDAClient assembles an EigenDAClient from subcomponents that have already been constructed and initialized. +func NewEigenDAClient( + log logging.Logger, + random *rand.Rand, + clientConfig *EigenDAClientConfig, + relayClient RelayClient, + blobVerifier verification.IBlobVerifier, + codec codecs.BlobCodec, + g1Srs []bn254.G1Affine) (*EigenDAClient, error) { + + return &EigenDAClient{ + log: log, + random: random, + clientConfig: clientConfig, + codec: codec, + relayClient: relayClient, + blobVerifier: blobVerifier, + g1Srs: g1Srs, + }, nil +} + +// GetPayload iteratively attempts to fetch a given blob with key blobKey from relays that have it, as claimed by the +// blob certificate. The relays are attempted in random order. +// +// If the blob is successfully retrieved, then the blob is verified. If the verification succeeds, the blob is decoded +// to yield the payload (the original user data), and the payload is returned. +func (c *EigenDAClient) GetPayload( + ctx context.Context, + blobKey core.BlobKey, + eigenDACert *verification.EigenDACert) ([]byte, error) { + + err := c.verifyCertWithTimeout(ctx, eigenDACert) + if err != nil { + return nil, fmt.Errorf("verify cert with timeout for blobKey %v: %w", blobKey, err) + } + + relayKeys := eigenDACert.BlobVerificationProof.BlobCertificate.RelayKeys + relayKeyCount := len(relayKeys) + if relayKeyCount == 0 { + return nil, errors.New("relay key count is zero") + } + + blobCommitmentProto := contractEigenDABlobVerifier.BlobCommitmentBindingToProto( + &eigenDACert.BlobVerificationProof.BlobCertificate.BlobHeader.Commitment) + blobCommitment, err := encoding.BlobCommitmentsFromProtobuf(blobCommitmentProto) + + if err != nil { + return nil, fmt.Errorf("blob commitments from protobuf: %w", err) + } + + // create a randomized array of indices, so that it isn't always the first relay in the list which gets hit + indices := c.random.Perm(relayKeyCount) + + // TODO (litt3): consider creating a utility which can deprioritize relays that fail to respond (or respond maliciously) + + // iterate over relays in random order, until we are able to get the blob from someone + for _, val := range indices { + relayKey := relayKeys[val] + + blob, err := c.getBlobWithTimeout(ctx, relayKey, blobKey) + // if GetBlob returned an error, try calling a different relay + if err != nil { + c.log.Warn("blob couldn't be retrieved from relay", "blobKey", blobKey, "relayKey", relayKey, "error", err) + continue + } + + err = c.verifyBlobAgainstCert(blobKey, relayKey, blob, blobCommitment.Commitment, blobCommitment.Length) + + // An honest relay should never send a blob which doesn't verify + if err != nil { + c.log.Warn("verify blob from relay: %w", err) + continue + } + + payload, err := c.codec.DecodeBlob(blob) + if err != nil { + c.log.Error( + `Blob verification was successful, but decode blob failed! + This is likely a problem with the local blob codec configuration, + but could potentially indicate a maliciously generated blob certificate. + It should not be possible for an honestly generated certificate to verify + for an invalid blob!`, + "blobKey", blobKey, "relayKey", relayKey, "eigenDACert", eigenDACert, "error", err) + return nil, fmt.Errorf("decode blob: %w", err) + } + + return payload, nil + } + + return nil, fmt.Errorf("unable to retrieve blob %v from any relay. relay count: %d", blobKey, relayKeyCount) +} + +// verifyBlobAgainstCert verifies the blob received from a relay against the certificate. +// This method does NOT verify the blob with an eth_call to verifyBlobV2, that must be done separately. +// +// The following verifications are performed in this method: +// 1. Verify that blob isn't empty +// 2. Verify the blob kzg commitment +// 3. Verify that the blob length is less than or equal to the claimed blob length +// +// If all verifications succeed, the method returns nil. Otherwise, it returns an error. +func (c *EigenDAClient) verifyBlobAgainstCert( + blobKey core.BlobKey, + relayKey core.RelayKey, + blob []byte, + kzgCommitment *encoding.G1Commitment, + blobLength uint) error { + + // An honest relay should never send an empty blob + if len(blob) == 0 { + return fmt.Errorf("blob %v received from relay %v had length 0", blobKey, relayKey) + } + + // TODO: in the future, this will be optimized to use fiat shamir transformation for verification, rather than + // regenerating the commitment: https://github.com/Layr-Labs/eigenda/issues/1037 + valid, err := verification.GenerateAndCompareBlobCommitment(c.g1Srs, blob, kzgCommitment) + if err != nil { + return fmt.Errorf( + "generate and compare commitment for blob %v received from relay %v: %w", + blobKey, + relayKey, + err) + } + + if !valid { + return fmt.Errorf("commitment for blob %v is invalid for bytes received from relay %v", blobKey, relayKey) + } + + // Checking that the length returned by the relay is <= the length claimed in the BlobCommitments is sufficient + // here: it isn't necessary to verify the length proof itself, since this will have been done by DA nodes prior to + // signing for availability. + // + // Note that the length in the commitment is the length of the blob in symbols + if uint(len(blob)) > blobLength*encoding.BYTES_PER_SYMBOL { + return fmt.Errorf( + "length for blob %v (%d bytes) received from relay %v is greater than claimed blob length (%d bytes)", + blobKey, + len(blob), + relayKey, + blobLength*encoding.BYTES_PER_SYMBOL) + } + + return nil +} + +// getBlobWithTimeout attempts to get a blob from a given relay, and times out based on config.RelayTimeout +func (c *EigenDAClient) getBlobWithTimeout( + ctx context.Context, + relayKey core.RelayKey, + blobKey core.BlobKey) ([]byte, error) { + + timeoutCtx, cancel := context.WithTimeout(ctx, c.clientConfig.RelayTimeout) + defer cancel() + + return c.relayClient.GetBlob(timeoutCtx, relayKey, blobKey) +} + +// verifyCertWithTimeout verifies an EigenDACert by making a call to VerifyBlobV2. +// +// This method times out after the duration configured in clientConfig.ContractCallTimeout +func (c *EigenDAClient) verifyCertWithTimeout( + ctx context.Context, + eigenDACert *verification.EigenDACert, +) error { + timeoutCtx, cancel := context.WithTimeout(ctx, c.clientConfig.ContractCallTimeout) + defer cancel() + + return c.blobVerifier.VerifyBlobV2(timeoutCtx, eigenDACert) +} + +// GetCodec returns the codec the client uses for encoding and decoding blobs +func (c *EigenDAClient) GetCodec() codecs.BlobCodec { + return c.codec +} + +// Close is responsible for calling close on all internal clients. This method will do its best to close all internal +// clients, even if some closes fail. +// +// Any and all errors returned from closing internal clients will be joined and returned. +// +// This method should only be called once. +func (c *EigenDAClient) Close() error { + relayClientErr := c.relayClient.Close() + + // TODO: this is using join, since there will be more subcomponents requiring closing after adding PUT functionality + return errors.Join(relayClientErr) +} + +// createCodec creates the codec based on client config values +func createCodec(config *EigenDAClientConfig) (codecs.BlobCodec, error) { + lowLevelCodec, err := codecs.BlobEncodingVersionToCodec(config.BlobEncodingVersion) + if err != nil { + return nil, fmt.Errorf("create low level codec: %w", err) + } + + switch config.BlobPolynomialForm { + case codecs.Eval: + // a blob polynomial is already in Eval form after being encoded. Therefore, we use the NoIFFTCodec, which + // doesn't do any further conversion. + return codecs.NewNoIFFTCodec(lowLevelCodec), nil + case codecs.Coeff: + // a blob polynomial starts in Eval form after being encoded. Therefore, we use the IFFT codec to transform + // the blob into Coeff form after initial encoding. This codec also transforms the Coeff form received from the + // relay back into Eval form when decoding. + return codecs.NewIFFTCodec(lowLevelCodec), nil + default: + return nil, fmt.Errorf("unsupported polynomial form: %d", config.BlobPolynomialForm) + } +} diff --git a/api/clients/v2/mock/blob_verifier.go b/api/clients/v2/mock/blob_verifier.go new file mode 100644 index 0000000000..04894307ae --- /dev/null +++ b/api/clients/v2/mock/blob_verifier.go @@ -0,0 +1,47 @@ +// Code generated by mockery v2.50.0. DO NOT EDIT. + +package mock + +import ( + "context" + + "github.com/Layr-Labs/eigenda/api/clients/v2/verification" + "github.com/stretchr/testify/mock" +) + +// MockBlobVerifier is an autogenerated mock type for the MockBlobVerifier type +type MockBlobVerifier struct { + mock.Mock +} + +// VerifyBlobV2 provides a mock function with given fields: ctx, eigenDACert +func (_m *MockBlobVerifier) VerifyBlobV2(ctx context.Context, eigenDACert *verification.EigenDACert) error { + ret := _m.Called(ctx, eigenDACert) + + if len(ret) == 0 { + panic("no return value specified for VerifyBlobV2") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *verification.EigenDACert) error); ok { + r0 = rf(ctx, eigenDACert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockBlobVerifier creates a new instance of MockBlobVerifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockBlobVerifier(t interface { + mock.TestingT + Cleanup(func()) +}) *MockBlobVerifier { + mock := &MockBlobVerifier{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/api/clients/v2/mock/relay_client.go b/api/clients/v2/mock/relay_client.go index ab41b11d22..7f575eaa0e 100644 --- a/api/clients/v2/mock/relay_client.go +++ b/api/clients/v2/mock/relay_client.go @@ -19,7 +19,7 @@ func NewRelayClient() *MockRelayClient { } func (c *MockRelayClient) GetBlob(ctx context.Context, relayKey corev2.RelayKey, blobKey corev2.BlobKey) ([]byte, error) { - args := c.Called(blobKey) + args := c.Called(ctx, relayKey, blobKey) if args.Get(0) == nil { return nil, args.Error(1) } diff --git a/api/clients/v2/test/eigenda_client_test.go b/api/clients/v2/test/eigenda_client_test.go new file mode 100644 index 0000000000..84c7f8babf --- /dev/null +++ b/api/clients/v2/test/eigenda_client_test.go @@ -0,0 +1,562 @@ +package test + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "math/big" + "runtime" + "testing" + "time" + + "github.com/Layr-Labs/eigenda/api/clients/codecs" + "github.com/Layr-Labs/eigenda/api/clients/v2" + clientsmock "github.com/Layr-Labs/eigenda/api/clients/v2/mock" + "github.com/Layr-Labs/eigenda/api/clients/v2/verification" + common2 "github.com/Layr-Labs/eigenda/api/grpc/common" + commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" + disperserv2 "github.com/Layr-Labs/eigenda/api/grpc/disperser/v2" + "github.com/Layr-Labs/eigenda/common" + testrandom "github.com/Layr-Labs/eigenda/common/testutils/random" + contractEigenDABlobVerifier "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDABlobVerifier" + core "github.com/Layr-Labs/eigenda/core/v2" + "github.com/Layr-Labs/eigenda/encoding" + "github.com/Layr-Labs/eigenda/encoding/kzg" + prover2 "github.com/Layr-Labs/eigenda/encoding/kzg/prover" + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +const g1Path = "../../../../inabox/resources/kzg/g1.point" +const payloadLength = 100 + +type ClientTester struct { + Random *testrandom.TestRandom + Client *clients.EigenDAClient + MockRelayClient *clientsmock.MockRelayClient + MockBlobVerifier *clientsmock.MockBlobVerifier + Codec *codecs.DefaultBlobCodec + G1Srs []bn254.G1Affine +} + +// buildClientTester sets up a client with mocks necessary for testing +func buildClientTester(t *testing.T) ClientTester { + logger, err := common.NewLogger(common.DefaultLoggerConfig()) + require.NoError(t, err) + + clientConfig := &clients.EigenDAClientConfig{ + RelayTimeout: 50 * time.Millisecond, + } + + mockRelayClient := clientsmock.MockRelayClient{} + codec := codecs.NewDefaultBlobCodec() + + mockBlobVerifier := clientsmock.MockBlobVerifier{} + + random := testrandom.NewTestRandom(t) + + g1Srs, err := kzg.ReadG1Points(g1Path, 5, uint64(runtime.GOMAXPROCS(0))) + require.NotNil(t, g1Srs) + require.NoError(t, err) + + client, err := clients.NewEigenDAClient( + logger, + random.Rand, + clientConfig, + &mockRelayClient, + &mockBlobVerifier, + &codec, + g1Srs) + + require.NotNil(t, client) + require.NoError(t, err) + + return ClientTester{ + Random: random, + Client: client, + MockRelayClient: &mockRelayClient, + MockBlobVerifier: &mockBlobVerifier, + Codec: &codec, + G1Srs: g1Srs, + } +} + +// Builds a random blob key, blob bytes, and valid certificate +func buildBlobAndCert( + t *testing.T, + tester ClientTester, + relayKeys []core.RelayKey, +) (core.BlobKey, []byte, *verification.EigenDACert) { + + blobKey := core.BlobKey(tester.Random.Bytes(32)) + payloadBytes := tester.Random.Bytes(payloadLength) + blobBytes, err := tester.Codec.EncodeBlob(payloadBytes) + require.NoError(t, err) + require.NotNil(t, blobBytes) + + kzgConfig := &kzg.KzgConfig{ + G1Path: "../../../../inabox/resources/kzg/g1.point", + G2Path: "../../../../inabox/resources/kzg/g2.point", + CacheDir: "../../../../inabox/resources/kzg/SRSTables", + SRSOrder: 3000, + SRSNumberToLoad: 3000, + NumWorker: uint64(runtime.GOMAXPROCS(0)), + LoadG2Points: true, + } + + prover, err := prover2.NewProver(kzgConfig, nil) + require.NoError(t, err) + + params := encoding.ParamsFromMins(16, 16) + commitments, _, err := prover.EncodeAndProve(blobBytes, params) + require.NoError(t, err) + + commitmentsProto, err := commitments.ToProtobuf() + require.NoError(t, err) + + blobHeader := &commonv2.BlobHeader{ + Version: 1, + QuorumNumbers: make([]uint32, 0), + PaymentHeader: &common2.PaymentHeader{}, + Commitment: commitmentsProto, + } + + blobCertificate := &commonv2.BlobCertificate{ + Relays: relayKeys, + BlobHeader: blobHeader, + } + + verificationInfo := &disperserv2.BlobVerificationInfo{ + BlobCertificate: blobCertificate, + } + + verificationProof, err := contractEigenDABlobVerifier.VerificationProofProtoToBinding(verificationInfo) + require.NoError(t, err) + + return blobKey, blobBytes, &verification.EigenDACert{ + BlobVerificationProof: *verificationProof, + } +} + +// TestGetPayloadSuccess tests that a blob is received without error in the happy case +func TestGetPayloadSuccess(t *testing.T) { + tester := buildClientTester(t) + relayKeys := make([]core.RelayKey, 1) + relayKeys[0] = tester.Random.Uint32() + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + tester.MockRelayClient.On("GetBlob", mock.Anything, relayKeys[0], blobKey).Return(blobBytes, nil).Once() + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + payload, err := tester.Client.GetPayload( + context.Background(), + blobKey, + blobCert) + + require.NotNil(t, payload) + require.NoError(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestRelayCallTimeout verifies that calls to the relay timeout after the expected duration +func TestRelayCallTimeout(t *testing.T) { + tester := buildClientTester(t) + relayKeys := make([]core.RelayKey, 1) + relayKeys[0] = tester.Random.Uint32() + blobKey, _, blobCert := buildBlobAndCert(t, tester, relayKeys) + + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + // the timeout should occur before the panic has a chance to be triggered + tester.MockRelayClient.On("GetBlob", mock.Anything, relayKeys[0], blobKey).Return( + nil, errors.New("timeout")).Once().Run( + func(args mock.Arguments) { + ctx := args.Get(0).(context.Context) + select { + case <-ctx.Done(): + // this is the expected case + return + case <-time.After(time.Second): + panic("call should have timed out first") + } + }) + + // the panic should be triggered, since it happens faster than the configured timout + tester.MockRelayClient.On("GetBlob", mock.Anything, relayKeys[0], blobKey).Return( + nil, errors.New("timeout")).Once().Run( + func(args mock.Arguments) { + ctx := args.Get(0).(context.Context) + select { + case <-ctx.Done(): + return + case <-time.After(time.Millisecond): + // this is the expected case + panic("call should not have timed out") + } + }) + + require.NotPanics( + t, func() { + _, _ = tester.Client.GetPayload(context.Background(), blobKey, blobCert) + }) + + require.Panics( + t, func() { + _, _ = tester.Client.GetPayload(context.Background(), blobKey, blobCert) + }) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestRandomRelayRetries verifies correct behavior when some relays do not respond with the blob, +// requiring the client to retry with other relays. +func TestRandomRelayRetries(t *testing.T) { + tester := buildClientTester(t) + + relayCount := 100 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + // for this test, only a single relay is online + // we will be requiring that it takes a different amount of retries to dial this relay, since the array of relay keys to try is randomized + onlineRelayKey := relayKeys[tester.Random.Intn(len(relayKeys))] + + offlineKeyMatcher := func(relayKey core.RelayKey) bool { return relayKey != onlineRelayKey } + onlineKeyMatcher := func(relayKey core.RelayKey) bool { return relayKey == onlineRelayKey } + var failedCallCount int + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.MatchedBy(offlineKeyMatcher), blobKey).Return( + nil, + fmt.Errorf("offline relay")).Run( + func(args mock.Arguments) { + failedCallCount++ + }) + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.MatchedBy(onlineKeyMatcher), blobKey).Return( + blobBytes, + nil) + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + // keep track of how many tries various blob retrievals require + // this allows us to require that there is variability, i.e. that relay call order is actually random + requiredTries := map[int]bool{} + + for i := 0; i < relayCount; i++ { + failedCallCount = 0 + payload, err := tester.Client.GetPayload(context.Background(), blobKey, blobCert) + require.NotNil(t, payload) + require.NoError(t, err) + + requiredTries[failedCallCount] = true + } + + // with 100 random tries, with possible values between 1 and 100, we can very confidently require that there are at least 10 unique values + require.Greater(t, len(requiredTries), 10) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestNoRelayResponse tests functionality when none of the relays respond +func TestNoRelayResponse(t *testing.T) { + tester := buildClientTester(t) + + relayCount := 10 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey, _, blobCert := buildBlobAndCert(t, tester, relayKeys) + + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return(nil, fmt.Errorf("offline relay")) + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + payload, err := tester.Client.GetPayload( + context.Background(), + blobKey, + blobCert) + require.Nil(t, payload) + require.NotNil(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestNoRelays tests that having no relay keys is handled gracefully +func TestNoRelays(t *testing.T) { + tester := buildClientTester(t) + + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + blobKey, _, blobCert := buildBlobAndCert(t, tester, []core.RelayKey{}) + + payload, err := tester.Client.GetPayload(context.Background(), blobKey, blobCert) + require.Nil(t, payload) + require.NotNil(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestGetBlobReturns0Len verifies that a 0 length blob returned from a relay is handled gracefully, and that the client retries after such a failure +func TestGetBlobReturns0Len(t *testing.T) { + tester := buildClientTester(t) + + relayCount := 10 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + // the first GetBlob will return a 0 len blob + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return([]byte{}, nil).Once() + // the second call will return blob bytes + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return( + blobBytes, + nil).Once() + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + // the call to the first relay will fail with a 0 len blob returned. the call to the second relay will succeed + payload, err := tester.Client.GetPayload(context.Background(), blobKey, blobCert) + require.NotNil(t, payload) + require.NoError(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestGetBlobReturnsDifferentBlob tests what happens when one relay returns a blob that doesn't match the commitment. +// It also tests that the client retries to get the correct blob from a different relay +func TestGetBlobReturnsDifferentBlob(t *testing.T) { + tester := buildClientTester(t) + relayCount := 10 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey1, blobBytes1, blobCert1 := buildBlobAndCert(t, tester, relayKeys) + _, blobBytes2, _ := buildBlobAndCert(t, tester, relayKeys) + + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey1).Return(blobBytes2, nil).Once() + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey1).Return(blobBytes1, nil).Once() + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + payload, err := tester.Client.GetPayload( + context.Background(), + blobKey1, + blobCert1) + require.NotNil(t, payload) + require.NoError(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestGetBlobReturnsInvalidBlob tests what happens if a relay returns a blob which causes commitment verification to +// throw an error. It verifies that the client tries again with a different relay after such a failure. +func TestGetBlobReturnsInvalidBlob(t *testing.T) { + tester := buildClientTester(t) + relayCount := 10 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + tooLongBytes := make([]byte, len(blobBytes)+100) + copy(tooLongBytes[:], blobBytes) + + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return(tooLongBytes, nil).Once() + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return(blobBytes, nil).Once() + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + // this will fail the first time, since there isn't enough srs loaded to compute the commitment of the returned bytes + // it will succeed when the second relay gives the correct bytes + payload, err := tester.Client.GetPayload( + context.Background(), + blobKey, + blobCert) + + require.NotNil(t, payload) + require.NoError(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestBlobFailsVerifyBlobV2 tests what happens if cert verification fails +func TestBlobFailsVerifyBlobV2(t *testing.T) { + tester := buildClientTester(t) + relayCount := 10 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + tooLongBytes := make([]byte, len(blobBytes)+100) + copy(tooLongBytes[:], blobBytes) + + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(errors.New("verification failed")).Once() + + // this will fail the first time, since verifyBlobV2 will fail + payload, err := tester.Client.GetPayload( + context.Background(), + blobKey, + blobCert) + + require.Nil(t, payload) + require.Error(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestGetBlobReturnsBlobWithInvalidLen check what happens if the blob length doesn't match the length that exists in +// the BlobCommitment +func TestGetBlobReturnsBlobWithInvalidLen(t *testing.T) { + tester := buildClientTester(t) + + relayKeys := make([]core.RelayKey, 1) + relayKeys[0] = tester.Random.Uint32() + + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + blobCert.BlobVerificationProof.BlobCertificate.BlobHeader.Commitment.DataLength-- + + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return(blobBytes, nil).Once() + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + // this will fail, since the length in the BlobCommitment doesn't match the actual blob length + payload, err := tester.Client.GetPayload( + context.Background(), + blobKey, + blobCert) + + require.Nil(t, payload) + require.Error(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestFailedDecoding verifies that a failed blob decode is handled gracefully +func TestFailedDecoding(t *testing.T) { + tester := buildClientTester(t) + + relayCount := 10 + relayKeys := make([]core.RelayKey, relayCount) + for i := 0; i < relayCount; i++ { + relayKeys[i] = tester.Random.Uint32() + } + blobKey, blobBytes, blobCert := buildBlobAndCert(t, tester, relayKeys) + + // intentionally cause the payload header claimed length to differ from the actual length + binary.BigEndian.PutUint32(blobBytes[2:6], uint32(len(blobBytes)-1)) + + // generate a malicious cert, which will verify for the invalid blob + maliciousCommitment, err := verification.GenerateBlobCommitment(tester.G1Srs, blobBytes) + require.NoError(t, err) + require.NotNil(t, maliciousCommitment) + + blobCert.BlobVerificationProof.BlobCertificate.BlobHeader.Commitment.Commitment = contractEigenDABlobVerifier.BN254G1Point{ + X: maliciousCommitment.X.BigInt(new(big.Int)), + Y: maliciousCommitment.Y.BigInt(new(big.Int)), + } + + tester.MockRelayClient.On("GetBlob", mock.Anything, mock.Anything, blobKey).Return( + blobBytes, + nil).Once() + tester.MockBlobVerifier.On( + "VerifyBlobV2", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything).Return(nil) + + payload, err := tester.Client.GetPayload(context.Background(), blobKey, blobCert) + require.Error(t, err) + require.Nil(t, payload) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestErrorFreeClose tests the happy case, where none of the internal closes yield an error +func TestErrorFreeClose(t *testing.T) { + tester := buildClientTester(t) + + tester.MockRelayClient.On("Close").Return(nil).Once() + + err := tester.Client.Close() + require.NoError(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestErrorClose tests what happens when subcomponents throw errors when being closed +func TestErrorClose(t *testing.T) { + tester := buildClientTester(t) + + tester.MockRelayClient.On("Close").Return(fmt.Errorf("close failed")).Once() + + err := tester.Client.Close() + require.NotNil(t, err) + + tester.MockRelayClient.AssertExpectations(t) +} + +// TestGetCodec checks that the codec used in construction is returned by GetCodec +func TestGetCodec(t *testing.T) { + tester := buildClientTester(t) + + require.Equal(t, tester.Codec, tester.Client.GetCodec()) + + tester.MockRelayClient.AssertExpectations(t) +} diff --git a/api/clients/v2/verification/blob_verifier.go b/api/clients/v2/verification/blob_verifier.go index 4dc8d4e06c..e8977bc5a5 100644 --- a/api/clients/v2/verification/blob_verifier.go +++ b/api/clients/v2/verification/blob_verifier.go @@ -4,8 +4,7 @@ import ( "context" "fmt" - commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" - "github.com/Layr-Labs/eigenda/common" + "github.com/Layr-Labs/eigenda/common/geth" disperser "github.com/Layr-Labs/eigenda/api/grpc/disperser/v2" verifierBindings "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDABlobVerifier" @@ -13,6 +12,16 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" ) +// IBlobVerifier is the interface representing a BlobVerifier +// +// This interface exists in order to allow verification mocking in unit tests. +type IBlobVerifier interface { + VerifyBlobV2( + ctx context.Context, + eigenDACert *EigenDACert, + ) error +} + // BlobVerifier is responsible for making eth calls against the BlobVerifier contract to ensure cryptographic and // structural integrity of V2 certificates // @@ -24,7 +33,7 @@ type BlobVerifier struct { // NewBlobVerifier constructs a BlobVerifier func NewBlobVerifier( - ethClient *common.EthClient, // the eth client, which should already be set up + ethClient *geth.EthClient, // the eth client, which should already be set up blobVerifierAddress string, // the hex address of the EigenDABlobVerifier contract ) (*BlobVerifier, error) { @@ -52,12 +61,12 @@ func (v *BlobVerifier) VerifyBlobV2FromSignedBatch( // Contains all necessary information about the blob, so that it can be verified. blobVerificationProof *disperser.BlobVerificationInfo, ) error { - convertedSignedBatch, err := verifierBindings.ConvertSignedBatch(signedBatch) + convertedSignedBatch, err := verifierBindings.SignedBatchProtoToBinding(signedBatch) if err != nil { return fmt.Errorf("convert signed batch: %s", err) } - convertedBlobVerificationProof, err := verifierBindings.ConvertVerificationProof(blobVerificationProof) + convertedBlobVerificationProof, err := verifierBindings.VerificationProofProtoToBinding(blobVerificationProof) if err != nil { return fmt.Errorf("convert blob verification proof: %s", err) } @@ -79,28 +88,13 @@ func (v *BlobVerifier) VerifyBlobV2FromSignedBatch( // This method returns nil if the blob is successfully verified. Otherwise, it returns an error. func (v *BlobVerifier) VerifyBlobV2( ctx context.Context, - // The header of the batch that the blob is contained in - batchHeader *commonv2.BatchHeader, - // Contains data pertaining to the blob's inclusion in the batch - blobVerificationProof *disperser.BlobVerificationInfo, - // Contains data that can be used to verify that the blob actually exists in the claimed batch - nonSignerStakesAndSignature verifierBindings.NonSignerStakesAndSignature, + eigenDACert *EigenDACert, ) error { - convertedBatchHeader, err := verifierBindings.ConvertBatchHeader(batchHeader) - if err != nil { - return fmt.Errorf("convert batch header: %s", err) - } - - convertedBlobVerificationProof, err := verifierBindings.ConvertVerificationProof(blobVerificationProof) - if err != nil { - return fmt.Errorf("convert blob verification proof: %s", err) - } - - err = v.blobVerifierCaller.VerifyBlobV2( + err := v.blobVerifierCaller.VerifyBlobV2( &bind.CallOpts{Context: ctx}, - *convertedBatchHeader, - *convertedBlobVerificationProof, - nonSignerStakesAndSignature) + eigenDACert.BatchHeader, + eigenDACert.BlobVerificationProof, + eigenDACert.NonSignerStakesAndSignature) if err != nil { return fmt.Errorf("verify blob v2: %s", err) diff --git a/api/clients/v2/verification/commitment_utils.go b/api/clients/v2/verification/commitment_utils.go index 3a488082b8..ce79262ddd 100644 --- a/api/clients/v2/verification/commitment_utils.go +++ b/api/clients/v2/verification/commitment_utils.go @@ -2,6 +2,7 @@ package verification import ( "fmt" + "github.com/Layr-Labs/eigenda/encoding" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254" @@ -36,23 +37,22 @@ func GenerateBlobCommitment( } // GenerateAndCompareBlobCommitment generates the kzg-bn254 commitment of the blob, and compares it with a claimed -// commitment. An error is returned if there is a problem generating the commitment, or if the comparison fails. +// commitment. An error is returned if there is a problem generating the commitment. True is returned if the commitment +// is successfully generated, and is equal to the claimed commitment, otherwise false. func GenerateAndCompareBlobCommitment( g1Srs []bn254.G1Affine, blobBytes []byte, - claimedCommitment *encoding.G1Commitment) error { + claimedCommitment *encoding.G1Commitment) (bool, error) { computedCommitment, err := GenerateBlobCommitment(g1Srs, blobBytes) if err != nil { - return fmt.Errorf("compute commitment: %w", err) + return false, fmt.Errorf("compute commitment: %w", err) } if claimedCommitment.X.Equal(&computedCommitment.X) && claimedCommitment.Y.Equal(&computedCommitment.Y) { - return nil + return true, nil } - return fmt.Errorf( - "commitment field elements do not match. computed commitment: (x: %x, y: %x), claimed commitment (x: %x, y: %x)", - computedCommitment.X, computedCommitment.Y, claimedCommitment.X, claimedCommitment.Y) + return false, nil } diff --git a/api/clients/v2/verification/commitment_utils_test.go b/api/clients/v2/verification/commitment_utils_test.go index 8e9927e0ca..2c5fe499aa 100644 --- a/api/clients/v2/verification/commitment_utils_test.go +++ b/api/clients/v2/verification/commitment_utils_test.go @@ -1,13 +1,14 @@ package verification import ( + "math" + "runtime" + "testing" + "github.com/Layr-Labs/eigenda/common/testutils/random" "github.com/Layr-Labs/eigenda/encoding/kzg" "github.com/Layr-Labs/eigenda/encoding/utils/codec" "github.com/stretchr/testify/require" - "math" - "runtime" - "testing" ) const g1Path = "../../../../inabox/resources/kzg/g1.point" @@ -42,10 +43,11 @@ func TestComputeAndCompareKzgCommitmentSuccess(t *testing.T) { require.NoError(t, err) // make sure the commitment verifies correctly - err = GenerateAndCompareBlobCommitment( + result, err := GenerateAndCompareBlobCommitment( g1Srs, randomBytes, commitment) + require.True(t, result) require.NoError(t, err) } @@ -65,11 +67,12 @@ func TestComputeAndCompareKzgCommitmentFailure(t *testing.T) { // randomly modify the bytes, and make sure the commitment verification fails randomlyModifyBytes(testRandom, randomBytes) - err = GenerateAndCompareBlobCommitment( + result, err := GenerateAndCompareBlobCommitment( g1Srs, randomBytes, commitment) - require.NotNil(t, err) + require.False(t, result) + require.NoError(t, err) } func TestGenerateBlobCommitmentEquality(t *testing.T) { diff --git a/api/clients/v2/verification/eigenda_cert.go b/api/clients/v2/verification/eigenda_cert.go new file mode 100644 index 0000000000..1525c7b5a5 --- /dev/null +++ b/api/clients/v2/verification/eigenda_cert.go @@ -0,0 +1,14 @@ +package verification + +import ( + contractEigenDABlobVerifier "github.com/Layr-Labs/eigenda/contracts/bindings/EigenDABlobVerifier" +) + +// EigenDACert contains all data necessary to verify a Blob +// +// This struct represents the composition of a eigenDA blob certificate, as it would exist in a rollup inbox. +type EigenDACert struct { + BlobVerificationProof contractEigenDABlobVerifier.BlobVerificationProofV2 + BatchHeader contractEigenDABlobVerifier.BatchHeaderV2 + NonSignerStakesAndSignature contractEigenDABlobVerifier.NonSignerStakesAndSignature +} diff --git a/contracts/bindings/EigenDABlobVerifier/conversion_utils.go b/contracts/bindings/EigenDABlobVerifier/conversion_utils.go index ff955af40f..63da6902bf 100644 --- a/contracts/bindings/EigenDABlobVerifier/conversion_utils.go +++ b/contracts/bindings/EigenDABlobVerifier/conversion_utils.go @@ -10,15 +10,16 @@ import ( disperserv2 "github.com/Layr-Labs/eigenda/api/grpc/disperser/v2" "github.com/Layr-Labs/eigenda/core" "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fp" ) -func ConvertSignedBatch(inputBatch *disperserv2.SignedBatch) (*SignedBatch, error) { - convertedBatchHeader, err := ConvertBatchHeader(inputBatch.GetHeader()) +func SignedBatchProtoToBinding(inputBatch *disperserv2.SignedBatch) (*SignedBatch, error) { + convertedBatchHeader, err := BatchHeaderProtoToBinding(inputBatch.GetHeader()) if err != nil { return nil, fmt.Errorf("convert batch header: %s", err) } - convertedAttestation, err := convertAttestation(inputBatch.GetAttestation()) + convertedAttestation, err := attestationProtoToBinding(inputBatch.GetAttestation()) if err != nil { return nil, fmt.Errorf("convert attestation: %s", err) } @@ -31,7 +32,7 @@ func ConvertSignedBatch(inputBatch *disperserv2.SignedBatch) (*SignedBatch, erro return outputSignedBatch, nil } -func ConvertBatchHeader(inputHeader *commonv2.BatchHeader) (*BatchHeaderV2, error) { +func BatchHeaderProtoToBinding(inputHeader *commonv2.BatchHeader) (*BatchHeaderV2, error) { var outputBatchRoot [32]byte inputBatchRoot := inputHeader.GetBatchRoot() @@ -56,13 +57,13 @@ func ConvertBatchHeader(inputHeader *commonv2.BatchHeader) (*BatchHeaderV2, erro return convertedHeader, nil } -func convertAttestation(inputAttestation *disperserv2.Attestation) (*Attestation, error) { - nonSignerPubkeys, err := repeatedBytesToG1Points(inputAttestation.GetNonSignerPubkeys()) +func attestationProtoToBinding(inputAttestation *disperserv2.Attestation) (*Attestation, error) { + nonSignerPubkeys, err := repeatedBytesToBN254G1Points(inputAttestation.GetNonSignerPubkeys()) if err != nil { return nil, fmt.Errorf("convert non signer pubkeys to g1 points: %s", err) } - quorumApks, err := repeatedBytesToG1Points(inputAttestation.GetQuorumApks()) + quorumApks, err := repeatedBytesToBN254G1Points(inputAttestation.GetQuorumApks()) if err != nil { return nil, fmt.Errorf("convert quorum apks to g1 points: %s", err) } @@ -88,8 +89,8 @@ func convertAttestation(inputAttestation *disperserv2.Attestation) (*Attestation return convertedAttestation, nil } -func ConvertVerificationProof(inputVerificationInfo *disperserv2.BlobVerificationInfo) (*BlobVerificationProofV2, error) { - convertedBlobCertificate, err := convertBlobCertificate(inputVerificationInfo.GetBlobCertificate()) +func VerificationProofProtoToBinding(inputVerificationInfo *disperserv2.BlobVerificationInfo) (*BlobVerificationProofV2, error) { + convertedBlobCertificate, err := blobCertificateProtoToBinding(inputVerificationInfo.GetBlobCertificate()) if err != nil { return nil, fmt.Errorf("convert blob certificate: %s", err) @@ -102,8 +103,8 @@ func ConvertVerificationProof(inputVerificationInfo *disperserv2.BlobVerificatio }, nil } -func convertBlobCertificate(inputCertificate *commonv2.BlobCertificate) (*BlobCertificate, error) { - convertedBlobHeader, err := convertBlobHeader(inputCertificate.GetBlobHeader()) +func blobCertificateProtoToBinding(inputCertificate *commonv2.BlobCertificate) (*BlobCertificate, error) { + convertedBlobHeader, err := blobHeaderProtoToBinding(inputCertificate.GetBlobHeader()) if err != nil { return nil, fmt.Errorf("convert blob header: %s", err) } @@ -114,7 +115,7 @@ func convertBlobCertificate(inputCertificate *commonv2.BlobCertificate) (*BlobCe }, nil } -func convertBlobHeader(inputHeader *commonv2.BlobHeader) (*BlobHeaderV2, error) { +func blobHeaderProtoToBinding(inputHeader *commonv2.BlobHeader) (*BlobHeaderV2, error) { inputVersion := inputHeader.GetVersion() if inputVersion > math.MaxUint16 { return nil, fmt.Errorf( @@ -135,7 +136,7 @@ func convertBlobHeader(inputHeader *commonv2.BlobHeader) (*BlobHeaderV2, error) quorumNumbers = append(quorumNumbers, byte(quorumNumber)) } - convertedBlobCommitment, err := convertBlobCommitment(inputHeader.GetCommitment()) + convertedBlobCommitment, err := blobCommitmentProtoToBinding(inputHeader.GetCommitment()) if err != nil { return nil, fmt.Errorf("convert blob commitment: %s", err) } @@ -153,7 +154,7 @@ func convertBlobHeader(inputHeader *commonv2.BlobHeader) (*BlobHeaderV2, error) }, nil } -func convertBlobCommitment(inputCommitment *common.BlobCommitment) (*BlobCommitment, error) { +func blobCommitmentProtoToBinding(inputCommitment *common.BlobCommitment) (*BlobCommitment, error) { convertedCommitment, err := bytesToBN254G1Point(inputCommitment.GetCommitment()) if err != nil { return nil, fmt.Errorf("convert commitment to g1 point: %s", err) @@ -177,6 +178,16 @@ func convertBlobCommitment(inputCommitment *common.BlobCommitment) (*BlobCommitm }, nil } +// BlobCommitmentBindingToProto converts a BlobCommitment binding into a common.BlobCommitment protobuf +func BlobCommitmentBindingToProto(inputCommitment *BlobCommitment) *common.BlobCommitment { + return &common.BlobCommitment{ + Commitment: bn254G1PointToBytes(&inputCommitment.Commitment), + LengthCommitment: bn254G2PointToBytes(&inputCommitment.LengthCommitment), + LengthProof: bn254G2PointToBytes(&inputCommitment.LengthProof), + Length: inputCommitment.DataLength, + } +} + func bytesToBN254G1Point(bytes []byte) (*BN254G1Point, error) { var g1Point bn254.G1Affine _, err := g1Point.SetBytes(bytes) @@ -191,6 +202,18 @@ func bytesToBN254G1Point(bytes []byte) (*BN254G1Point, error) { }, nil } +func bn254G1PointToBytes(inputPoint *BN254G1Point) []byte { + var x fp.Element + x.SetBigInt(inputPoint.X) + var y fp.Element + y.SetBigInt(inputPoint.Y) + + g1Point := &bn254.G1Affine{X: x, Y: y} + + bytes := g1Point.Bytes() + return bytes[:] +} + func bytesToBN254G2Point(bytes []byte) (*BN254G2Point, error) { var g2Point bn254.G2Affine @@ -216,7 +239,24 @@ func bytesToBN254G2Point(bytes []byte) (*BN254G2Point, error) { }, nil } -func repeatedBytesToG1Points(repeatedBytes [][]byte) ([]BN254G1Point, error) { +func bn254G2PointToBytes(inputPoint *BN254G2Point) []byte { + var g2Point bn254.G2Affine + + // Order is intentionally reversed when converting here + // (see https://github.com/Layr-Labs/eigenlayer-middleware/blob/512ce7326f35e8060b9d46e23f9c159c0000b546/src/libraries/BN254.sol#L43) + + var xa0, xa1, ya0, ya1 fp.Element + g2Point.X.A0 = *(xa0.SetBigInt(inputPoint.X[1])) + g2Point.X.A1 = *(xa1.SetBigInt(inputPoint.X[0])) + + g2Point.Y.A0 = *(ya0.SetBigInt(inputPoint.Y[1])) + g2Point.Y.A1 = *(ya1.SetBigInt(inputPoint.Y[0])) + + pointBytes := g2Point.Bytes() + return pointBytes[:] +} + +func repeatedBytesToBN254G1Points(repeatedBytes [][]byte) ([]BN254G1Point, error) { var outputPoints []BN254G1Point for _, bytes := range repeatedBytes { g1Point, err := bytesToBN254G1Point(bytes)