Skip to content
This repository was archived by the owner on May 7, 2025. It is now read-only.

Commit 1b89957

Browse files
authored
feat: implement custom error types for DA (#115)
* 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
1 parent df792b1 commit 1b89957

File tree

10 files changed

+497
-58
lines changed

10 files changed

+497
-58
lines changed

errors.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package da
2+
3+
import (
4+
"google.golang.org/grpc/codes"
5+
"google.golang.org/grpc/status"
6+
7+
pbda "github.com/rollkit/go-da/types/pb/da"
8+
)
9+
10+
// Code defines error codes for JSON-RPC.
11+
//
12+
// They are reused for gRPC
13+
type Code int
14+
15+
// gRPC checks for GRPCStatus method on errors to enable advanced error handling.
16+
17+
// Codes are used by JSON-RPC client and server
18+
const (
19+
CodeBlobNotFound Code = 32001
20+
CodeBlobSizeOverLimit Code = 32002
21+
CodeTxTimedOut Code = 32003
22+
CodeTxAlreadyInMempool Code = 32004
23+
CodeTxIncorrectAccountSequence Code = 32005
24+
CodeTxTooLarge Code = 32006
25+
CodeContextDeadline Code = 32007
26+
CodeFutureHeight Code = 32008
27+
)
28+
29+
// ErrBlobNotFound is used to indicate that the blob was not found.
30+
type ErrBlobNotFound struct{}
31+
32+
func (e *ErrBlobNotFound) Error() string {
33+
return "blob: not found"
34+
}
35+
36+
// GRPCStatus returns the gRPC status with details for an ErrBlobNotFound error.
37+
func (e *ErrBlobNotFound) GRPCStatus() *status.Status {
38+
return getGRPCStatus(e, codes.NotFound, pbda.ErrorCode_ERROR_CODE_BLOB_NOT_FOUND)
39+
}
40+
41+
// ErrBlobSizeOverLimit is used to indicate that the blob size is over limit.
42+
type ErrBlobSizeOverLimit struct{}
43+
44+
func (e *ErrBlobSizeOverLimit) Error() string {
45+
return "blob: over size limit"
46+
}
47+
48+
// GRPCStatus returns the gRPC status with details for an ErrBlobSizeOverLimit error.
49+
func (e *ErrBlobSizeOverLimit) GRPCStatus() *status.Status {
50+
return getGRPCStatus(e, codes.ResourceExhausted, pbda.ErrorCode_ERROR_CODE_BLOB_SIZE_OVER_LIMIT)
51+
}
52+
53+
// ErrTxTimedOut is the error message returned by the DA when mempool is congested.
54+
type ErrTxTimedOut struct{}
55+
56+
func (e *ErrTxTimedOut) Error() string {
57+
return "timed out waiting for tx to be included in a block"
58+
}
59+
60+
// GRPCStatus returns the gRPC status with details for an ErrTxTimedOut error.
61+
func (e *ErrTxTimedOut) GRPCStatus() *status.Status {
62+
return getGRPCStatus(e, codes.DeadlineExceeded, pbda.ErrorCode_ERROR_CODE_TX_TIMED_OUT)
63+
}
64+
65+
// ErrTxAlreadyInMempool is the error message returned by the DA when tx is already in mempool.
66+
type ErrTxAlreadyInMempool struct{}
67+
68+
func (e *ErrTxAlreadyInMempool) Error() string {
69+
return "tx already in mempool"
70+
}
71+
72+
// GRPCStatus returns the gRPC status with details for an ErrTxAlreadyInMempool error.
73+
func (e *ErrTxAlreadyInMempool) GRPCStatus() *status.Status {
74+
return getGRPCStatus(e, codes.AlreadyExists, pbda.ErrorCode_ERROR_CODE_TX_ALREADY_IN_MEMPOOL)
75+
}
76+
77+
// ErrTxIncorrectAccountSequence is the error message returned by the DA when tx has incorrect sequence.
78+
type ErrTxIncorrectAccountSequence struct{}
79+
80+
func (e *ErrTxIncorrectAccountSequence) Error() string {
81+
return "incorrect account sequence"
82+
}
83+
84+
// GRPCStatus returns the gRPC status with details for an ErrTxIncorrectAccountSequence error.
85+
func (e *ErrTxIncorrectAccountSequence) GRPCStatus() *status.Status {
86+
return getGRPCStatus(e, codes.InvalidArgument, pbda.ErrorCode_ERROR_CODE_TX_INCORRECT_ACCOUNT_SEQUENCE)
87+
}
88+
89+
// ErrTxTooLarge is the err message returned by the DA when tx size is too large.
90+
type ErrTxTooLarge struct{}
91+
92+
func (e *ErrTxTooLarge) Error() string {
93+
return "tx too large"
94+
}
95+
96+
// GRPCStatus returns the gRPC status with details for an ErrTxTooLarge error.
97+
func (e *ErrTxTooLarge) GRPCStatus() *status.Status {
98+
return getGRPCStatus(e, codes.ResourceExhausted, pbda.ErrorCode_ERROR_CODE_TX_TOO_LARGE)
99+
}
100+
101+
// ErrContextDeadline is the error message returned by the DA when context deadline exceeds.
102+
type ErrContextDeadline struct{}
103+
104+
func (e *ErrContextDeadline) Error() string {
105+
return "context deadline"
106+
}
107+
108+
// GRPCStatus returns the gRPC status with details for an ErrContextDeadline error.
109+
func (e *ErrContextDeadline) GRPCStatus() *status.Status {
110+
return getGRPCStatus(e, codes.DeadlineExceeded, pbda.ErrorCode_ERROR_CODE_CONTEXT_DEADLINE)
111+
}
112+
113+
// ErrFutureHeight is returned when requested height is from the future
114+
type ErrFutureHeight struct{}
115+
116+
func (e *ErrFutureHeight) Error() string {
117+
return "given height is from the future"
118+
}
119+
120+
// GRPCStatus returns the gRPC status with details for an ErrFutureHeight error.
121+
func (e *ErrFutureHeight) GRPCStatus() *status.Status {
122+
return getGRPCStatus(e, codes.OutOfRange, pbda.ErrorCode_ERROR_CODE_FUTURE_HEIGHT)
123+
}
124+
125+
// getGRPCStatus constructs a gRPC status with error details based on the provided error, gRPC code, and DA error code.
126+
func getGRPCStatus(err error, grpcCode codes.Code, daCode pbda.ErrorCode) *status.Status {
127+
base := status.New(grpcCode, err.Error())
128+
detailed, err := base.WithDetails(&pbda.ErrorDetails{Code: daCode})
129+
if err != nil {
130+
return base
131+
}
132+
return detailed
133+
}

proto/da/da.proto

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,19 @@ message ValidateRequest {
130130
message ValidateResponse {
131131
repeated bool results = 1;
132132
}
133+
134+
enum ErrorCode {
135+
ERROR_CODE_UNSPECIFIED = 0;
136+
ERROR_CODE_BLOB_NOT_FOUND = 32001;
137+
ERROR_CODE_BLOB_SIZE_OVER_LIMIT = 32002;
138+
ERROR_CODE_TX_TIMED_OUT = 32003;
139+
ERROR_CODE_TX_ALREADY_IN_MEMPOOL = 32004;
140+
ERROR_CODE_TX_INCORRECT_ACCOUNT_SEQUENCE = 32005;
141+
ERROR_CODE_TX_TOO_LARGE = 32006;
142+
ERROR_CODE_CONTEXT_DEADLINE = 32007;
143+
ERROR_CODE_FUTURE_HEIGHT = 32008;
144+
}
145+
146+
message ErrorDetails {
147+
ErrorCode code = 1;
148+
}

proxy/grpc/client.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (c *Client) MaxBlobSize(ctx context.Context) (uint64, error) {
4343
req := &pbda.MaxBlobSizeRequest{}
4444
resp, err := c.client.MaxBlobSize(ctx, req)
4545
if err != nil {
46-
return 0, err
46+
return 0, tryToMapError(err)
4747
}
4848
return resp.MaxBlobSize, nil
4949
}
@@ -59,7 +59,7 @@ func (c *Client) Get(ctx context.Context, ids []da.ID, namespace da.Namespace) (
5959
}
6060
resp, err := c.client.Get(ctx, req)
6161
if err != nil {
62-
return nil, err
62+
return nil, tryToMapError(err)
6363
}
6464

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

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

104104
resp, err := c.client.Commit(ctx, req)
105105
if err != nil {
106-
return nil, err
106+
return nil, tryToMapError(err)
107107
}
108108

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

120120
resp, err := c.client.Submit(ctx, req)
121121
if err != nil {
122-
return nil, err
122+
return nil, tryToMapError(err)
123123
}
124124

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

142142
resp, err := c.client.Submit(ctx, req)
143143
if err != nil {
144-
return nil, err
144+
return nil, tryToMapError(err)
145145
}
146146

147147
ids := make([]da.ID, len(resp.Ids))
@@ -160,5 +160,5 @@ func (c *Client) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, n
160160
Namespace: &pbda.Namespace{Value: namespace},
161161
}
162162
resp, err := c.client.Validate(ctx, req)
163-
return resp.Results, err
163+
return resp.Results, tryToMapError(err)
164164
}

proxy/grpc/errors.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package grpc
2+
3+
import (
4+
"errors"
5+
6+
"google.golang.org/grpc/status"
7+
8+
"github.com/rollkit/go-da"
9+
pbda "github.com/rollkit/go-da/types/pb/da"
10+
)
11+
12+
func tryToMapError(err error) error {
13+
if err == nil {
14+
return nil
15+
}
16+
17+
s, ok := status.FromError(err)
18+
if ok {
19+
details := s.Proto().Details
20+
if len(details) == 1 {
21+
var errorDetail pbda.ErrorDetails
22+
unmarshalError := errorDetail.Unmarshal(details[0].Value)
23+
if unmarshalError != nil {
24+
return err
25+
}
26+
return errorForCode(errorDetail.Code)
27+
}
28+
}
29+
return err
30+
}
31+
32+
func errorForCode(code pbda.ErrorCode) error {
33+
switch code {
34+
case pbda.ErrorCode_ERROR_CODE_BLOB_NOT_FOUND:
35+
return &da.ErrBlobNotFound{}
36+
case pbda.ErrorCode_ERROR_CODE_BLOB_SIZE_OVER_LIMIT:
37+
return &da.ErrBlobSizeOverLimit{}
38+
case pbda.ErrorCode_ERROR_CODE_TX_TIMED_OUT:
39+
return &da.ErrTxTimedOut{}
40+
case pbda.ErrorCode_ERROR_CODE_TX_ALREADY_IN_MEMPOOL:
41+
return &da.ErrTxAlreadyInMempool{}
42+
case pbda.ErrorCode_ERROR_CODE_TX_INCORRECT_ACCOUNT_SEQUENCE:
43+
return &da.ErrTxIncorrectAccountSequence{}
44+
case pbda.ErrorCode_ERROR_CODE_TX_TOO_LARGE:
45+
return &da.ErrTxTooLarge{}
46+
case pbda.ErrorCode_ERROR_CODE_CONTEXT_DEADLINE:
47+
return &da.ErrContextDeadline{}
48+
case pbda.ErrorCode_ERROR_CODE_FUTURE_HEIGHT:
49+
return &da.ErrFutureHeight{}
50+
default:
51+
return errors.New("unknown error code")
52+
}
53+
}

proxy/jsonrpc/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ func NewClient(ctx context.Context, addr string, token string) (*Client, error)
109109
func newClient(ctx context.Context, addr string, authHeader http.Header) (*Client, error) {
110110
var multiCloser multiClientCloser
111111
var client Client
112+
errs := getKnownErrorsMapping()
112113
for name, module := range moduleMap(&client) {
113-
closer, err := jsonrpc.NewClient(ctx, addr, name, module, authHeader)
114+
closer, err := jsonrpc.NewMergeClient(ctx, addr, name, []interface{}{module}, authHeader, jsonrpc.WithErrors(errs))
114115
if err != nil {
115116
return nil, err
116117
}

proxy/jsonrpc/errors.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package jsonrpc
2+
3+
import (
4+
"github.com/filecoin-project/go-jsonrpc"
5+
6+
"github.com/rollkit/go-da"
7+
)
8+
9+
// getKnownErrorsMapping returns a mapping of known error codes to their corresponding error types.
10+
func getKnownErrorsMapping() jsonrpc.Errors {
11+
errs := jsonrpc.NewErrors()
12+
errs.Register(jsonrpc.ErrorCode(da.CodeBlobNotFound), new(*da.ErrBlobNotFound))
13+
errs.Register(jsonrpc.ErrorCode(da.CodeBlobSizeOverLimit), new(*da.ErrBlobSizeOverLimit))
14+
errs.Register(jsonrpc.ErrorCode(da.CodeTxTimedOut), new(*da.ErrTxTimedOut))
15+
errs.Register(jsonrpc.ErrorCode(da.CodeTxAlreadyInMempool), new(*da.ErrTxAlreadyInMempool))
16+
errs.Register(jsonrpc.ErrorCode(da.CodeTxIncorrectAccountSequence), new(*da.ErrTxIncorrectAccountSequence))
17+
errs.Register(jsonrpc.ErrorCode(da.CodeTxTooLarge), new(*da.ErrTxTooLarge))
18+
errs.Register(jsonrpc.ErrorCode(da.CodeContextDeadline), new(*da.ErrContextDeadline))
19+
errs.Register(jsonrpc.ErrorCode(da.CodeFutureHeight), new(*da.ErrFutureHeight))
20+
return errs
21+
}

proxy/jsonrpc/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (s *Server) RegisterService(namespace string, service interface{}, out inte
3232

3333
// NewServer accepts the host address port and the DA implementation to serve as a jsonrpc service
3434
func NewServer(address, port string, DA da.DA) *Server {
35-
rpc := jsonrpc.NewServer()
35+
rpc := jsonrpc.NewServer(jsonrpc.WithServerErrors(getKnownErrorsMapping()))
3636
srv := &Server{
3737
rpc: rpc,
3838
srv: &http.Server{

test/dummy.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ import (
1717
// DefaultMaxBlobSize is the default max blob size
1818
const DefaultMaxBlobSize = 64 * 64 * 482
1919

20-
// ErrTooHigh is returned when requested height is to high
21-
var ErrTooHigh = errors.New("given height is from the future")
22-
2320
// DummyDA is a simple implementation of in-memory DA. Not production ready! Intended only for testing!
2421
//
2522
// Data is stored in a map, where key is a serialized sequence number. This key is returned as ID.
@@ -78,7 +75,7 @@ func (d *DummyDA) Get(ctx context.Context, ids []da.ID, _ da.Namespace) ([]da.Bl
7875
}
7976
}
8077
if !found {
81-
return nil, errors.New("no blob for given ID")
78+
return nil, &da.ErrBlobNotFound{}
8279
}
8380
}
8481
return blobs, nil
@@ -90,7 +87,7 @@ func (d *DummyDA) GetIDs(ctx context.Context, height uint64, _ da.Namespace) (*d
9087
defer d.mu.Unlock()
9188

9289
if height > d.height {
93-
return nil, ErrTooHigh
90+
return nil, &da.ErrFutureHeight{}
9491
}
9592

9693
kvps, ok := d.data[height]

test/test_suite.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ func BasicDATest(t *testing.T, d da.DA) {
8181
// CheckErrors ensures that errors are handled properly by DA.
8282
func CheckErrors(t *testing.T, d da.DA) {
8383
ctx := context.TODO()
84-
blob, err := d.Get(ctx, []da.ID{[]byte("invalid")}, testNamespace)
84+
blob, err := d.Get(ctx, []da.ID{[]byte("invalid blob id")}, testNamespace)
8585
assert.Error(t, err)
86+
assert.ErrorIs(t, err, &da.ErrBlobNotFound{})
8687
assert.Empty(t, blob)
8788
}
8889

@@ -140,7 +141,7 @@ func ConcurrentReadWriteTest(t *testing.T, d da.DA) {
140141
for i := uint64(1); i <= 100; i++ {
141142
_, err := d.GetIDs(ctx, i, []byte{})
142143
if err != nil {
143-
assert.Equal(t, err.Error(), ErrTooHigh.Error())
144+
assert.ErrorIs(t, err, &da.ErrFutureHeight{})
144145
}
145146
}
146147
}()
@@ -161,5 +162,6 @@ func HeightFromFutureTest(t *testing.T, d da.DA) {
161162
ctx := context.TODO()
162163
ret, err := d.GetIDs(ctx, 999999999, []byte{})
163164
assert.Error(t, err)
165+
assert.ErrorIs(t, err, &da.ErrFutureHeight{})
164166
assert.Nil(t, ret)
165167
}

0 commit comments

Comments
 (0)