Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement v2 client GET functionality #972

Draft
wants to merge 23 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/clients/codecs/polynomial_form.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 expanded
// roots of unity
Eval PolynomialForm = iota
// Coeff is short for "coefficient form". The field elements represent the coefficients of the polynomial
Coeff
)
26 changes: 26 additions & 0 deletions api/clients/v2/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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

// 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
RelayTimeout time.Duration
}
229 changes: 229 additions & 0 deletions api/clients/v2/eigenda_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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"
core "github.com/Layr-Labs/eigenda/core/v2"
"github.com/Layr-Labs/eigensdk-go/logging"
"github.com/consensys/gnark-crypto/ecc/bn254"
)

// 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
config *EigenDAClientConfig
codec codecs.BlobCodec
relayClient RelayClient
g1Srs []bn254.G1Affine
}

// BuildEigenDAClient builds an EigenDAClient from config structs.
func BuildEigenDAClient(
log logging.Logger,
config *EigenDAClientConfig,
relayClientConfig *RelayClientConfig,
g1Srs []bn254.G1Affine) (*EigenDAClient, error) {

relayClient, err := NewRelayClient(relayClientConfig, log)
if err != nil {
return nil, fmt.Errorf("new relay client: %w", err)
}

codec, err := createCodec(config)
if err != nil {
return nil, err
}

return NewEigenDAClient(log, rand.New(rand.NewSource(rand.Int63())), config, relayClient, codec, g1Srs)
}

// NewEigenDAClient assembles an EigenDAClient from subcomponents that have already been constructed and initialized.
func NewEigenDAClient(
log logging.Logger,
random *rand.Rand,
config *EigenDAClientConfig,
relayClient RelayClient,
codec codecs.BlobCodec,
g1Srs []bn254.G1Affine) (*EigenDAClient, error) {

return &EigenDAClient{
log: log,
random: random,
config: config,
codec: codec,
relayClient: relayClient,
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,
blobCertificate *core.BlobCertificate) ([]byte, error) {

relayKeys := blobCertificate.RelayKeys
relayKeyCount := len(relayKeys)

if relayKeyCount == 0 {
return nil, errors.New("relay key count is zero")
}

// 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
}

// An honest relay should never send a blob which doesn't verify
if !c.verifyBlobFromRelay(blobKey, relayKey, blob, blobCertificate) {
// specifics are logged in verifyBlobFromRelay
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, "blobCertificate", blobCertificate, "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)
}

// verifyBlobFromRelay performs the necessary verification after having retrieved a blob from a relay.
//
// If all verifications succeed, the method returns true. Otherwise, it logs a warning and returns false.
func (c *EigenDAClient) verifyBlobFromRelay(
blobKey core.BlobKey,
relayKey core.RelayKey,
blob []byte,
blobCertificate *core.BlobCertificate) bool {

// An honest relay should never send an empty blob
if len(blob) == 0 {
c.log.Warn("blob received from relay had length 0", "blobKey", blobKey, "relayKey", relayKey)
return false
}

blobCommitments := blobCertificate.BlobHeader.BlobCommitments

// 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, blobCommitments.Commitment)
if err != nil {
c.log.Warn(
"error generating commitment from received blob",
"blobKey", blobKey, "relayKey", relayKey, "error", err)
return false
}

if !valid {
c.log.Warn(
"blob commitment is invalid for received bytes",
"blobKey", blobKey, "relayKey", relayKey)
return false
}

// 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, padded to a power of 2. For this reason, we
// assert <=, as opposed to asserting equality
if uint(len(blob)) > blobCommitments.Length {
c.log.Warn(
"blob length is greater than length claimed in blob commitments",
"blobKey", blobKey, "relayKey", relayKey, "blobLength", len(blob),
"blobCommitments.Length", blobCommitments.Length)
return false
}

// TODO: call verifyBlobV2

return true
}

// 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.config.RelayTimeout)
defer cancel()

return c.relayClient.GetBlob(timeoutCtx, relayKey, blobKey)
}

// 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)
}
}
2 changes: 1 addition & 1 deletion api/clients/v2/mock/relay_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading
Loading