engine_client.go 6.78 KB
Newer Older
1 2 3 4 5 6 7
package sources

import (
	"context"
	"fmt"
	"time"

8
	"github.com/ethereum/go-ethereum/common"
9
	"github.com/ethereum/go-ethereum/eth/catalyst"
10
	"github.com/ethereum/go-ethereum/log"
11
	"github.com/ethereum/go-ethereum/params"
12
	"github.com/ethereum/go-ethereum/rpc"
13

14
	"github.com/ethereum-optimism/optimism/op-node/rollup"
Sabnock01's avatar
Sabnock01 committed
15
	"github.com/ethereum-optimism/optimism/op-service/client"
16
	"github.com/ethereum-optimism/optimism/op-service/eth"
Sabnock01's avatar
Sabnock01 committed
17
	"github.com/ethereum-optimism/optimism/op-service/sources/caching"
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
)

type EngineClientConfig struct {
	L2ClientConfig
}

func EngineClientDefaultConfig(config *rollup.Config) *EngineClientConfig {
	return &EngineClientConfig{
		// engine is trusted, no need to recompute responses etc.
		L2ClientConfig: *L2ClientDefaultConfig(config, true),
	}
}

// EngineClient extends L2Client with engine API bindings.
type EngineClient struct {
	*L2Client
34
	*EngineAPIClient
35 36 37 38 39 40 41 42
}

func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EngineClientConfig) (*EngineClient, error) {
	l2Client, err := NewL2Client(client, log, metrics, &config.L2ClientConfig)
	if err != nil {
		return nil, err
	}

43 44
	engineAPIClient := NewEngineAPIClient(client, log, config.RollupCfg)

45
	return &EngineClient{
46 47
		L2Client:        l2Client,
		EngineAPIClient: engineAPIClient,
48 49 50
	}, nil
}

51 52
// EngineAPIClient is an RPC client for the Engine API functions.
type EngineAPIClient struct {
53 54 55 56
	RPC     client.RPC
	log     log.Logger
	evp     EngineVersionProvider
	timeout time.Duration
57 58 59 60 61 62 63 64 65 66
}

type EngineVersionProvider interface {
	ForkchoiceUpdatedVersion(attr *eth.PayloadAttributes) eth.EngineAPIMethod
	NewPayloadVersion(timestamp uint64) eth.EngineAPIMethod
	GetPayloadVersion(timestamp uint64) eth.EngineAPIMethod
}

func NewEngineAPIClient(rpc client.RPC, l log.Logger, evp EngineVersionProvider) *EngineAPIClient {
	return &EngineAPIClient{
67 68 69 70 71 72 73 74 75 76 77 78 79
		RPC:     rpc,
		log:     l,
		evp:     evp,
		timeout: time.Second * 5,
	}
}

func NewEngineAPIClientWithTimeout(rpc client.RPC, l log.Logger, evp EngineVersionProvider, timeout time.Duration) *EngineAPIClient {
	return &EngineAPIClient{
		RPC:     rpc,
		log:     l,
		evp:     evp,
		timeout: timeout,
80 81 82 83 84 85 86
	}
}

// EngineVersionProvider returns the underlying engine version provider used for
// resolving the correct Engine API versions.
func (s *EngineAPIClient) EngineVersionProvider() EngineVersionProvider { return s.evp }

87 88 89
// ForkchoiceUpdate updates the forkchoice on the execution client. If attributes is not nil, the engine client will also begin building a block
// based on attributes after the new head block and return the payload ID.
//
90 91 92 93
// The RPC may return three types of errors:
// 1. Processing error: ForkchoiceUpdatedResult.PayloadStatusV1.ValidationError or other non-success PayloadStatusV1,
// 2. `error` as eth.InputError: the forkchoice state or attributes are not valid.
// 3. Other types of `error`: temporary RPC errors, like timeouts.
94
func (s *EngineAPIClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceState, attributes *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
95 96 97
	llog := s.log.New("state", fc)       // local logger
	tlog := llog.New("attr", attributes) // trace logger
	tlog.Trace("Sharing forkchoice-updated signal")
98
	fcCtx, cancel := context.WithTimeout(ctx, s.timeout)
99 100
	defer cancel()
	var result eth.ForkchoiceUpdatedResult
101 102
	method := s.evp.ForkchoiceUpdatedVersion(attributes)
	err := s.RPC.CallContext(fcCtx, &result, string(method), fc, attributes)
103
	if err == nil {
104
		tlog.Trace("Shared forkchoice-updated signal")
105
		if attributes != nil { // block building is optional, we only get a payload ID if we are building a block
106
			tlog.Trace("Received payload id", "payloadId", result.PayloadID)
107 108 109
		}
		return &result, nil
	} else {
110
		llog.Warn("Failed to share forkchoice-updated signal", "err", err)
111 112
		if rpcErr, ok := err.(rpc.Error); ok {
			code := eth.ErrorCode(rpcErr.ErrorCode())
113
			switch code {
114
			case eth.InvalidParams, eth.InvalidForkchoiceState, eth.InvalidPayloadAttributes:
115 116 117 118 119 120 121
				return nil, eth.InputError{
					Inner: err,
					Code:  code,
				}
			default:
				return nil, fmt.Errorf("unrecognized rpc error: %w", err)
			}
122 123 124 125 126 127 128 129
		}
		return nil, err
	}
}

// NewPayload executes a full block on the execution engine.
// This returns a PayloadStatusV1 which encodes any validation/processing error,
// and this type of error is kept separate from the returned `error` used for RPC errors, like timeouts.
130
func (s *EngineAPIClient) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) {
131 132 133
	e := s.log.New("block_hash", payload.BlockHash)
	e.Trace("sending payload for execution")

134
	execCtx, cancel := context.WithTimeout(ctx, s.timeout)
135 136
	defer cancel()
	var result eth.PayloadStatusV1
137 138

	var err error
139
	switch method := s.evp.NewPayloadVersion(uint64(payload.Timestamp)); method {
140
	case eth.NewPayloadV3:
141
		err = s.RPC.CallContext(execCtx, &result, string(method), payload, []common.Hash{}, parentBeaconBlockRoot)
142
	case eth.NewPayloadV2:
143
		err = s.RPC.CallContext(execCtx, &result, string(method), payload)
144 145
	default:
		return nil, fmt.Errorf("unsupported NewPayload version: %s", method)
146 147
	}

148 149 150 151 152 153 154 155
	e.Trace("Received payload execution result", "status", result.Status, "latestValidHash", result.LatestValidHash, "message", result.ValidationError)
	if err != nil {
		e.Error("Payload execution failed", "err", err)
		return nil, fmt.Errorf("failed to execute payload: %w", err)
	}
	return &result, nil
}

156 157 158 159
// GetPayload gets the execution payload associated with the PayloadId.
// There may be two types of error:
// 1. `error` as eth.InputError: the payload ID may be unknown
// 2. Other types of `error`: temporary RPC errors, like timeouts.
160
func (s *EngineAPIClient) GetPayload(ctx context.Context, payloadInfo eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error) {
161
	e := s.log.New("payload_id", payloadInfo.ID)
162
	e.Trace("getting payload")
163
	var result eth.ExecutionPayloadEnvelope
164 165
	method := s.evp.GetPayloadVersion(payloadInfo.Timestamp)
	err := s.RPC.CallContext(ctx, &result, string(method), payloadInfo.ID)
166
	if err != nil {
167
		e.Warn("Failed to get payload", "payload_id", payloadInfo.ID, "err", err)
168 169
		if rpcErr, ok := err.(rpc.Error); ok {
			code := eth.ErrorCode(rpcErr.ErrorCode())
170 171 172 173 174 175 176 177
			switch code {
			case eth.UnknownPayload:
				return nil, eth.InputError{
					Inner: err,
					Code:  code,
				}
			default:
				return nil, fmt.Errorf("unrecognized rpc error: %w", err)
178 179 180 181 182
			}
		}
		return nil, err
	}
	e.Trace("Received payload")
183
	return &result, nil
184
}
185

186
func (s *EngineAPIClient) SignalSuperchainV1(ctx context.Context, recommended, required params.ProtocolVersion) (params.ProtocolVersion, error) {
187
	var result params.ProtocolVersion
188
	err := s.RPC.CallContext(ctx, &result, "engine_signalSuperchainV1", &catalyst.SuperchainSignal{
189 190 191 192 193
		Recommended: recommended,
		Required:    required,
	})
	return result, err
}