Skip to content

Commit 390aaeb

Browse files
committed
Merge branch 'master' into dataapi-reservations
2 parents fdedee7 + 0da0c7b commit 390aaeb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3237
-784
lines changed

api/clients/v2/coretypes/blob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func (b *Blob) toEncodedPayload(payloadForm codecs.PolynomialForm) (*encodedPayl
116116

117117
encodedPayload, err := encodedPayloadFromElements(payloadElements, maxPermissiblePayloadLength)
118118
if err != nil {
119-
return nil, fmt.Errorf("encoded payload from elements %w", err)
119+
return nil, fmt.Errorf("encoded payload from elements: %w", err)
120120
}
121121

122122
return encodedPayload, nil
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package coretypes
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
_ "github.com/Layr-Labs/eigenda/api/clients/codecs"
8+
)
9+
10+
// DerivationErrorStatusCode is an enum for the different error status codes
11+
// that can be returned during EigenDA "derivation" of a payload from a DA cert.
12+
// For more details, see the EigenDA spec on derivation:
13+
// https://github.com/Layr-Labs/eigenda/blob/f4ef5cd5/docs/spec/src/integration/spec/6-secure-integration.md#derivation-process
14+
//
15+
// This error is meant to be marshalled to JSON and returned as an HTTP 418 body
16+
// to indicate that the cert should be discarded from rollups' derivation pipelines.
17+
//
18+
// See https://github.com/Layr-Labs/optimism/pull/50 for how this is
19+
// used in optimism's derivation pipeline.
20+
type DerivationError struct {
21+
StatusCode uint8
22+
Msg string
23+
}
24+
25+
func (e DerivationError) Error() string {
26+
return fmt.Sprintf("derivation error: status code %d, message: %s", e.StatusCode, e.Msg)
27+
}
28+
29+
// Marshalled to JSON and returned as an HTTP 418 body
30+
// to indicate that the cert should be discarded from rollups' derivation pipelines.
31+
// We panic if marshalling fails, since the caller won't be able to handle the derivation error
32+
// properly, so they'll receive a 500 and must retry.
33+
func (e DerivationError) MarshalToTeapotBody() string {
34+
e.Validate()
35+
bodyJSON, err := json.Marshal(e)
36+
if err != nil {
37+
panic(fmt.Errorf("failed to marshal derivation error: %w", err))
38+
}
39+
return string(bodyJSON)
40+
}
41+
42+
// Used to add context to the sentinel errors below. For example:
43+
// ErrInvalidCertDerivationError.WithMessage("failed to parse cert")
44+
func (e DerivationError) WithMessage(msg string) DerivationError {
45+
return DerivationError{
46+
StatusCode: e.StatusCode,
47+
Msg: msg,
48+
}
49+
}
50+
51+
// Validate that the DerivationError has a valid status code.
52+
// The only valid status codes are 1-4, as defined in the sentinel errors below, eg [ErrCertParsingFailedDerivationError].
53+
func (e DerivationError) Validate() {
54+
if e.StatusCode < 1 || e.StatusCode > 4 {
55+
panic(fmt.Sprintf("DerivationError: invalid status code %d, must be between 1 and 4", e.StatusCode))
56+
}
57+
// The Msg field should ideally be a human-readable string that explains the error,
58+
// but we don't enforce it.
59+
}
60+
61+
// These errors can be used as sentinels to indicate specific derivation errors,
62+
// but they should be used with the `WithMessage` method to add context.
63+
// Also see the constructors below for creating these errors with context.
64+
//
65+
// Note: we purposefully don't use StatusCode 0 here, to prevent default value bugs in case people
66+
// create a DerivationError by hand without using the constructors or sentinel errors defined here.
67+
var (
68+
// Signifies that the input can't be parsed into a versioned cert.
69+
ErrCertParsingFailedDerivationError = DerivationError{StatusCode: 1}
70+
// Signifies that the cert is invalid due to a recency check failure,
71+
// meaning that `cert.L1InclusionBlock > batch.RBN + rbnRecencyWindowSize`.
72+
// See https://layr-labs.github.io/eigenda/integration/spec/6-secure-integration.html#1-rbn-recency-validation
73+
ErrRecencyCheckFailedDerivationError = DerivationError{StatusCode: 2}
74+
// Signifies that the CertVerifier.checkDACert eth-call returned an error status code.
75+
// See https://layr-labs.github.io/eigenda/integration/spec/6-secure-integration.html#2-cert-validation
76+
ErrInvalidCertDerivationError = DerivationError{StatusCode: 3}
77+
// Signifies that the blob is incorrectly encoded, and cannot be decoded into a valid payload.
78+
// See [codecs.PayloadEncodingVersion] for the different supported encodings.
79+
// See https://layr-labs.github.io/eigenda/integration/spec/6-secure-integration.html#3-blob-validation
80+
ErrBlobDecodingFailedDerivationError = DerivationError{StatusCode: 4}
81+
)
82+
83+
// Signifies that the cert is invalid due to a parsing failure,
84+
// meaning that a versioned cert could not be parsed from the serialized hex string.
85+
// For example a CertV3 failed to get rlp.decoded from the hex string.
86+
func NewCertParsingFailedError(serializedCertHex string, err string) DerivationError {
87+
return ErrCertParsingFailedDerivationError.WithMessage(
88+
fmt.Sprintf("cert parsing failed for cert %s: %v", serializedCertHex, err),
89+
)
90+
}
91+
92+
// Signifies that the cert is invalid due to a recency check failure,
93+
// meaning that `cert.L1InclusionBlock > batch.RBN + rbnRecencyWindowSize`.
94+
func NewRBNRecencyCheckFailedError(
95+
certRBN, certL1InclusionBlock, rbnRecencyWindowSize uint64,
96+
) DerivationError {
97+
return ErrRecencyCheckFailedDerivationError.WithMessage(
98+
fmt.Sprintf(
99+
"RBN recency check failed: certL1InclusionBlockNumber (%d) > cert.RBN (%d) + RBNRecencyWindowSize (%d)",
100+
certL1InclusionBlock, certRBN, rbnRecencyWindowSize,
101+
))
102+
}

api/clients/v2/coretypes/eigenda_cert.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ const (
4545
VersionThreeCert = 0x3
4646
)
4747

48-
// VerificationStatusCode reflects the return byte generated by a verification call to a generic cert verifier
48+
// VerificationStatusCode represents the status codes that can be returned by the EigenDACertVerifier.checkDACert contract calls.
49+
// It should match exactly the status codes defined in the contract:
50+
// https://github.com/Layr-Labs/eigenda/blob/1091f460ba762b84019389cbb82d9b04bb2c2bdb/contracts/src/integrations/cert/libraries/EigenDACertVerificationLib.sol#L48-L54
4951
type VerificationStatusCode uint8
5052

5153
const (
@@ -94,7 +96,10 @@ const (
9496

9597
// EigenDACert is a sum type interface returned by the payload disperser
9698
type EigenDACert interface {
97-
Version() CertificateVersion
99+
// isEigenDACert is an unexported method that restricts
100+
// which types can implement this interface to only those
101+
// defined in this package
102+
isEigenDACert()
98103
}
99104

100105
// RetrievableEigenDACert is an interface that defines data field accessor methods
@@ -253,9 +258,7 @@ func (c *EigenDACertV3) Commitments() (*encoding.BlobCommitments, error) {
253258
}
254259

255260
// Version returns the version of the EigenDA certificate
256-
func (c *EigenDACertV3) Version() CertificateVersion {
257-
return VersionThreeCert
258-
}
261+
func (c *EigenDACertV3) isEigenDACert() {}
259262

260263
// This struct represents the composition of an EigenDA V2 certificate
261264
// NOTE: This type is hardforked from the V3 type and will no longer
@@ -384,9 +387,7 @@ func (c *EigenDACertV2) ComputeBlobKey() (*coreV2.BlobKey, error) {
384387
}
385388

386389
// Version returns the version of the EigenDA certificate
387-
func (c *EigenDACertV2) Version() CertificateVersion {
388-
return VersionTwoCert
389-
}
390+
func (c *EigenDACertV2) isEigenDACert() {}
390391

391392
// ToV3 converts an EigenDACertV2 to an EigenDACertV3
392393
func (c *EigenDACertV2) ToV3() (*EigenDACertV3, error) {

api/clients/v2/coretypes/encoded_payload.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ func newEncodedPayload(payload *Payload) (*encodedPayload, error) {
4444

4545
// decode applies the inverse of PayloadEncodingVersion0 to an encodedPayload, and returns the decoded Payload
4646
func (ep *encodedPayload) decode() (*Payload, error) {
47+
if len(ep.bytes) < 32 {
48+
return nil, fmt.Errorf("encoded payload must be at least 32 bytes long, but got %d bytes", len(ep.bytes))
49+
}
50+
if ep.bytes[0] != 0x00 {
51+
return nil, fmt.Errorf("encoded payload header first byte must be 0x00, but got %x", ep.bytes[0])
52+
}
53+
if ep.bytes[1] != byte(codecs.PayloadEncodingVersion0) {
54+
return nil, fmt.Errorf("encoded payload header version byte must be %x, but got %x", codecs.PayloadEncodingVersion0, ep.bytes[1])
55+
}
56+
4757
claimedLength := binary.BigEndian.Uint32(ep.bytes[2:6])
4858

4959
// decode raw data modulo bn254

api/clients/v2/payload_retriever.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package clients
33
import (
44
"context"
55

6+
_ "github.com/Layr-Labs/eigenda/api/clients/codecs"
67
"github.com/Layr-Labs/eigenda/api/clients/v2/coretypes"
78
)
89

@@ -12,5 +13,7 @@ import (
1213
// bucket instead of from EigenDA relays or nodes.
1314
type PayloadRetriever interface {
1415
// GetPayload retrieves a payload from some backend, using the provided certificate
16+
// GetPayload should return a [coretypes.ErrBlobDecodingFailedDerivationError] if the blob cannot be decoding according
17+
// to one of the encodings available via [codecs.PayloadEncodingVersion]s.
1518
GetPayload(ctx context.Context, eigenDACert coretypes.RetrievableEigenDACert) (*coretypes.Payload, error)
1619
}

api/clients/v2/payloadretrieval/relay_payload_retriever.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ func (pr *RelayPayloadRetriever) GetPayload(
130130
payload, err := blob.ToPayload(pr.config.PayloadPolynomialForm)
131131
if err != nil {
132132
pr.log.Error(
133-
`Commitment verification was successful, but conversion from blob to payload failed!
134-
This is likely a problem with the local configuration, but could potentially indicate
135-
malicious dispersed data. It should not be possible for a commitment to verify for an
136-
invalid blob!`,
133+
`Commitment verification was successful, but decoding of blob to payload failed!
134+
This client only supports blobs that were encoded using the codec(s) defined in
135+
https://github.com/Layr-Labs/eigenda/blob/86e27fa03/api/clients/codecs/blob_codec.go.`,
137136
"blobKey", blobKey.Hex(), "relayKey", relayKey, "eigenDACert", eigenDACert, "error", err)
138-
return nil, fmt.Errorf("decode blob: %w", err)
137+
return nil, coretypes.ErrBlobDecodingFailedDerivationError.WithMessage(
138+
fmt.Sprintf("blob %v from relay %v: %v", blobKey.Hex(), relayKey, err))
139139
}
140140

141141
return payload, nil

api/clients/v2/payloadretrieval/relay_payload_retriever_test.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ type RelayPayloadRetrieverTester struct {
3939
RelayPayloadRetriever *RelayPayloadRetriever
4040
MockRelayClient *clientsmock.MockRelayClient
4141
G1Srs []bn254.G1Affine
42-
PayloadPolynomialForm codecs.PolynomialForm
42+
}
43+
44+
func (t *RelayPayloadRetrieverTester) PayloadPolynomialForm() codecs.PolynomialForm {
45+
return t.RelayPayloadRetriever.config.PayloadPolynomialForm
4346
}
4447

4548
// buildRelayPayloadRetrieverTester sets up a client with mocks necessary for testing
@@ -76,7 +79,6 @@ func buildRelayPayloadRetrieverTester(t *testing.T) RelayPayloadRetrieverTester
7679
RelayPayloadRetriever: client,
7780
MockRelayClient: &mockRelayClient,
7881
G1Srs: g1Srs,
79-
PayloadPolynomialForm: clientConfig.PayloadPolynomialForm,
8082
}
8183
}
8284

@@ -88,11 +90,22 @@ func buildBlobAndCert(
8890
) (core.BlobKey, []byte, *coretypes.EigenDACertV3) {
8991

9092
payloadBytes := tester.Random.Bytes(tester.Random.Intn(maxPayloadBytes))
91-
blob, err := coretypes.NewPayload(payloadBytes).ToBlob(tester.PayloadPolynomialForm)
93+
blob, err := coretypes.NewPayload(payloadBytes).ToBlob(tester.PayloadPolynomialForm())
9294
require.NoError(t, err)
93-
9495
blobBytes := blob.Serialize()
9596
require.NotNil(t, blobBytes)
97+
blobKey, cert := buildCertFromBlobBytes(t, blobBytes, relayKeys)
98+
return blobKey, blobBytes, cert
99+
100+
}
101+
102+
// buildCert builds a blob key, blob bytes, and valid certificate from the given blob and relay keys.
103+
// It is used to generate a valid cert from a wrongly encoded blob, to test for decoding errors.
104+
func buildCertFromBlobBytes(
105+
t *testing.T,
106+
blobBytes []byte,
107+
relayKeys []core.RelayKey,
108+
) (core.BlobKey, *coretypes.EigenDACertV3) {
96109

97110
kzgConfig := &kzg.KzgConfig{
98111
G1Path: "../../../../inabox/resources/kzg/g1.point",
@@ -141,7 +154,7 @@ func buildBlobAndCert(
141154
blobKey, err := eigenDACert.ComputeBlobKey()
142155
require.NoError(t, err)
143156

144-
return *blobKey, blobBytes, eigenDACert
157+
return *blobKey, eigenDACert
145158
}
146159

147160
// TestGetPayloadSuccess tests that a blob is received without error in the happy case
@@ -446,3 +459,38 @@ func TestErrorClose(t *testing.T) {
446459

447460
tester.MockRelayClient.AssertExpectations(t)
448461
}
462+
463+
// TestCommitmentVerifiesButBlobToPayloadFails tests the case where commitment verification succeeds
464+
// but conversion from blob to payload fails. This is a critical edge case that should not be possible
465+
// with valid data, but could indicate malicious dispersed data.
466+
func TestCommitmentVerifiesButBlobToPayloadFails(t *testing.T) {
467+
tester := buildRelayPayloadRetrieverTester(t)
468+
// We keep the blob in coeff form so that we can manipulate it directly (otherwise it gets IFFT'd)
469+
tester.RelayPayloadRetriever.config.PayloadPolynomialForm = codecs.PolynomialFormCoeff
470+
relayKeys := make([]core.RelayKey, 1)
471+
relayKeys[0] = tester.Random.Uint32()
472+
473+
payloadBytes := tester.Random.Bytes(tester.Random.Intn(maxPayloadBytes))
474+
blob, err := coretypes.NewPayload(payloadBytes).ToBlob(tester.PayloadPolynomialForm())
475+
require.NoError(t, err)
476+
blobBytes := blob.Serialize()
477+
require.NotNil(t, blobBytes)
478+
blobBytes[1] = 0xFF // Invalid encoding version - this will cause decode to fail
479+
480+
blobKey, blobCert := buildCertFromBlobBytes(t, blobBytes, relayKeys)
481+
482+
// Mock the relay to return our incorrectly encoded blob
483+
tester.MockRelayClient.On("GetBlob", mock.Anything, relayKeys[0], blobKey).Return(blobBytes, nil).Once()
484+
485+
// Try to get the payload - this should fail during blob to payload conversion
486+
payload, err := tester.RelayPayloadRetriever.GetPayload(context.Background(), blobCert)
487+
require.Nil(t, payload)
488+
require.Error(t, err)
489+
490+
// Verify it's specifically a DerivationError with status code 4 (blob decoding failed)
491+
derivationErr := coretypes.DerivationError{}
492+
require.ErrorAs(t, err, &derivationErr)
493+
require.Equal(t, coretypes.ErrBlobDecodingFailedDerivationError.StatusCode, derivationErr.StatusCode)
494+
495+
tester.MockRelayClient.AssertExpectations(t)
496+
}

api/clients/v2/payloadretrieval/validator_payload_retriever.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,9 @@ func (pr *ValidatorPayloadRetriever) GetPayload(
102102
payload, err := blob.ToPayload(pr.config.PayloadPolynomialForm)
103103
if err != nil {
104104
pr.logger.Error(
105-
`Commitment verification was successful, but conversion from blob to payload failed!
106-
This is likely a problem with the local configuration, but could potentially indicate
107-
malicious dispersed data. It should not be possible for a commitment to verify for an
108-
invalid blob!`,
105+
`Commitment verification was successful, but decoding of blob to payload failed!
106+
This client only supports blobs that were encoded using the codec(s) defined in
107+
https://github.com/Layr-Labs/eigenda/blob/86e27fa03/api/clients/codecs/blob_codec.go.`,
109108
"blobKey", blobKey.Hex(), "quorumID", quorumID, "eigenDACert", eigenDACert, "error", err)
110109
return nil, fmt.Errorf("decode blob: %w", err)
111110
}

api/clients/v2/verification/cert_verifier.go

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,30 @@ func NewCertVerifier(
4747
}
4848

4949
// CheckDACert calls the CheckDACert view function on the EigenDACertVerifier contract.
50-
// This method returns nil if the certificate is successfully verified; otherwise, it returns an error.
50+
// This method returns nil if the certificate is successfully verified; otherwise, it returns a
51+
// [CertVerifierInvalidCertError] or [CertVerifierInternalError] error.
5152
func (cv *CertVerifier) CheckDACert(
5253
ctx context.Context,
5354
cert coretypes.EigenDACert,
5455
) error {
55-
// 1 - switch on the certificate version to determine which underlying type to decode into
56-
// and which contract to call
57-
58-
// EigenDACertV3 is the only version that is supported by the CheckDACert function
56+
// 1 - switch on the certificate type to determine which contract to call
5957
var certV3 *coretypes.EigenDACertV3
6058
var err error
61-
switch cert.Version() {
62-
case coretypes.VersionThreeCert:
63-
var ok bool
64-
certV3, ok = cert.(*coretypes.EigenDACertV3)
65-
if !ok {
66-
return &CertVerifierInputError{Msg: fmt.Sprintf("expected cert to be of type EigenDACertV3, got %T", cert)}
67-
}
68-
case coretypes.VersionTwoCert:
69-
certV2, ok := cert.(*coretypes.EigenDACertV2)
70-
if !ok {
71-
return &CertVerifierInputError{Msg: fmt.Sprintf("expected cert to be of type EigenDACertV2, got %T", cert)}
72-
}
73-
74-
certV3, err = certV2.ToV3()
59+
switch cert := cert.(type) {
60+
case *coretypes.EigenDACertV3:
61+
certV3 = cert
62+
case *coretypes.EigenDACertV2:
63+
// EigenDACertV3 is the only version that is supported by the CheckDACert function
64+
// but the V2 cert is a simple permutation of the V3 cert fields, so we convert it.
65+
certV3, err = cert.ToV3()
7566
if err != nil {
7667
return &CertVerifierInternalError{Msg: "convert V2 cert to V3", Err: err}
7768
}
7869
default:
79-
return &CertVerifierInputError{Msg: fmt.Sprintf("unsupported cert version: %d", cert.Version())}
70+
// If golang had enums the world would be a better place.
71+
panic(fmt.Sprintf("unsupported cert version: %T. All cert versions that we can "+
72+
"construct offchain should have a CertVerifier contract which we can call to "+
73+
"verify the certificate", cert))
8074
}
8175

8276
// 2 - Call the contract method CheckDACert to verify the certificate
@@ -108,9 +102,9 @@ func (cv *CertVerifier) CheckDACert(
108102
if verifyResultCode == coretypes.StatusNullError {
109103
return &CertVerifierInternalError{Msg: fmt.Sprintf("checkDACert eth-call bug: %s", verifyResultCode.String())}
110104
} else if verifyResultCode != coretypes.StatusSuccess {
111-
return &CertVerificationFailedError{
105+
return &CertVerifierInvalidCertError{
112106
StatusCode: verifyResultCode,
113-
Msg: fmt.Sprintf("cert verification failed: status code (%d) %s", verifyResultCode, verifyResultCode.String()),
107+
Msg: verifyResultCode.String(),
114108
}
115109
}
116110
return nil

0 commit comments

Comments
 (0)