Skip to content

Commit

Permalink
feat: implement custom error types for DA (#115)
Browse files Browse the repository at this point in the history
* feat: implement custom error types for DA

Introduced multiple custom error types for the DA package.
Because of JSON-RPC library used, each error has to be it's own type (can't use sentinel values).
gRPC on the other hand doesn't support wrapping typed errors, and codes are "standardized" (currently only "Not Found" used).

* feat: add detailed gRPC error handling for DA errors

Implemented detailed gRPC error handling by defining custom error types and appropriate error codes for better differentiation in the DA package. Each error type now provides a gRPC status with granular error details using the newly introduced error codes.

* refactor: add proper support for future height error

Introduced a new error code and corresponding error type for when a requested height is from the future. Updated functions to handle this new type and included its gRPC status representation.

* feat: add error mapping to all methods in gRPC proxy client

Previously, only `Get` method supported proper error mapping.

* refactor: extract error mapping into a reusable function

Centralize the error code to error type mapping by creating a reusable `getKnownErrorsMapping` function. This change reduces code redundancy and improves maintainability.

* refactor: reformat ErrorCode enum entries

Unified the indentation style for all enum entries in ErrorCode. This improves code readability and consistency within the proto file.

* chore: fix protobuf enum names (linting)

* chore: regenerate protobuf with latest version of protoc-gen-gocosmos

* docs: fix typos in comments

* refactor: group methods by type in errors.go
  • Loading branch information
tzdybal authored Nov 6, 2024
1 parent df792b1 commit 1b89957
Show file tree
Hide file tree
Showing 10 changed files with 497 additions and 58 deletions.
133 changes: 133 additions & 0 deletions errors.go
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
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;
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
}
return errorForCode(errorDetail.Code)
}
}
return err
}

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

0 comments on commit 1b89957

Please sign in to comment.