github.com/ethereum/go-ethereum@v1.16.1/eth/catalyst/witness.go (about)

     1  // Copyright 2025 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package catalyst
    18  
    19  import (
    20  	"errors"
    21  	"strconv"
    22  	"time"
    23  
    24  	"github.com/ethereum/go-ethereum/beacon/engine"
    25  	"github.com/ethereum/go-ethereum/common"
    26  	"github.com/ethereum/go-ethereum/common/hexutil"
    27  	"github.com/ethereum/go-ethereum/core"
    28  	"github.com/ethereum/go-ethereum/core/stateless"
    29  	"github.com/ethereum/go-ethereum/core/vm"
    30  	"github.com/ethereum/go-ethereum/log"
    31  	"github.com/ethereum/go-ethereum/params/forks"
    32  	"github.com/ethereum/go-ethereum/rlp"
    33  )
    34  
    35  // ForkchoiceUpdatedWithWitnessV1 is analogous to ForkchoiceUpdatedV1, only it
    36  // generates an execution witness too if block building was requested.
    37  func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV1(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) {
    38  	if payloadAttributes != nil {
    39  		switch {
    40  		case payloadAttributes.Withdrawals != nil || payloadAttributes.BeaconRoot != nil:
    41  			return engine.STATUS_INVALID, paramsErr("withdrawals and beacon root not supported in V1")
    42  		case !api.checkFork(payloadAttributes.Timestamp, forks.Paris, forks.Shanghai):
    43  			return engine.STATUS_INVALID, paramsErr("fcuV1 called post-shanghai")
    44  		}
    45  	}
    46  	return api.forkchoiceUpdated(update, payloadAttributes, engine.PayloadV1, true)
    47  }
    48  
    49  // ForkchoiceUpdatedWithWitnessV2 is analogous to ForkchoiceUpdatedV2, only it
    50  // generates an execution witness too if block building was requested.
    51  func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV2(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) {
    52  	if params != nil {
    53  		switch {
    54  		case params.BeaconRoot != nil:
    55  			return engine.STATUS_INVALID, attributesErr("unexpected beacon root")
    56  		case api.checkFork(params.Timestamp, forks.Paris) && params.Withdrawals != nil:
    57  			return engine.STATUS_INVALID, attributesErr("withdrawals before shanghai")
    58  		case api.checkFork(params.Timestamp, forks.Shanghai) && params.Withdrawals == nil:
    59  			return engine.STATUS_INVALID, attributesErr("missing withdrawals")
    60  		case !api.checkFork(params.Timestamp, forks.Paris, forks.Shanghai):
    61  			return engine.STATUS_INVALID, unsupportedForkErr("fcuV2 must only be called with paris or shanghai payloads")
    62  		}
    63  	}
    64  	return api.forkchoiceUpdated(update, params, engine.PayloadV2, true)
    65  }
    66  
    67  // ForkchoiceUpdatedWithWitnessV3 is analogous to ForkchoiceUpdatedV3, only it
    68  // generates an execution witness too if block building was requested.
    69  func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) {
    70  	if params != nil {
    71  		switch {
    72  		case params.Withdrawals == nil:
    73  			return engine.STATUS_INVALID, attributesErr("missing withdrawals")
    74  		case params.BeaconRoot == nil:
    75  			return engine.STATUS_INVALID, attributesErr("missing beacon root")
    76  		case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague):
    77  			return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun or prague payloads")
    78  		}
    79  	}
    80  	// TODO(matt): the spec requires that fcu is applied when called on a valid
    81  	// hash, even if params are wrong. To do this we need to split up
    82  	// forkchoiceUpdate into a function that only updates the head and then a
    83  	// function that kicks off block construction.
    84  	return api.forkchoiceUpdated(update, params, engine.PayloadV3, true)
    85  }
    86  
    87  // NewPayloadWithWitnessV1 is analogous to NewPayloadV1, only it also generates
    88  // and returns a stateless witness after running the payload.
    89  func (api *ConsensusAPI) NewPayloadWithWitnessV1(params engine.ExecutableData) (engine.PayloadStatusV1, error) {
    90  	if params.Withdrawals != nil {
    91  		return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1"))
    92  	}
    93  	return api.newPayload(params, nil, nil, nil, true)
    94  }
    95  
    96  // NewPayloadWithWitnessV2 is analogous to NewPayloadV2, only it also generates
    97  // and returns a stateless witness after running the payload.
    98  func (api *ConsensusAPI) NewPayloadWithWitnessV2(params engine.ExecutableData) (engine.PayloadStatusV1, error) {
    99  	var (
   100  		cancun   = api.config().IsCancun(api.config().LondonBlock, params.Timestamp)
   101  		shanghai = api.config().IsShanghai(api.config().LondonBlock, params.Timestamp)
   102  	)
   103  	switch {
   104  	case cancun:
   105  		return invalidStatus, paramsErr("can't use newPayloadV2 post-cancun")
   106  	case shanghai && params.Withdrawals == nil:
   107  		return invalidStatus, paramsErr("nil withdrawals post-shanghai")
   108  	case !shanghai && params.Withdrawals != nil:
   109  		return invalidStatus, paramsErr("non-nil withdrawals pre-shanghai")
   110  	case params.ExcessBlobGas != nil:
   111  		return invalidStatus, paramsErr("non-nil excessBlobGas pre-cancun")
   112  	case params.BlobGasUsed != nil:
   113  		return invalidStatus, paramsErr("non-nil blobGasUsed pre-cancun")
   114  	}
   115  	return api.newPayload(params, nil, nil, nil, true)
   116  }
   117  
   118  // NewPayloadWithWitnessV3 is analogous to NewPayloadV3, only it also generates
   119  // and returns a stateless witness after running the payload.
   120  func (api *ConsensusAPI) NewPayloadWithWitnessV3(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) {
   121  	switch {
   122  	case params.Withdrawals == nil:
   123  		return invalidStatus, paramsErr("nil withdrawals post-shanghai")
   124  	case params.ExcessBlobGas == nil:
   125  		return invalidStatus, paramsErr("nil excessBlobGas post-cancun")
   126  	case params.BlobGasUsed == nil:
   127  		return invalidStatus, paramsErr("nil blobGasUsed post-cancun")
   128  	case versionedHashes == nil:
   129  		return invalidStatus, paramsErr("nil versionedHashes post-cancun")
   130  	case beaconRoot == nil:
   131  		return invalidStatus, paramsErr("nil beaconRoot post-cancun")
   132  	case !api.checkFork(params.Timestamp, forks.Cancun):
   133  		return invalidStatus, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads")
   134  	}
   135  	return api.newPayload(params, versionedHashes, beaconRoot, nil, true)
   136  }
   137  
   138  // NewPayloadWithWitnessV4 is analogous to NewPayloadV4, only it also generates
   139  // and returns a stateless witness after running the payload.
   140  func (api *ConsensusAPI) NewPayloadWithWitnessV4(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes) (engine.PayloadStatusV1, error) {
   141  	switch {
   142  	case params.Withdrawals == nil:
   143  		return invalidStatus, paramsErr("nil withdrawals post-shanghai")
   144  	case params.ExcessBlobGas == nil:
   145  		return invalidStatus, paramsErr("nil excessBlobGas post-cancun")
   146  	case params.BlobGasUsed == nil:
   147  		return invalidStatus, paramsErr("nil blobGasUsed post-cancun")
   148  	case versionedHashes == nil:
   149  		return invalidStatus, paramsErr("nil versionedHashes post-cancun")
   150  	case beaconRoot == nil:
   151  		return invalidStatus, paramsErr("nil beaconRoot post-cancun")
   152  	case executionRequests == nil:
   153  		return invalidStatus, paramsErr("nil executionRequests post-prague")
   154  	case !api.checkFork(params.Timestamp, forks.Prague):
   155  		return invalidStatus, unsupportedForkErr("newPayloadV4 must only be called for prague payloads")
   156  	}
   157  	requests := convertRequests(executionRequests)
   158  	if err := validateRequests(requests); err != nil {
   159  		return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err)
   160  	}
   161  	return api.newPayload(params, versionedHashes, beaconRoot, requests, true)
   162  }
   163  
   164  // ExecuteStatelessPayloadV1 is analogous to NewPayloadV1, only it operates in
   165  // a stateless mode on top of a provided witness instead of the local database.
   166  func (api *ConsensusAPI) ExecuteStatelessPayloadV1(params engine.ExecutableData, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) {
   167  	if params.Withdrawals != nil {
   168  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1"))
   169  	}
   170  	return api.executeStatelessPayload(params, nil, nil, nil, opaqueWitness)
   171  }
   172  
   173  // ExecuteStatelessPayloadV2 is analogous to NewPayloadV2, only it operates in
   174  // a stateless mode on top of a provided witness instead of the local database.
   175  func (api *ConsensusAPI) ExecuteStatelessPayloadV2(params engine.ExecutableData, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) {
   176  	var (
   177  		cancun   = api.config().IsCancun(api.config().LondonBlock, params.Timestamp)
   178  		shanghai = api.config().IsShanghai(api.config().LondonBlock, params.Timestamp)
   179  	)
   180  	switch {
   181  	case cancun:
   182  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("can't use newPayloadV2 post-cancun")
   183  	case shanghai && params.Withdrawals == nil:
   184  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil withdrawals post-shanghai")
   185  	case !shanghai && params.Withdrawals != nil:
   186  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("non-nil withdrawals pre-shanghai")
   187  	case params.ExcessBlobGas != nil:
   188  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("non-nil excessBlobGas pre-cancun")
   189  	case params.BlobGasUsed != nil:
   190  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("non-nil blobGasUsed pre-cancun")
   191  	}
   192  	return api.executeStatelessPayload(params, nil, nil, nil, opaqueWitness)
   193  }
   194  
   195  // ExecuteStatelessPayloadV3 is analogous to NewPayloadV3, only it operates in
   196  // a stateless mode on top of a provided witness instead of the local database.
   197  func (api *ConsensusAPI) ExecuteStatelessPayloadV3(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) {
   198  	switch {
   199  	case params.Withdrawals == nil:
   200  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil withdrawals post-shanghai")
   201  	case params.ExcessBlobGas == nil:
   202  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil excessBlobGas post-cancun")
   203  	case params.BlobGasUsed == nil:
   204  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil blobGasUsed post-cancun")
   205  	case versionedHashes == nil:
   206  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil versionedHashes post-cancun")
   207  	case beaconRoot == nil:
   208  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun")
   209  	case !api.checkFork(params.Timestamp, forks.Cancun):
   210  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads")
   211  	}
   212  	return api.executeStatelessPayload(params, versionedHashes, beaconRoot, nil, opaqueWitness)
   213  }
   214  
   215  // ExecuteStatelessPayloadV4 is analogous to NewPayloadV4, only it operates in
   216  // a stateless mode on top of a provided witness instead of the local database.
   217  func (api *ConsensusAPI) ExecuteStatelessPayloadV4(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) {
   218  	switch {
   219  	case params.Withdrawals == nil:
   220  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil withdrawals post-shanghai")
   221  	case params.ExcessBlobGas == nil:
   222  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil excessBlobGas post-cancun")
   223  	case params.BlobGasUsed == nil:
   224  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil blobGasUsed post-cancun")
   225  	case versionedHashes == nil:
   226  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil versionedHashes post-cancun")
   227  	case beaconRoot == nil:
   228  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun")
   229  	case executionRequests == nil:
   230  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil executionRequests post-prague")
   231  	case !api.checkFork(params.Timestamp, forks.Prague):
   232  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("newPayloadV3 must only be called for cancun payloads")
   233  	}
   234  	requests := convertRequests(executionRequests)
   235  	if err := validateRequests(requests); err != nil {
   236  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err)
   237  	}
   238  	return api.executeStatelessPayload(params, versionedHashes, beaconRoot, requests, opaqueWitness)
   239  }
   240  
   241  func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) {
   242  	log.Trace("Engine API request received", "method", "ExecuteStatelessPayload", "number", params.Number, "hash", params.BlockHash)
   243  	block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, requests)
   244  	if err != nil {
   245  		bgu := "nil"
   246  		if params.BlobGasUsed != nil {
   247  			bgu = strconv.Itoa(int(*params.BlobGasUsed))
   248  		}
   249  		ebg := "nil"
   250  		if params.ExcessBlobGas != nil {
   251  			ebg = strconv.Itoa(int(*params.ExcessBlobGas))
   252  		}
   253  		log.Warn("Invalid ExecuteStatelessPayload params",
   254  			"params.Number", params.Number,
   255  			"params.ParentHash", params.ParentHash,
   256  			"params.BlockHash", params.BlockHash,
   257  			"params.StateRoot", params.StateRoot,
   258  			"params.FeeRecipient", params.FeeRecipient,
   259  			"params.LogsBloom", common.PrettyBytes(params.LogsBloom),
   260  			"params.Random", params.Random,
   261  			"params.GasLimit", params.GasLimit,
   262  			"params.GasUsed", params.GasUsed,
   263  			"params.Timestamp", params.Timestamp,
   264  			"params.ExtraData", common.PrettyBytes(params.ExtraData),
   265  			"params.BaseFeePerGas", params.BaseFeePerGas,
   266  			"params.BlobGasUsed", bgu,
   267  			"params.ExcessBlobGas", ebg,
   268  			"len(params.Transactions)", len(params.Transactions),
   269  			"len(params.Withdrawals)", len(params.Withdrawals),
   270  			"beaconRoot", beaconRoot,
   271  			"len(requests)", len(requests),
   272  			"error", err)
   273  		errorMsg := err.Error()
   274  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID, ValidationError: &errorMsg}, nil
   275  	}
   276  	witness := new(stateless.Witness)
   277  	if err := rlp.DecodeBytes(opaqueWitness, witness); err != nil {
   278  		log.Warn("Invalid ExecuteStatelessPayload witness", "err", err)
   279  		errorMsg := err.Error()
   280  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID, ValidationError: &errorMsg}, nil
   281  	}
   282  	// Stash away the last update to warn the user if the beacon client goes offline
   283  	api.lastNewPayloadUpdate.Store(time.Now().Unix())
   284  
   285  	log.Trace("Executing block statelessly", "number", block.Number(), "hash", params.BlockHash)
   286  	stateRoot, receiptRoot, err := core.ExecuteStateless(api.config(), vm.Config{}, block, witness)
   287  	if err != nil {
   288  		log.Warn("ExecuteStatelessPayload: execution failed", "err", err)
   289  		errorMsg := err.Error()
   290  		return engine.StatelessPayloadStatusV1{Status: engine.INVALID, ValidationError: &errorMsg}, nil
   291  	}
   292  	return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: stateRoot, ReceiptsRoot: receiptRoot}, nil
   293  }