Skip to content
This repository was archived by the owner on May 30, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
324 changes: 324 additions & 0 deletions execution/op/execution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
package op

import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"math/big"
"net/http"
"strings"
"time"

"github.com/ethereum/go-ethereum/beacon/engine"
"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"
"github.com/golang-jwt/jwt/v5"

"github.com/rollkit/rollkit/core/execution"
)

var (
// ErrNilPayloadStatus indicates that PayloadID returned by EVM was nil
ErrNilPayloadStatus = errors.New("nil payload status")
// ErrInvalidPayloadStatus indicates that EVM returned status != VALID
ErrInvalidPayloadStatus = errors.New("invalid payload status")
)

// Ensure EngineAPIExecutionClient implements the execution.Execute interface
var _ execution.Executor = (*EngineClient)(nil)

// EngineClient represents a client that interacts with an Ethereum execution engine
// through the Engine API. It manages connections to both the engine and standard Ethereum
// APIs, and maintains state related to block processing.
type EngineClient struct {
engineClient *rpc.Client // Client for Engine API calls
ethClient *ethclient.Client // Client for standard Ethereum API calls
genesisHash common.Hash // Hash of the genesis block
initialHeight uint64
feeRecipient common.Address // Address to receive transaction fees
}

// NewPureEngineExecutionClient creates a new instance of EngineAPIExecutionClient
func NewEngineExecutionClient(
ethURL,
engineURL string,
jwtSecret string,
genesisHash common.Hash,
feeRecipient common.Address,
) (*EngineClient, error) {
ethClient, err := ethclient.Dial(ethURL)
if err != nil {
return nil, err
}

secret, err := decodeSecret(jwtSecret)
if err != nil {
return nil, err
}

engineClient, err := rpc.DialOptions(context.Background(), engineURL,
rpc.WithHTTPAuth(func(h http.Header) error {
authToken, err := getAuthToken(secret)
if err != nil {
return err
}

if authToken != "" {
h.Set("Authorization", "Bearer "+authToken)
}
return nil
}))
if err != nil {
return nil, err
}

return &EngineClient{
engineClient: engineClient,
ethClient: ethClient,
genesisHash: genesisHash,
feeRecipient: feeRecipient,
}, nil
}

// InitChain initializes the blockchain with the given genesis parameters
func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) {
if initialHeight != 1 {
return nil, 0, fmt.Errorf("initialHeight must be 1, got %d", initialHeight)
}

// Acknowledge the genesis block
var forkchoiceResult engine.ForkChoiceResponse
err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3",
engine.ForkchoiceStateV1{
HeadBlockHash: c.genesisHash,
SafeBlockHash: c.genesisHash,
FinalizedBlockHash: c.genesisHash,
},
nil,
)
if err != nil {
return nil, 0, fmt.Errorf("engine_forkchoiceUpdatedV3 failed: %w", err)
}

_, stateRoot, gasLimit, _, err := c.getBlockInfo(ctx, 0)
if err != nil {
return nil, 0, fmt.Errorf("failed to get block info: %w", err)
}

c.initialHeight = initialHeight

return stateRoot[:], gasLimit, nil
}

// GetTxs retrieves transactions from the current execution payload
func (c *EngineClient) GetTxs(ctx context.Context) ([][]byte, error) {
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 [][]byte

// add pending txs
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, txBytes)
}
}

// add queued txs
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, txBytes)
}
}
return txs, nil
}

// ExecuteTxs executes the given transactions at the specified block height and timestamp
func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, maxBytes uint64, err error) {
// convert rollkit tx to eth tx
ethTxs := make([]*types.Transaction, len(txs))
for i, tx := range txs {
ethTxs[i] = new(types.Transaction)
err := ethTxs[i].UnmarshalBinary(tx)
if err != nil {
return nil, 0, fmt.Errorf("failed to unmarshal transaction: %w", err)
}
}

// encode
txsPayload := make([][]byte, len(txs))
for i, tx := range ethTxs {
buf := bytes.Buffer{}
err := tx.EncodeRLP(&buf)
if err != nil {
return nil, 0, fmt.Errorf("failed to RLP encode tx: %w", err)
}
txsPayload[i] = buf.Bytes()
}

Comment on lines +155 to +176
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential integer-overflow & clarity for timestamp conversion

timestamp.Unix() returns int64; casting directly to uint64 triggers gosec G115 because a negative value would wrap.
While real block timestamps are non-negative, adding an explicit guard silences the warning and documents intent.

-ts := uint64(timestamp.Unix())
+rawTs := timestamp.Unix()
+if rawTs < 0 {
+	return nil, 0, fmt.Errorf("negative timestamp is invalid: %d", rawTs)
+}
+ts := uint64(rawTs)

Committable suggestion skipped: line range outside the PR's diff.

var (
prevBlockHash common.Hash
prevTimestamp uint64
)

// fetch previous block hash to update forkchoice for the next payload id
// if blockHeight == c.initialHeight {
// prevBlockHash = c.genesisHash
// } else {
prevBlockHash, _, _, prevTimestamp, err = c.getBlockInfo(ctx, blockHeight-1)
if err != nil {
return nil, 0, err
}
// }

// make sure that the timestamp is increasing
ts := uint64(timestamp.Unix())
if ts <= prevTimestamp {
ts = prevTimestamp + 1 // Subsequent blocks must have a higher timestamp.
}

// update forkchoice to get the next payload id
var forkchoiceResult engine.ForkChoiceResponse
err = c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3",
engine.ForkchoiceStateV1{
HeadBlockHash: prevBlockHash,
SafeBlockHash: prevBlockHash,
FinalizedBlockHash: prevBlockHash,
},
&engine.PayloadAttributes{
Timestamp: ts,
Random: prevBlockHash, //c.derivePrevRandao(height),
SuggestedFeeRecipient: c.feeRecipient,
Withdrawals: []*types.Withdrawal{},
BeaconRoot: &c.genesisHash,
Transactions: txsPayload, // force to use txsPayload
NoTxPool: true,
},
Comment on lines +206 to +214
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use derivePrevRandao and avoid dead code

derivePrevRandao is currently unused, triggering golangci-lint.
You already compute prevBlockHash but reuse it as Random. Either delete the helper or use it here:

-			Random:                prevBlockHash, //c.derivePrevRandao(height),
+			Random:                c.derivePrevRandao(blockHeight),

This keeps the helper relevant and documents how the RANDAO value is derived.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
&engine.PayloadAttributes{
Timestamp: ts,
Random: prevBlockHash, //c.derivePrevRandao(height),
SuggestedFeeRecipient: c.feeRecipient,
Withdrawals: []*types.Withdrawal{},
BeaconRoot: &c.genesisHash,
Transactions: txsPayload, // force to use txsPayload
NoTxPool: true,
},
&engine.PayloadAttributes{
Timestamp: ts,
- Random: prevBlockHash, //c.derivePrevRandao(height),
+ Random: c.derivePrevRandao(blockHeight),
SuggestedFeeRecipient: c.feeRecipient,
Withdrawals: []*types.Withdrawal{},
BeaconRoot: &c.genesisHash,
Transactions: txsPayload, // force to use txsPayload
NoTxPool: true,
},

)
if err != nil {
return nil, 0, fmt.Errorf("forkchoice update failed: %w", err)
}

if forkchoiceResult.PayloadID == nil {
return nil, 0, ErrNilPayloadStatus
}

// get payload
var payloadResult engine.ExecutionPayloadEnvelope
err = c.engineClient.CallContext(ctx, &payloadResult, "engine_getPayloadV3", *forkchoiceResult.PayloadID)
if err != nil {
return nil, 0, fmt.Errorf("get payload failed: %w", err)
}

// submit payload
var newPayloadResult engine.PayloadStatusV1
err = c.engineClient.CallContext(ctx, &newPayloadResult, "engine_newPayloadV3",
payloadResult.ExecutionPayload,
[]string{}, // No blob hashes
c.genesisHash.Hex(),
)
if err != nil {
return nil, 0, fmt.Errorf("new payload submission failed: %w", err)
}

if newPayloadResult.Status != engine.VALID {
return nil, 0, ErrInvalidPayloadStatus
}

// forkchoice update
blockHash := payloadResult.ExecutionPayload.BlockHash
err = c.setFinal(ctx, blockHash, false)
if err != nil {
return nil, 0, err
}

return payloadResult.ExecutionPayload.StateRoot.Bytes(), payloadResult.ExecutionPayload.GasUsed, nil
}
Comment on lines +247 to +254
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Return GasLimit, not GasUsed, for maxBytes

The sibling “pure” client returns ExecutionPayload.GasLimit.
Returning GasUsed here changes the meaning of maxBytes and may break anything consuming the execution.Executor contract.

-return payloadResult.ExecutionPayload.StateRoot.Bytes(), payloadResult.ExecutionPayload.GasUsed, nil
+return payloadResult.ExecutionPayload.StateRoot.Bytes(), payloadResult.ExecutionPayload.GasLimit, nil


func (c *EngineClient) setFinal(ctx context.Context, blockHash common.Hash, isFinal bool) error {
args := engine.ForkchoiceStateV1{
HeadBlockHash: blockHash,
SafeBlockHash: blockHash,
}
if isFinal {
args.FinalizedBlockHash = blockHash
}

var forkchoiceResult engine.ForkChoiceResponse
err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3",
args,
nil,
)
if err != nil {
return fmt.Errorf("forkchoice update failed with error: %w", err)
}

if forkchoiceResult.PayloadStatus.Status != engine.VALID {
return ErrInvalidPayloadStatus
}

return nil
}

// SetFinal marks the block at the given height as finalized
func (c *EngineClient) SetFinal(ctx context.Context, blockHeight uint64) error {
blockHash, _, _, _, err := c.getBlockInfo(ctx, blockHeight)
if err != nil {
return fmt.Errorf("failed to get block info: %w", err)
}
return c.setFinal(ctx, blockHash, true)
}

func (c *EngineClient) derivePrevRandao(blockHeight uint64) common.Hash {
return common.BigToHash(new(big.Int).SetUint64(blockHeight))
}

func (c *EngineClient) getBlockInfo(ctx context.Context, height uint64) (common.Hash, common.Hash, uint64, uint64, error) {
header, err := c.ethClient.HeaderByNumber(ctx, new(big.Int).SetUint64(height))

if err != nil {
return common.Hash{}, common.Hash{}, 0, 0, fmt.Errorf("failed to get block at height %d: %w", height, err)
}

return header.Hash(), header.Root, header.GasLimit, header.Time, nil
}

func decodeSecret(jwtSecret string) ([]byte, error) {
secret, err := hex.DecodeString(strings.TrimPrefix(jwtSecret, "0x"))
if err != nil {
return nil, fmt.Errorf("failed to decode JWT secret: %w", err)
}
return secret, nil
}

func getAuthToken(jwtSecret []byte) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": time.Now().Add(time.Hour * 1).Unix(), // Expires in 1 hour
"iat": time.Now().Unix(),
})

// Sign the token with the decoded secret
authToken, err := token.SignedString(jwtSecret)
if err != nil {
return "", fmt.Errorf("failed to sign JWT token: %w", err)
}
return authToken, nil
}
Loading
Loading