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

feat: implement custom error types for DA #115

Merged
merged 10 commits into from
Nov 6, 2024
133 changes: 133 additions & 0 deletions errors.go
tzdybal marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package da

import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

pbda "github.com/rollkit/go-da/types/pb/da"
)

// Code defines error codes for JSON-RPC.
//
// They are reused for gRPC
type Code int

// gRPC checks for GRPCStatus method on errors to enable advanced error handling.

// Codes are used by JSON-RPC client and server
const (
CodeBlobNotFound Code = 32001
tzdybal marked this conversation as resolved.
Show resolved Hide resolved
CodeBlobSizeOverLimit Code = 32002
CodeTxTimedOut Code = 32003
CodeTxAlreadyInMempool Code = 32004
CodeTxIncorrectAccountSequence Code = 32005
CodeTxTooLarge Code = 32006
CodeContextDeadline Code = 32007
CodeFutureHeight Code = 32008
)

// ErrBlobNotFound is used to indicate that the blob was not found.
type ErrBlobNotFound struct{}

func (e *ErrBlobNotFound) Error() string {
return "blob: not found"
}

// GRPCStatus returns the gRPC status with details for an ErrBlobNotFound error.
func (e *ErrBlobNotFound) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.NotFound, pbda.ErrorCode_ERROR_CODE_BLOB_NOT_FOUND)
}

// ErrBlobSizeOverLimit is used to indicate that the blob size is over limit.
type ErrBlobSizeOverLimit struct{}

func (e *ErrBlobSizeOverLimit) Error() string {
return "blob: over size limit"
}

// GRPCStatus returns the gRPC status with details for an ErrBlobSizeOverLimit error.
func (e *ErrBlobSizeOverLimit) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.ResourceExhausted, pbda.ErrorCode_ERROR_CODE_BLOB_SIZE_OVER_LIMIT)
}

// ErrTxTimedOut is the error message returned by the DA when mempool is congested.
type ErrTxTimedOut struct{}

func (e *ErrTxTimedOut) Error() string {
return "timed out waiting for tx to be included in a block"
}

// GRPCStatus returns the gRPC status with details for an ErrTxTimedOut error.
func (e *ErrTxTimedOut) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.DeadlineExceeded, pbda.ErrorCode_ERROR_CODE_TX_TIMED_OUT)
}

// ErrTxAlreadyInMempool is the error message returned by the DA when tx is already in mempool.
type ErrTxAlreadyInMempool struct{}

func (e *ErrTxAlreadyInMempool) Error() string {
return "tx already in mempool"
}

// GRPCStatus returns the gRPC status with details for an ErrTxAlreadyInMempool error.
func (e *ErrTxAlreadyInMempool) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.AlreadyExists, pbda.ErrorCode_ERROR_CODE_TX_ALREADY_IN_MEMPOOL)
}

// ErrTxIncorrectAccountSequence is the error message returned by the DA when tx has incorrect sequence.
type ErrTxIncorrectAccountSequence struct{}

func (e *ErrTxIncorrectAccountSequence) Error() string {
return "incorrect account sequence"
}

// GRPCStatus returns the gRPC status with details for an ErrTxIncorrectAccountSequence error.
func (e *ErrTxIncorrectAccountSequence) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.InvalidArgument, pbda.ErrorCode_ERROR_CODE_TX_INCORRECT_ACCOUNT_SEQUENCE)
}

// ErrTxTooLarge is the err message returned by the DA when tx size is too large.
type ErrTxTooLarge struct{}

func (e *ErrTxTooLarge) Error() string {
return "tx too large"
}

// GRPCStatus returns the gRPC status with details for an ErrTxTooLarge error.
func (e *ErrTxTooLarge) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.ResourceExhausted, pbda.ErrorCode_ERROR_CODE_TX_TOO_LARGE)
}

// ErrContextDeadline is the error message returned by the DA when context deadline exceeds.
type ErrContextDeadline struct{}

func (e *ErrContextDeadline) Error() string {
return "context deadline"
}

// GRPCStatus returns the gRPC status with details for an ErrContextDeadline error.
func (e *ErrContextDeadline) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.DeadlineExceeded, pbda.ErrorCode_ERROR_CODE_CONTEXT_DEADLINE)
}

// ErrFutureHeight is returned when requested height is from the future
type ErrFutureHeight struct{}

func (e *ErrFutureHeight) Error() string {
return "given height is from the future"
}

// GRPCStatus returns the gRPC status with details for an ErrFutureHeight error.
func (e *ErrFutureHeight) GRPCStatus() *status.Status {
return getGRPCStatus(e, codes.OutOfRange, pbda.ErrorCode_ERROR_CODE_FUTURE_HEIGHT)
}

// getGRPCStatus constructs a gRPC status with error details based on the provided error, gRPC code, and DA error code.
func getGRPCStatus(err error, grpcCode codes.Code, daCode pbda.ErrorCode) *status.Status {
base := status.New(grpcCode, err.Error())
detailed, err := base.WithDetails(&pbda.ErrorDetails{Code: daCode})
if err != nil {
return base
}
return detailed
}
16 changes: 16 additions & 0 deletions proto/da/da.proto
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,19 @@ message ValidateRequest {
message ValidateResponse {
repeated bool results = 1;
}

enum ErrorCode {
ERROR_CODE_UNSPECIFIED = 0;
gupadhyaya marked this conversation as resolved.
Show resolved Hide resolved
ERROR_CODE_BLOB_NOT_FOUND = 32001;
ERROR_CODE_BLOB_SIZE_OVER_LIMIT = 32002;
ERROR_CODE_TX_TIMED_OUT = 32003;
ERROR_CODE_TX_ALREADY_IN_MEMPOOL = 32004;
ERROR_CODE_TX_INCORRECT_ACCOUNT_SEQUENCE = 32005;
ERROR_CODE_TX_TOO_LARGE = 32006;
ERROR_CODE_CONTEXT_DEADLINE = 32007;
ERROR_CODE_FUTURE_HEIGHT = 32008;
}

message ErrorDetails {
ErrorCode code = 1;
}
14 changes: 7 additions & 7 deletions proxy/grpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (c *Client) MaxBlobSize(ctx context.Context) (uint64, error) {
req := &pbda.MaxBlobSizeRequest{}
resp, err := c.client.MaxBlobSize(ctx, req)
if err != nil {
return 0, err
return 0, tryToMapError(err)
}
return resp.MaxBlobSize, nil
}
Expand All @@ -59,7 +59,7 @@ func (c *Client) Get(ctx context.Context, ids []da.ID, namespace da.Namespace) (
}
resp, err := c.client.Get(ctx, req)
if err != nil {
return nil, err
return nil, tryToMapError(err)
}

return blobsPB2DA(resp.Blobs), nil
Expand All @@ -70,7 +70,7 @@ func (c *Client) GetIDs(ctx context.Context, height uint64, namespace da.Namespa
req := &pbda.GetIdsRequest{Height: height, Namespace: &pbda.Namespace{Value: namespace}}
resp, err := c.client.GetIds(ctx, req)
if err != nil {
return nil, err
return nil, tryToMapError(err)
}

timestamp, err := types.TimestampFromProto(resp.Timestamp)
Expand Down Expand Up @@ -103,7 +103,7 @@ func (c *Client) Commit(ctx context.Context, blobs []da.Blob, namespace da.Names

resp, err := c.client.Commit(ctx, req)
if err != nil {
return nil, err
return nil, tryToMapError(err)
}

return commitsPB2DA(resp.Commitments), nil
Expand All @@ -119,7 +119,7 @@ func (c *Client) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64,

resp, err := c.client.Submit(ctx, req)
if err != nil {
return nil, err
return nil, tryToMapError(err)
}

ids := make([]da.ID, len(resp.Ids))
Expand All @@ -141,7 +141,7 @@ func (c *Client) SubmitWithOptions(ctx context.Context, blobs []da.Blob, gasPric

resp, err := c.client.Submit(ctx, req)
if err != nil {
return nil, err
return nil, tryToMapError(err)
}

ids := make([]da.ID, len(resp.Ids))
Expand All @@ -160,5 +160,5 @@ func (c *Client) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, n
Namespace: &pbda.Namespace{Value: namespace},
}
resp, err := c.client.Validate(ctx, req)
return resp.Results, err
return resp.Results, tryToMapError(err)
}
53 changes: 53 additions & 0 deletions proxy/grpc/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package grpc

import (
"errors"

"google.golang.org/grpc/status"

"github.com/rollkit/go-da"
pbda "github.com/rollkit/go-da/types/pb/da"
)

func tryToMapError(err error) error {
if err == nil {
return nil
}

s, ok := status.FromError(err)
if ok {
details := s.Proto().Details
if len(details) == 1 {
var errorDetail pbda.ErrorDetails
unmarshalError := errorDetail.Unmarshal(details[0].Value)
if unmarshalError != nil {
return err
}
tzdybal marked this conversation as resolved.
Show resolved Hide resolved
return errorForCode(errorDetail.Code)
}
}
return err
}
tzdybal marked this conversation as resolved.
Show resolved Hide resolved

func errorForCode(code pbda.ErrorCode) error {
switch code {
case pbda.ErrorCode_ERROR_CODE_BLOB_NOT_FOUND:
return &da.ErrBlobNotFound{}
case pbda.ErrorCode_ERROR_CODE_BLOB_SIZE_OVER_LIMIT:
return &da.ErrBlobSizeOverLimit{}
case pbda.ErrorCode_ERROR_CODE_TX_TIMED_OUT:
return &da.ErrTxTimedOut{}
case pbda.ErrorCode_ERROR_CODE_TX_ALREADY_IN_MEMPOOL:
return &da.ErrTxAlreadyInMempool{}
case pbda.ErrorCode_ERROR_CODE_TX_INCORRECT_ACCOUNT_SEQUENCE:
return &da.ErrTxIncorrectAccountSequence{}
case pbda.ErrorCode_ERROR_CODE_TX_TOO_LARGE:
return &da.ErrTxTooLarge{}
case pbda.ErrorCode_ERROR_CODE_CONTEXT_DEADLINE:
return &da.ErrContextDeadline{}
case pbda.ErrorCode_ERROR_CODE_FUTURE_HEIGHT:
return &da.ErrFutureHeight{}
default:
return errors.New("unknown error code")
}
}
3 changes: 2 additions & 1 deletion proxy/jsonrpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ func NewClient(ctx context.Context, addr string, token string) (*Client, error)
func newClient(ctx context.Context, addr string, authHeader http.Header) (*Client, error) {
var multiCloser multiClientCloser
var client Client
errs := getKnownErrorsMapping()
for name, module := range moduleMap(&client) {
closer, err := jsonrpc.NewClient(ctx, addr, name, module, authHeader)
closer, err := jsonrpc.NewMergeClient(ctx, addr, name, []interface{}{module}, authHeader, jsonrpc.WithErrors(errs))
if err != nil {
return nil, err
}
Expand Down
21 changes: 21 additions & 0 deletions proxy/jsonrpc/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package jsonrpc

import (
"github.com/filecoin-project/go-jsonrpc"

"github.com/rollkit/go-da"
)

// getKnownErrorsMapping returns a mapping of known error codes to their corresponding error types.
func getKnownErrorsMapping() jsonrpc.Errors {
errs := jsonrpc.NewErrors()
errs.Register(jsonrpc.ErrorCode(da.CodeBlobNotFound), new(*da.ErrBlobNotFound))
errs.Register(jsonrpc.ErrorCode(da.CodeBlobSizeOverLimit), new(*da.ErrBlobSizeOverLimit))
errs.Register(jsonrpc.ErrorCode(da.CodeTxTimedOut), new(*da.ErrTxTimedOut))
errs.Register(jsonrpc.ErrorCode(da.CodeTxAlreadyInMempool), new(*da.ErrTxAlreadyInMempool))
errs.Register(jsonrpc.ErrorCode(da.CodeTxIncorrectAccountSequence), new(*da.ErrTxIncorrectAccountSequence))
errs.Register(jsonrpc.ErrorCode(da.CodeTxTooLarge), new(*da.ErrTxTooLarge))
errs.Register(jsonrpc.ErrorCode(da.CodeContextDeadline), new(*da.ErrContextDeadline))
errs.Register(jsonrpc.ErrorCode(da.CodeFutureHeight), new(*da.ErrFutureHeight))
return errs
}
2 changes: 1 addition & 1 deletion proxy/jsonrpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (s *Server) RegisterService(namespace string, service interface{}, out inte

// NewServer accepts the host address port and the DA implementation to serve as a jsonrpc service
func NewServer(address, port string, DA da.DA) *Server {
rpc := jsonrpc.NewServer()
rpc := jsonrpc.NewServer(jsonrpc.WithServerErrors(getKnownErrorsMapping()))
srv := &Server{
rpc: rpc,
srv: &http.Server{
Expand Down
7 changes: 2 additions & 5 deletions test/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import (
// DefaultMaxBlobSize is the default max blob size
const DefaultMaxBlobSize = 64 * 64 * 482

// ErrTooHigh is returned when requested height is to high
var ErrTooHigh = errors.New("given height is from the future")

// DummyDA is a simple implementation of in-memory DA. Not production ready! Intended only for testing!
//
// Data is stored in a map, where key is a serialized sequence number. This key is returned as ID.
Expand Down Expand Up @@ -78,7 +75,7 @@ func (d *DummyDA) Get(ctx context.Context, ids []da.ID, _ da.Namespace) ([]da.Bl
}
}
if !found {
return nil, errors.New("no blob for given ID")
return nil, &da.ErrBlobNotFound{}
}
}
return blobs, nil
Expand All @@ -90,7 +87,7 @@ func (d *DummyDA) GetIDs(ctx context.Context, height uint64, _ da.Namespace) (*d
defer d.mu.Unlock()

if height > d.height {
return nil, ErrTooHigh
return nil, &da.ErrFutureHeight{}
}

kvps, ok := d.data[height]
Expand Down
6 changes: 4 additions & 2 deletions test/test_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ func BasicDATest(t *testing.T, d da.DA) {
// CheckErrors ensures that errors are handled properly by DA.
func CheckErrors(t *testing.T, d da.DA) {
ctx := context.TODO()
blob, err := d.Get(ctx, []da.ID{[]byte("invalid")}, testNamespace)
blob, err := d.Get(ctx, []da.ID{[]byte("invalid blob id")}, testNamespace)
assert.Error(t, err)
assert.ErrorIs(t, err, &da.ErrBlobNotFound{})
assert.Empty(t, blob)
}

Expand Down Expand Up @@ -140,7 +141,7 @@ func ConcurrentReadWriteTest(t *testing.T, d da.DA) {
for i := uint64(1); i <= 100; i++ {
_, err := d.GetIDs(ctx, i, []byte{})
if err != nil {
assert.Equal(t, err.Error(), ErrTooHigh.Error())
assert.ErrorIs(t, err, &da.ErrFutureHeight{})
}
}
}()
Expand All @@ -161,5 +162,6 @@ func HeightFromFutureTest(t *testing.T, d da.DA) {
ctx := context.TODO()
ret, err := d.GetIDs(ctx, 999999999, []byte{})
assert.Error(t, err)
assert.ErrorIs(t, err, &da.ErrFutureHeight{})
assert.Nil(t, ret)
}
Loading
Loading