Skip to content

Commit

Permalink
feat: support json-rpc proxy client
Browse files Browse the repository at this point in the history
  • Loading branch information
jim380 committed Oct 30, 2024
1 parent adc0ad0 commit 8d69c2e
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 162 deletions.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
## Architecture

```mermaid
graph LR
subgraph Test Environment
TestClient[Test Client]
MockExecutor[Mock Executor]
end
subgraph Execution Client
EngineAPIExecutionClient
subgraph Client Components
EthClient[Eth Client]
ProxyClient[JSON-RPC Proxy Client]
end
end
subgraph Proxy Layer
ProxyServer[JSON-RPC Proxy Server]
end
subgraph Execution Layer
Reth[Reth Node]
subgraph Reth APIs
EngineAPI[Engine API]
JsonRPC[JSON-RPC API]
end
end
%% Test Environment Connections
TestClient -->|uses| EngineAPIExecutionClient
ProxyServer -->|delegates to| MockExecutor
%% Execution Client Connections
EngineAPIExecutionClient -->|eth calls| EthClient
EngineAPIExecutionClient -->|engine calls| ProxyClient
EthClient -->|eth/net/web3| JsonRPC
ProxyClient -->|forwards requests| ProxyServer
%% Proxy to Reth Connections
ProxyServer -->|authenticated requests| EngineAPI
JsonRPC -->|internal| Reth
EngineAPI -->|internal| Reth
%% Styling
classDef primary fill:#f9f,stroke:#333,stroke-width:2px
classDef secondary fill:#bbf,stroke:#333,stroke-width:1px
class EngineAPIExecutionClient,ProxyServer primary
class EthClient,ProxyClient,MockExecutor,EngineAPI,JsonRPC secondary
```

The architecture consists of several key components:

1. **Execution Client**

- `EngineAPIExecutionClient`: Main client interface
- `EthClient`: Handles standard Ethereum JSON-RPC calls
- `ProxyClient`: Handles Engine API calls through the proxy

2. **Proxy Layer**

- `JSON-RPC Proxy Server`: Authenticates and forwards Engine API requests
- Handles JWT authentication with Reth

3. **Execution Layer**

- `Reth Node`: Ethereum execution client
- Exposes Engine API and standard JSON-RPC endpoints

4. **Test Environment**
- `Test Client`: Integration tests
- `Mock Executor`: Simulates execution behavior for unit tests

## Development

```bash
Expand Down
5 changes: 1 addition & 4 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ services:
/bin/sh -c "mkdir -p /jwt &&
if [ ! -f /jwt/jwt.hex ]; then
apk add --no-cache openssl &&
openssl rand -hex 32 | tr -d '\n' > /jwt/jwt.hex &&
echo '✅ JWT token generated successfully';
else
echo '✅ JWT token already exists';
openssl rand -hex 32 | tr -d '\n' > /jwt/jwt.hex;
fi"
reth:
Expand Down
161 changes: 10 additions & 151 deletions execution.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package execution

import (
"context"
"errors"
"fmt"
"math/big"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
execution "github.com/rollkit/go-execution"
"github.com/rollkit/go-execution/proxy/jsonrpc"
rollkitTypes "github.com/rollkit/rollkit/types"
)

Expand All @@ -31,24 +27,21 @@ var (

type EngineAPIExecutionClient struct {
ethClient *ethclient.Client
engineClient *rpc.Client
proxyClient *jsonrpc.Client
genesisHash common.Hash
feeRecipient common.Address
}

// NewEngineAPIExecutionClient creates a new instance of EngineAPIExecutionClient.
func NewEngineAPIExecutionClient(ethURL, engineURL string, genesisHash common.Hash, feeRecipient common.Address) (*EngineAPIExecutionClient, error) {
func NewEngineAPIExecutionClient(ethURL string, proxyClient *jsonrpc.Client, genesisHash common.Hash, feeRecipient common.Address) (*EngineAPIExecutionClient, error) {
ethClient, err := ethclient.Dial(ethURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to Ethereum client: %w", err)
}
engineClient, err := rpc.Dial(engineURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to Engine API: %w", err)
return nil, err
}

return &EngineAPIExecutionClient{
ethClient: ethClient,
engineClient: engineClient,
proxyClient: proxyClient,
genesisHash: genesisHash,
feeRecipient: feeRecipient,
}, nil
Expand All @@ -62,70 +55,12 @@ func (c *EngineAPIExecutionClient) InitChain(
initialHeight uint64,
chainID string,
) (rollkitTypes.Hash, uint64, error) {
ctx := context.Background()
var forkchoiceResult map[string]interface{}
err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV1",
map[string]interface{}{
"headBlockHash": c.genesisHash,
"safeBlockHash": c.genesisHash,
"finalizedBlockHash": c.genesisHash,
},
map[string]interface{}{
"timestamp": genesisTime.Unix(),
"prevRandao": common.Hash{}, // TO-DO
"suggestedFeeRecipient": c.feeRecipient,
},
)
if err != nil {
return rollkitTypes.Hash{}, 0, fmt.Errorf("engine_forkchoiceUpdatedV1 failed: %w", err)
}
payloadID, ok := forkchoiceResult["payloadId"].(string)
if !ok {
return rollkitTypes.Hash{}, 0, ErrNilPayloadStatus
}
var payload map[string]interface{}
err = c.engineClient.CallContext(ctx, &payload, "engine_getPayloadV1", payloadID)
if err != nil {
return rollkitTypes.Hash{}, 0, fmt.Errorf("engine_getPayloadV1 failed: %w", err)
}
stateRoot := common.HexToHash(payload["stateRoot"].(string))
gasLimit := uint64(payload["gasLimit"].(float64))
var rollkitStateRoot rollkitTypes.Hash
copy(rollkitStateRoot[:], stateRoot[:])
return rollkitStateRoot, gasLimit, nil
return c.proxyClient.InitChain(genesisTime, initialHeight, chainID)
}

// GetTxs retrieves transactions from the transaction pool.
func (c *EngineAPIExecutionClient) GetTxs() ([]rollkitTypes.Tx, error) {
ctx := context.Background()
var result struct {
Pending map[string]map[string]*types.Transaction `json:"pending"`
Queued map[string]map[string]*types.Transaction `json:"queued"`
}
err := c.ethClient.Client().CallContext(ctx, &result, "txpool_content")
if err != nil {
return nil, fmt.Errorf("failed to get tx pool content: %w", err)
}
var txs []rollkitTypes.Tx
for _, accountTxs := range result.Pending {
for _, tx := range accountTxs {
txBytes, err := tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to marshal transaction: %w", err)
}
txs = append(txs, rollkitTypes.Tx(txBytes))
}
}
for _, accountTxs := range result.Queued {
for _, tx := range accountTxs {
txBytes, err := tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to marshal transaction: %w", err)
}
txs = append(txs, rollkitTypes.Tx(txBytes))
}
}
return txs, nil
return c.proxyClient.GetTxs()
}

// ExecuteTxs executes the given transactions and returns the new state root and gas used.
Expand All @@ -135,86 +70,10 @@ func (c *EngineAPIExecutionClient) ExecuteTxs(
timestamp time.Time,
prevStateRoot rollkitTypes.Hash,
) (rollkitTypes.Hash, uint64, error) {
ctx := context.Background()
ethTxs := make([][]byte, len(txs))
for i, tx := range txs {
ethTxs[i] = tx
}
prevRandao := c.derivePrevRandao(blockHeight)
var forkchoiceResult map[string]interface{}
err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV1",
map[string]interface{}{
"headBlockHash": common.BytesToHash(prevStateRoot[:]),
"safeBlockHash": common.BytesToHash(prevStateRoot[:]),
"finalizedBlockHash": common.BytesToHash(prevStateRoot[:]),
},
map[string]interface{}{
"timestamp": timestamp.Unix(),
"prevRandao": prevRandao,
"suggestedFeeRecipient": c.feeRecipient,
},
)
if err != nil {
return rollkitTypes.Hash{}, 0, fmt.Errorf("engine_forkchoiceUpdatedV1 failed: %w", err)
}
payloadID, ok := forkchoiceResult["payloadId"].(string)
if !ok {
return rollkitTypes.Hash{}, 0, ErrNilPayloadStatus
}
var payload map[string]interface{}
err = c.engineClient.CallContext(ctx, &payload, "engine_getPayloadV1", payloadID)
if err != nil {
return rollkitTypes.Hash{}, 0, fmt.Errorf("engine_getPayloadV1 failed: %w", err)
}
payload["transactions"] = ethTxs
var newPayloadResult map[string]interface{}
err = c.engineClient.CallContext(ctx, &newPayloadResult, "engine_newPayloadV1", payload)
if err != nil {
return rollkitTypes.Hash{}, 0, fmt.Errorf("engine_newPayloadV1 failed: %w", err)
}
status, ok := newPayloadResult["status"].(string)
if !ok || PayloadStatus(status) != PayloadStatusValid {
return rollkitTypes.Hash{}, 0, ErrInvalidPayloadStatus
}
newStateRoot := common.HexToHash(payload["stateRoot"].(string))
gasUsed := uint64(payload["gasUsed"].(float64))
var rollkitNewStateRoot rollkitTypes.Hash
copy(rollkitNewStateRoot[:], newStateRoot[:])
return rollkitNewStateRoot, gasUsed, nil
return c.proxyClient.ExecuteTxs(txs, blockHeight, timestamp, prevStateRoot)
}

// SetFinal marks a block at the given height as final.
func (c *EngineAPIExecutionClient) SetFinal(blockHeight uint64) error {
ctx := context.Background()
block, err := c.ethClient.BlockByNumber(ctx, big.NewInt(int64(blockHeight)))
if err != nil {
return fmt.Errorf("failed to get block at height %d: %w", blockHeight, err)
}
var result map[string]interface{}
err = c.engineClient.CallContext(ctx, &result, "engine_forkchoiceUpdatedV1",
map[string]interface{}{
"headBlockHash": block.Hash(),
"safeBlockHash": block.Hash(),
"finalizedBlockHash": block.Hash(),
},
nil, // No payload attributes for finalization
)
if err != nil {
return fmt.Errorf("engine_forkchoiceUpdatedV1 failed for finalization: %w", err)
}
payloadStatus, ok := result["payloadStatus"].(map[string]interface{})
if !ok {
return ErrNilPayloadStatus
}
status, ok := payloadStatus["status"].(string)
if !ok || PayloadStatus(status) != PayloadStatusValid {
return ErrInvalidPayloadStatus
}
return nil
}

// derivePrevRandao generates a deterministic prevRandao value based on block height.
func (c *EngineAPIExecutionClient) derivePrevRandao(blockHeight uint64) common.Hash {
// TO-DO
return common.BigToHash(big.NewInt(int64(blockHeight)))
return c.proxyClient.SetFinal(blockHeight)
}
Loading

0 comments on commit 8d69c2e

Please sign in to comment.