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

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4625e96
Write GET tests
litt3 Dec 9, 2024
f07e820
Merge branch 'master' into client-v2-get
litt3 Dec 10, 2024
885c131
Respond to PR comments
litt3 Dec 10, 2024
6848663
Create new V2 client config
litt3 Dec 10, 2024
a48afb1
Respond to more PR comments
litt3 Dec 11, 2024
225f2a3
Fix failing unit test
litt3 Dec 12, 2024
d265f6a
Merge branch 'master' into client-v2-get
litt3 Dec 12, 2024
e9d91c5
Adopt new package structure
litt3 Dec 12, 2024
dd3c262
Use new test random util
litt3 Dec 12, 2024
88df865
Implement relay call timeout
litt3 Dec 12, 2024
505a1f0
Use correct error join method
litt3 Dec 12, 2024
2b87633
Merge branch 'master' into client-v2-get
litt3 Jan 8, 2025
cf1cd80
Make updates required by upstream changes
litt3 Jan 8, 2025
53893d8
Update how FFT and IFFT are referred to
litt3 Jan 13, 2025
0373dd7
Implement GetPayload
litt3 Jan 13, 2025
826a026
Remove GetBlob, leaving only GetPayload
litt3 Jan 13, 2025
975b6e5
Remove unnecessary codec mock
litt3 Jan 13, 2025
0666d24
Use more reasonable line breaks for logs
litt3 Jan 13, 2025
0a49aa5
Test malicious cert
litt3 Jan 13, 2025
1193ce7
Merge branch 'master' into client-v2-get
litt3 Jan 13, 2025
496e277
Merge branch 'master' into client-v2-get
litt3 Jan 14, 2025
2d392ff
Finish test coverage
litt3 Jan 14, 2025
db51291
Fix commitment length check
litt3 Jan 14, 2025
4f3280c
Merge branch 'master' into client-v2-get
litt3 Jan 16, 2025
aaa1342
Call VerifyBlobV2
litt3 Jan 17, 2025
9be51e6
Simply verify blob
litt3 Jan 17, 2025
cc6b9a1
Merge branch 'master' into client-v2-get
litt3 Jan 17, 2025
ae926c7
Clean up
litt3 Jan 17, 2025
f82d128
Merge branch 'master' into client-v2-get
litt3 Jan 17, 2025
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
)
35 changes: 35 additions & 0 deletions api/clients/v2/config.go
Original file line number Diff line number Diff line change
@@ -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
RelayTimeout time.Duration

// The timeout duration for contract calls
ContractCallTimeout time.Duration
}
282 changes: 282 additions & 0 deletions api/clients/v2/eigenda_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
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) {

relayKeys := eigenDACert.BlobVerificationProof.BlobCertificate.RelayKeys
relayKeyCount := len(relayKeys)

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

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, blobCommitment.Commitment, blobCommitment.Length) {
// specifics are logged in verifyBlobFromRelay
continue
}

err = c.verifyBlobV2WithTimeout(ctx, eigenDACert)
if err != nil {
c.log.Warn("verifyBlobV2 failed", "blobKey", blobKey, "relayKey", relayKey, "error", 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)
}

// verifyBlobFromRelay performs the necessary local verifications after having retrieved a blob from a relay.
// This method does NOT verify the blob with a 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 true. Otherwise, it logs a warning and returns false.
func (c *EigenDAClient) verifyBlobFromRelay(
blobKey core.BlobKey,
relayKey core.RelayKey,
blob []byte,
kzgCommitment *encoding.G1Commitment,
blobLength uint) 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
}

// 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 {
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 in symbols
if uint(len(blob)) > blobLength*encoding.BYTES_PER_SYMBOL {
c.log.Warn(
"blob length is greater than claimed blob length",
"blobKey", blobKey, "relayKey", relayKey, "blobLength", len(blob),
"claimedBlobLength", blobLength)
return false
}

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

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

// verifyBlobV2WithTimeout verifies a blob by making a call to VerifyBlobV2.
//
// This method times out after the duration configured in clientConfig.ContractCallTimeout
func (c *EigenDAClient) verifyBlobV2WithTimeout(
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)
}
}
47 changes: 47 additions & 0 deletions api/clients/v2/mock/blob_verifier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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