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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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

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

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

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

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

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

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

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

func (e *ErrContextDeadline) Error() string {
return "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"
}

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

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

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

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

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

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

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

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

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

// 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)
}
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when does this happen? doesn't look like it is being used anywhere?

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