diff --git a/proxy/jsonrpc/client.go b/proxy/jsonrpc/client.go deleted file mode 100644 index 294a88e..0000000 --- a/proxy/jsonrpc/client.go +++ /dev/null @@ -1,198 +0,0 @@ -package jsonrpc - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/rollkit/go-execution/types" -) - -// Client defines JSON-RPC proxy client of execution API. -type Client struct { - endpoint string - client *http.Client - config *Config -} - -// NewClient creates new proxy client with default config. -func NewClient() *Client { - return &Client{ - config: DefaultConfig(), - client: &http.Client{}, - } -} - -// SetConfig updates the client's configuration with the provided config. -func (c *Client) SetConfig(config *Config) { - if config != nil { - c.config = config - c.client.Timeout = config.DefaultTimeout - } -} - -// Start is used to start the client. -func (c *Client) Start(endpoint string) error { - c.endpoint = endpoint - return nil -} - -// Stop method is used to stop the client. -func (c *Client) Stop() error { - return nil -} - -// InitChain initializes the blockchain with genesis information. -func (c *Client) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (types.Hash, uint64, error) { - params := map[string]interface{}{ - "genesis_time": genesisTime.Unix(), - "initial_height": initialHeight, - "chain_id": chainID, - } - - var result struct { - StateRoot string `json:"state_root"` - MaxBytes uint64 `json:"max_bytes"` - } - - if err := c.call(ctx, "init_chain", params, &result); err != nil { - return types.Hash{}, 0, err - } - - stateRootBytes, err := base64.StdEncoding.DecodeString(result.StateRoot) - if err != nil { - return types.Hash{}, 0, fmt.Errorf("failed to decode state root: %w", err) - } - - var stateRoot types.Hash - copy(stateRoot[:], stateRootBytes) - - return stateRoot, result.MaxBytes, nil -} - -// GetTxs retrieves all available transactions from the execution client's mempool. -func (c *Client) GetTxs(ctx context.Context) ([]types.Tx, error) { - var result struct { - Txs []string `json:"txs"` - } - - if err := c.call(ctx, "get_txs", nil, &result); err != nil { - return nil, err - } - - txs := make([]types.Tx, len(result.Txs)) - for i, encodedTx := range result.Txs { - tx, err := base64.StdEncoding.DecodeString(encodedTx) - if err != nil { - return nil, fmt.Errorf("failed to decode tx: %w", err) - } - txs[i] = tx - } - - return txs, nil -} - -// ExecuteTxs executes a set of transactions to produce a new block header. -func (c *Client) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHeight uint64, timestamp time.Time, prevStateRoot types.Hash) (types.Hash, uint64, error) { - // Encode txs to base64 - encodedTxs := make([]string, len(txs)) - for i, tx := range txs { - encodedTxs[i] = base64.StdEncoding.EncodeToString(tx) - } - - params := map[string]interface{}{ - "txs": encodedTxs, - "block_height": blockHeight, - "timestamp": timestamp.Unix(), - "prev_state_root": base64.StdEncoding.EncodeToString(prevStateRoot[:]), - } - - var result struct { - UpdatedStateRoot string `json:"updated_state_root"` - MaxBytes uint64 `json:"max_bytes"` - } - - if err := c.call(ctx, "execute_txs", params, &result); err != nil { - return types.Hash{}, 0, err - } - - updatedStateRootBytes, err := base64.StdEncoding.DecodeString(result.UpdatedStateRoot) - if err != nil { - return types.Hash{}, 0, fmt.Errorf("failed to decode updated state root: %w", err) - } - - var updatedStateRoot types.Hash - copy(updatedStateRoot[:], updatedStateRootBytes) - - return updatedStateRoot, result.MaxBytes, nil -} - -// SetFinal marks a block at the given height as final. -func (c *Client) SetFinal(ctx context.Context, blockHeight uint64) error { - params := map[string]interface{}{ - "block_height": blockHeight, - } - - return c.call(ctx, "set_final", params, nil) -} - -func (c *Client) call(ctx context.Context, method string, params interface{}, result interface{}) error { - request := struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` - ID int `json:"id"` - }{ - JSONRPC: "2.0", - Method: method, - Params: params, - ID: 1, - } - - reqBody, err := json.Marshal(request) - if err != nil { - return fmt.Errorf("failed to marshal request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, "POST", c.endpoint, bytes.NewReader(reqBody)) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - - resp, err := c.client.Do(req) - if err != nil { - return fmt.Errorf("failed to send request: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - var jsonRPCResponse struct { - Error *jsonRPCError `json:"error,omitempty"` - Result json.RawMessage `json:"result,omitempty"` - } - - if err := json.NewDecoder(resp.Body).Decode(&jsonRPCResponse); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - if jsonRPCResponse.Error != nil { - return fmt.Errorf("RPC error: %d %s", jsonRPCResponse.Error.Code, jsonRPCResponse.Error.Message) - } - - if result != nil { - if err := json.Unmarshal(jsonRPCResponse.Result, result); err != nil { - return fmt.Errorf("failed to unmarshal result: %w", err) - } - } - - return nil -} diff --git a/proxy/jsonrpc/client_server_test.go b/proxy/jsonrpc/client_server_test.go deleted file mode 100644 index ebb6a5d..0000000 --- a/proxy/jsonrpc/client_server_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package jsonrpc_test - -import ( - "context" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/rollkit/go-execution/mocks" - jsonrpcproxy "github.com/rollkit/go-execution/proxy/jsonrpc" - "github.com/rollkit/go-execution/types" -) - -func TestClientServer(t *testing.T) { - mockExec := mocks.NewMockExecutor(t) - config := &jsonrpcproxy.Config{ - DefaultTimeout: 5 * time.Second, - MaxRequestSize: 1024 * 1024, - } - server := jsonrpcproxy.NewServer(mockExec, config) - - testServer := httptest.NewServer(server) - defer testServer.Close() - - client := jsonrpcproxy.NewClient() - client.SetConfig(config) - - err := client.Start(testServer.URL) - require.NoError(t, err) - defer func() { _ = client.Stop() }() - - t.Run("InitChain", func(t *testing.T) { - genesisTime := time.Now().UTC().Truncate(time.Second) - initialHeight := uint64(1) - chainID := "test-chain" - - expectedStateRoot := make([]byte, 32) - copy(expectedStateRoot, []byte{1, 2, 3}) - var stateRootHash types.Hash - copy(stateRootHash[:], expectedStateRoot) - - expectedMaxBytes := uint64(1000000) - - // convert time to Unix and back to ensure consistency - unixTime := genesisTime.Unix() - expectedTime := time.Unix(unixTime, 0).UTC() - - mockExec.On("InitChain", mock.Anything, expectedTime, initialHeight, chainID). - Return(stateRootHash, expectedMaxBytes, nil).Once() - - stateRoot, maxBytes, err := client.InitChain(context.TODO(), genesisTime, initialHeight, chainID) - - require.NoError(t, err) - assert.Equal(t, stateRootHash, stateRoot) - assert.Equal(t, expectedMaxBytes, maxBytes) - mockExec.AssertExpectations(t) - }) - - t.Run("GetTxs", func(t *testing.T) { - expectedTxs := []types.Tx{[]byte("tx1"), []byte("tx2")} - mockExec.On("GetTxs", mock.Anything).Return(expectedTxs, nil).Once() - - txs, err := client.GetTxs(context.TODO()) - require.NoError(t, err) - assert.Equal(t, expectedTxs, txs) - mockExec.AssertExpectations(t) - }) - - t.Run("ExecuteTxs", func(t *testing.T) { - txs := []types.Tx{[]byte("tx1"), []byte("tx2")} - blockHeight := uint64(1) - timestamp := time.Now().UTC().Truncate(time.Second) - - var prevStateRoot types.Hash - copy(prevStateRoot[:], []byte{1, 2, 3}) - - var expectedStateRoot types.Hash - copy(expectedStateRoot[:], []byte{4, 5, 6}) - - expectedMaxBytes := uint64(1000000) - - // convert time to Unix and back to ensure consistency - unixTime := timestamp.Unix() - expectedTime := time.Unix(unixTime, 0).UTC() - - mockExec.On("ExecuteTxs", mock.Anything, txs, blockHeight, expectedTime, prevStateRoot). - Return(expectedStateRoot, expectedMaxBytes, nil).Once() - - updatedStateRoot, maxBytes, err := client.ExecuteTxs(context.TODO(), txs, blockHeight, timestamp, prevStateRoot) - - require.NoError(t, err) - assert.Equal(t, expectedStateRoot, updatedStateRoot) - assert.Equal(t, expectedMaxBytes, maxBytes) - mockExec.AssertExpectations(t) - }) - - t.Run("SetFinal", func(t *testing.T) { - blockHeight := uint64(1) - mockExec.On("SetFinal", mock.Anything, blockHeight).Return(nil).Once() - - err := client.SetFinal(context.TODO(), blockHeight) - require.NoError(t, err) - mockExec.AssertExpectations(t) - }) -} diff --git a/proxy/jsonrpc/config.go b/proxy/jsonrpc/config.go deleted file mode 100644 index 409f64d..0000000 --- a/proxy/jsonrpc/config.go +++ /dev/null @@ -1,17 +0,0 @@ -package jsonrpc - -import "time" - -// Config represents configuration settings for server/client. -type Config struct { - DefaultTimeout time.Duration - MaxRequestSize int64 -} - -// DefaultConfig returns Config struct initialized with default settings. -func DefaultConfig() *Config { - return &Config{ - DefaultTimeout: time.Second, - MaxRequestSize: 1024 * 1024, // 1MB - } -} diff --git a/proxy/jsonrpc/errors.go b/proxy/jsonrpc/errors.go deleted file mode 100644 index 91cc299..0000000 --- a/proxy/jsonrpc/errors.go +++ /dev/null @@ -1,37 +0,0 @@ -package jsonrpc - -type jsonRPCError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -const ( - // ErrCodeParse is a reserved JSON-RPC error code - ErrCodeParse = -32700 - // ErrCodeInvalidRequest is a reserved JSON-RPC error code - ErrCodeInvalidRequest = -32600 - // ErrCodeMethodNotFound is a reserved JSON-RPC error code - ErrCodeMethodNotFound = -32601 - // ErrCodeInvalidParams is a reserved JSON-RPC error code - ErrCodeInvalidParams = -32602 - // ErrCodeInternal is a reserved JSON-RPC error code - ErrCodeInternal = -32603 -) - -var ( - - // ErrParse represents a JSON-RPC error indicating a problem with parsing the JSON request payload. - ErrParse = &jsonRPCError{Code: ErrCodeParse, Message: "Parse error"} - - // ErrInvalidRequest represents a JSON-RPC error indicating an invalid JSON request payload. - ErrInvalidRequest = &jsonRPCError{Code: ErrCodeInvalidRequest, Message: "Invalid request"} - - // ErrMethodNotFound represents a JSON-RPC error indicating that the requested method could not be found. - ErrMethodNotFound = &jsonRPCError{Code: ErrCodeMethodNotFound, Message: "Method not found"} - - // ErrInvalidParams represents a JSON-RPC error indicating invalid parameters in the request. - ErrInvalidParams = &jsonRPCError{Code: ErrCodeInvalidParams, Message: "Invalid params"} - - // ErrInternal represents a JSON-RPC error indicating an unspecified internal error within the server. - ErrInternal = &jsonRPCError{Code: ErrCodeInternal, Message: "Internal error"} -) diff --git a/proxy/jsonrpc/proxy_test.go b/proxy/jsonrpc/proxy_test.go deleted file mode 100644 index cd7b245..0000000 --- a/proxy/jsonrpc/proxy_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package jsonrpc_test - -import ( - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - jsonrpcproxy "github.com/rollkit/go-execution/proxy/jsonrpc" - "github.com/rollkit/go-execution/test" -) - -type ProxyTestSuite struct { - test.ExecutorSuite - server *httptest.Server - client *jsonrpcproxy.Client - cleanup func() -} - -func (s *ProxyTestSuite) SetupTest() { - exec := test.NewDummyExecutor() - config := &jsonrpcproxy.Config{ - DefaultTimeout: time.Second, - MaxRequestSize: 1024 * 1024, - } - server := jsonrpcproxy.NewServer(exec, config) - - s.server = httptest.NewServer(server) - - client := jsonrpcproxy.NewClient() - client.SetConfig(config) - - err := client.Start(s.server.URL) - require.NoError(s.T(), err) - - s.client = client - s.Exec = client - s.cleanup = func() { - _ = client.Stop() - s.server.Close() - } -} - -func (s *ProxyTestSuite) TearDownTest() { - s.cleanup() -} - -func TestProxySuite(t *testing.T) { - suite.Run(t, new(ProxyTestSuite)) -} diff --git a/proxy/jsonrpc/server.go b/proxy/jsonrpc/server.go deleted file mode 100644 index b91b24b..0000000 --- a/proxy/jsonrpc/server.go +++ /dev/null @@ -1,198 +0,0 @@ -package jsonrpc - -import ( - "context" - "encoding/base64" - "encoding/json" - "net/http" - "time" - - "github.com/rollkit/go-execution" - "github.com/rollkit/go-execution/types" -) - -// Server defines JSON-RPC proxy server for execution API. -type Server struct { - exec execution.Executor - config *Config -} - -// NewServer initializes and returns a new Server instance with the given execution interface and configuration. -func NewServer(exec execution.Executor, config *Config) *Server { - if config == nil { - config = DefaultConfig() - } - return &Server{ - exec: exec, - config: config, - } -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - writeError(w, ErrInvalidRequest) - return - } - - if r.ContentLength > s.config.MaxRequestSize { - writeError(w, ErrInvalidRequest) - return - } - - var request struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` - ID interface{} `json:"id"` - } - - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - writeError(w, ErrParse) - return - } - - if request.JSONRPC != "2.0" { - writeError(w, ErrInvalidRequest) - return - } - - var result interface{} - var err *jsonRPCError - - switch request.Method { - case "init_chain": - result, err = s.handleInitChain(request.Params) - case "get_txs": - result, err = s.handleGetTxs() - case "execute_txs": - result, err = s.handleExecuteTxs(request.Params) - case "set_final": - result, err = s.handleSetFinal(request.Params) - default: - err = ErrMethodNotFound - } - - if err != nil { - writeResponse(w, request.ID, nil, err) - return - } - - writeResponse(w, request.ID, result, nil) -} - -func (s *Server) handleInitChain(params json.RawMessage) (interface{}, *jsonRPCError) { - var p struct { - GenesisTime int64 `json:"genesis_time"` - InitialHeight uint64 `json:"initial_height"` - ChainID string `json:"chain_id"` - } - - if err := json.Unmarshal(params, &p); err != nil { - return nil, ErrInvalidParams - } - - stateRoot, maxBytes, err := s.exec.InitChain(context.TODO(), time.Unix(p.GenesisTime, 0).UTC(), p.InitialHeight, p.ChainID) - if err != nil { - return nil, &jsonRPCError{Code: ErrCodeInternal, Message: err.Error()} - } - - return map[string]interface{}{ - "state_root": base64.StdEncoding.EncodeToString(stateRoot[:]), - "max_bytes": maxBytes, - }, nil -} - -func (s *Server) handleGetTxs() (interface{}, *jsonRPCError) { - txs, err := s.exec.GetTxs(context.TODO()) - if err != nil { - return nil, &jsonRPCError{Code: ErrCodeInternal, Message: err.Error()} - } - - encodedTxs := make([]string, len(txs)) - for i, tx := range txs { - encodedTxs[i] = base64.StdEncoding.EncodeToString(tx) - } - - return map[string]interface{}{ - "txs": encodedTxs, - }, nil -} - -func (s *Server) handleExecuteTxs(params json.RawMessage) (interface{}, *jsonRPCError) { - var p struct { - Txs []string `json:"txs"` - BlockHeight uint64 `json:"block_height"` - Timestamp int64 `json:"timestamp"` - PrevStateRoot string `json:"prev_state_root"` - } - - if err := json.Unmarshal(params, &p); err != nil { - return nil, ErrInvalidParams - } - - // Decode base64 txs - txs := make([]types.Tx, len(p.Txs)) - for i, encodedTx := range p.Txs { - tx, err := base64.StdEncoding.DecodeString(encodedTx) - if err != nil { - return nil, ErrInvalidParams - } - txs[i] = tx - } - - // Decode base64 prev state root - prevStateRootBytes, err := base64.StdEncoding.DecodeString(p.PrevStateRoot) - if err != nil { - return nil, ErrInvalidParams - } - - var prevStateRoot types.Hash - copy(prevStateRoot[:], prevStateRootBytes) - - updatedStateRoot, maxBytes, err := s.exec.ExecuteTxs(context.TODO(), txs, p.BlockHeight, time.Unix(p.Timestamp, 0).UTC(), prevStateRoot) - if err != nil { - return nil, &jsonRPCError{Code: ErrCodeInternal, Message: err.Error()} - } - - return map[string]interface{}{ - "updated_state_root": base64.StdEncoding.EncodeToString(updatedStateRoot[:]), - "max_bytes": maxBytes, - }, nil -} - -func (s *Server) handleSetFinal(params json.RawMessage) (interface{}, *jsonRPCError) { - var p struct { - BlockHeight uint64 `json:"block_height"` - } - - if err := json.Unmarshal(params, &p); err != nil { - return nil, ErrInvalidParams - } - - if err := s.exec.SetFinal(context.TODO(), p.BlockHeight); err != nil { - return nil, &jsonRPCError{Code: ErrCodeInternal, Message: err.Error()} - } - - return map[string]interface{}{}, nil -} - -func writeError(w http.ResponseWriter, err *jsonRPCError) { - writeResponse(w, nil, nil, err) -} - -func writeResponse(w http.ResponseWriter, id interface{}, result interface{}, err *jsonRPCError) { - response := struct { - JSONRPC string `json:"jsonrpc"` - Result interface{} `json:"result,omitempty"` - Error *jsonRPCError `json:"error,omitempty"` - ID interface{} `json:"id"` - }{ - JSONRPC: "2.0", - Result: result, - Error: err, - ID: id, - } - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) // TODO(tzdybal): add proper error handling -}