diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 9dd0f7cdf..f16dced85 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -45,6 +45,7 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/validation" "github.com/naoina/toml" "github.com/urfave/cli/v2" ) @@ -93,11 +94,12 @@ type ethstatsConfig struct { } type gethConfig struct { - Eth ethconfig.Config - Node node.Config - Ethstats ethstatsConfig - Metrics metrics.Config - Builder builder.Config + Eth ethconfig.Config + Node node.Config + Ethstats ethstatsConfig + Metrics metrics.Config + Builder builder.Config + Validation validation.Config } func loadConfig(file string, cfg *gethConfig) error { @@ -131,10 +133,11 @@ func defaultNodeConfig() node.Config { func loadBaseConfig(ctx *cli.Context) gethConfig { // Load defaults. cfg := gethConfig{ - Eth: ethconfig.Defaults, - Node: defaultNodeConfig(), - Metrics: metrics.DefaultConfig, - Builder: builder.DefaultConfig, + Eth: ethconfig.Defaults, + Node: defaultNodeConfig(), + Metrics: metrics.DefaultConfig, + Builder: builder.DefaultConfig, + Validation: validation.DefaultConfig, } // Load config file. @@ -170,6 +173,9 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { // Apply builder flags utils.SetBuilderConfig(ctx, &cfg.Builder) + // Apply validation flags + utils.SetValidationConfig(ctx, &cfg.Validation) + return stack, cfg } @@ -218,6 +224,10 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { utils.Fatalf("Failed to register the Block Validation API: %v", err) } + if err := validation.Register(stack, eth, &cfg.Validation); err != nil { + utils.Fatalf("Failed to register the Validation API: %v", err) + } + // Configure GraphQL if requested if ctx.IsSet(utils.GraphQLEnabledFlag.Name) { utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d7d63db78..602a46ea1 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -74,6 +74,7 @@ import ( "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/hashdb" "github.com/ethereum/go-ethereum/triedb/pathdb" + "github.com/ethereum/go-ethereum/validation" pcsclite "github.com/gballet/go-libpcsclite" gopsutil "github.com/shirou/gopsutil/mem" "github.com/urfave/cli/v2" @@ -810,6 +811,43 @@ var ( Category: flags.BuilderCategory, } + // Block Validation Settings + BlockValidationEnabled = &cli.BoolFlag{ + Name: "validation", + Usage: "Enable the block validation service", + Value: validation.DefaultConfig.Enabled, + Category: flags.ValidationCategory, + } + + BlockValidationListenAddr = &cli.StringFlag{ + Name: "validation.listen_addr", + Usage: "Listening address for block validation endpoint", + Value: validation.DefaultConfig.ListenAddr, + Category: flags.ValidationCategory, + } + + BlockValidationBlacklistSourceFilePath = &cli.StringFlag{ + Name: "validation.blacklist", + Usage: "Path to file containing blacklisted addresses, json-encoded list of strings. " + + "Builder will ignore transactions that touch mentioned addresses. This flag is also used for block validation API.\n" + + "NOTE: builder.blacklist is deprecated and will be removed in the future in favor of validation.blacklist", + Category: flags.ValidationCategory, + } + + BlockValidationUseBalanceDiff = &cli.BoolFlag{ + Name: "validation.use_balance_diff", + Usage: "Block validation API will use fee recipient balance difference for profit calculation.", + Value: validation.DefaultConfig.ExcludeWithdrawals, + Category: flags.ValidationCategory, + } + + BlockValidationExcludeWithdrawals = &cli.BoolFlag{ + Name: "validation.exclude_withdrawals", + Usage: "Block validation API will exclude CL withdrawals to the fee recipient from the balance delta.", + Value: validation.DefaultConfig.ExcludeWithdrawals, + Category: flags.ValidationCategory, + } + // RPC settings IPCDisabledFlag = &cli.BoolFlag{ Name: "ipcdisable", @@ -1641,6 +1679,37 @@ func SetBuilderConfig(ctx *cli.Context, cfg *builder.Config) { cfg.BlockProcessorURL = ctx.String(BuilderBlockProcessorURL.Name) } +// SetValidationConfig applies node-related command line flags to the block validation config. +func SetValidationConfig(ctx *cli.Context, cfg *validation.Config) { + if ctx.IsSet(BlockValidationEnabled.Name) { + cfg.Enabled = ctx.Bool(BlockValidationEnabled.Name) + } + + // this flag should be deprecated in favor of the validation api + if ctx.IsSet(BuilderBlockValidationBlacklistSourceFilePath.Name) { + cfg.Blocklist = ctx.String(BuilderBlockValidationBlacklistSourceFilePath.Name) + } + if ctx.IsSet(BlockValidationBlacklistSourceFilePath.Name) { + cfg.Blocklist = ctx.String(BlockValidationBlacklistSourceFilePath.Name) + } + + // this flag should be deprecated in favor of the validation api + if ctx.IsSet(BuilderBlockValidationUseBalanceDiff.Name) { + cfg.UseCoinbaseDiff = ctx.Bool(BuilderBlockValidationUseBalanceDiff.Name) + } + if ctx.IsSet(BlockValidationUseBalanceDiff.Name) { + cfg.UseCoinbaseDiff = ctx.Bool(BlockValidationUseBalanceDiff.Name) + } + + // this flag should be deprecated in favor of the validation api + if ctx.IsSet(BuilderBlockValidationExcludeWithdrawals.Name) { + cfg.ExcludeWithdrawals = ctx.Bool(BuilderBlockValidationExcludeWithdrawals.Name) + } + if ctx.IsSet(BlockValidationExcludeWithdrawals.Name) { + cfg.ExcludeWithdrawals = ctx.Bool(BlockValidationExcludeWithdrawals.Name) + } +} + // SetNodeConfig applies node-related command line flags to the config. func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { SetP2PConfig(ctx, &cfg.P2P) diff --git a/internal/flags/categories.go b/internal/flags/categories.go index da1889b48..b15ff4ffd 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -36,6 +36,7 @@ const ( MetricsCategory = "METRICS AND STATS" MiscCategory = "MISC" BuilderCategory = "BUILDER" + ValidationCategory = "VALIDATION" TestingCategory = "TESTING" DeprecatedCategory = "ALIASED (deprecated)" ) diff --git a/validation/api.go b/validation/api.go new file mode 100644 index 000000000..6d59b6bc4 --- /dev/null +++ b/validation/api.go @@ -0,0 +1,113 @@ +package validation + +import ( + "compress/gzip" + "encoding/json" + "io" + "net/http" + "time" + + validation "github.com/ethereum/go-ethereum/eth/block-validation" + "github.com/ethereum/go-ethereum/log" + "github.com/gorilla/mux" +) + +type BlockValidationApi struct { + validationApi *validation.BlockValidationAPI +} + +func (api *BlockValidationApi) getRouter() http.Handler { + r := mux.NewRouter() + + r.HandleFunc("/", api.handleRoot).Methods(http.MethodGet) + r.HandleFunc("/validate/block_submission", api.handleBuilderSubmission).Methods(http.MethodPost) + return r +} + +func (api *BlockValidationApi) handleRoot(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} + +func (api *BlockValidationApi) handleBuilderSubmission(w http.ResponseWriter, req *http.Request) { + var prevTime, nextTime time.Time + receivedAt := time.Now().UTC() + prevTime = receivedAt + + var err error + var r io.Reader = req.Body + + isGzip := req.Header.Get("Content-Encoding") == "gzip" + if isGzip { + r, err = gzip.NewReader(req.Body) + if err != nil { + log.Error("could not create gzip reader", "err", err) + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + } + + limitReader := io.LimitReader(r, 10*1024*1024) // 10 MB + requestPayloadBytes, err := io.ReadAll(limitReader) + if err != nil { + log.Error("could not read payload", "err", err) + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + + nextTime = time.Now().UTC() + readTime := uint64(nextTime.Sub(prevTime).Microseconds()) + prevTime = nextTime + + payload := new(BuilderBlockValidationRequestV3) + + // Check for SSZ encoding + contentType := req.Header.Get("Content-Type") + if contentType == "application/octet-stream" { + if err = payload.UnmarshalSSZ(requestPayloadBytes); err != nil { + log.Error("could not decode payload - SSZ", "err", err) + } + } else { + if err := json.Unmarshal(requestPayloadBytes, payload); err != nil { + log.Error("could not decode payload - JSON", "err", err) + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + } + + nextTime = time.Now().UTC() + decodeTime := uint64(nextTime.Sub(prevTime).Microseconds()) + prevTime = nextTime + + // Validate the payload + err = api.validationApi.ValidateBuilderSubmissionV3(&validation.BuilderBlockValidationRequestV3{ + SubmitBlockRequest: payload.SubmitBlockRequest, + ParentBeaconBlockRoot: payload.ParentBeaconBlockRoot, + RegisteredGasLimit: payload.RegisteredGasLimit, + }) + validationTime := uint64(time.Now().UTC().Sub(prevTime).Microseconds()) + + l := log.New("isGzip", isGzip, "payloadBytes", len(requestPayloadBytes), "contentType", contentType, + "numBlobs", len(payload.BlobsBundle.Blobs), "numTx", len(payload.ExecutionPayload.Transactions), + "slot", payload.Message.Slot, "readTime", readTime, "decodeTime", decodeTime, "validationTime", validationTime) + + if err != nil { + l.Info("Validation failed", "err", err) + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + l.Info("Validation successful") + w.WriteHeader(http.StatusOK) +} + +func (api *BlockValidationApi) RespondError(w http.ResponseWriter, code int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + + // write the json response + response := HTTPErrorResp{code, message} + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Error("Couldn't write response", "error", err, "response", response) + http.Error(w, "", http.StatusInternalServerError) + } +} diff --git a/validation/api_test.go b/validation/api_test.go new file mode 100644 index 000000000..5d6d160f3 --- /dev/null +++ b/validation/api_test.go @@ -0,0 +1,290 @@ +package validation + +import ( + "bytes" + "errors" + "math/big" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + beaconConsensus "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" + blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation" + "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/miner" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +var ( + // testKey is a private key to use for funding a tester account. + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + + // testAddr is the Ethereum address of the tester account. + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + + testValidatorKey, _ = crypto.HexToECDSA("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0") + testValidatorAddr = crypto.PubkeyToAddress(testValidatorKey.PublicKey) + + testBuilderKeyHex = "0bfbbbc68fefd990e61ba645efb84e0a62e94d5fff02c9b1da8eb45fea32b4e0" + testBuilderKey, _ = crypto.HexToECDSA(testBuilderKeyHex) + testBuilderAddr = crypto.PubkeyToAddress(testBuilderKey.PublicKey) + + testBalance = big.NewInt(2e18) +) + +func generateMergeChain(n int) (*core.Genesis, []*types.Block) { + config := *params.AllEthashProtocolChanges + + config.TerminalTotalDifficulty = common.Big0 + config.TerminalTotalDifficultyPassed = true + engine := beaconConsensus.NewFaker() + + genesis := &core.Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + testAddr: {Balance: testBalance}, + params.BeaconRootsStorageAddress: {Balance: common.Big0, Code: common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604457602036146024575f5ffd5b620180005f350680545f35146037575f5ffd5b6201800001545f5260205ff35b6201800042064281555f359062018000015500")}, + }, + ExtraData: []byte("test genesis"), + Timestamp: 9000, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(0), + } + testNonce := uint64(0) + generate := func(_ int, g *core.BlockGen) { + g.OffsetTime(5) + g.SetExtra([]byte("test")) + tx, _ := types.SignTx(types.NewTransaction(testNonce, common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a"), big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(&config), testKey) + g.AddTx(tx) + testNonce++ + } + _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, n, generate) + + return genesis, blocks +} + +// startEthService creates a full node instance for testing. +func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) { + t.Helper() + + n, err := node.New(&node.Config{ + P2P: p2p.Config{ + ListenAddr: "0.0.0.0:0", + NoDiscovery: true, + MaxPeers: 25, + }, + }) + if err != nil { + t.Fatal("can't create node:", err) + } + + ethcfg := ðconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256} + ethservice, err := eth.New(n, ethcfg) + if err != nil { + t.Fatal("can't create eth service:", err) + } + if err := n.Start(); err != nil { + t.Fatal("can't start node:", err) + } + if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil { + n.Close() + t.Fatal("can't import test blocks:", err) + } + time.Sleep(500 * time.Millisecond) // give txpool enough time to consume head event + + ethservice.SetEtherbase(testAddr) + ethservice.SetSynced() + return n, ethservice +} + +func assembleBlock(eth *eth.Ethereum, parentHash common.Hash, params *engine.PayloadAttributes) (*engine.ExecutableData, error) { + args := &miner.BuildPayloadArgs{ + Parent: parentHash, + Timestamp: params.Timestamp, + FeeRecipient: params.SuggestedFeeRecipient, + GasLimit: params.GasLimit, + Random: params.Random, + Withdrawals: params.Withdrawals, + BeaconRoot: params.BeaconRoot, + } + + payload, err := eth.Miner().BuildPayload(args) + if err != nil { + return nil, err + } + + if payload := payload.ResolveFull(); payload != nil { + return payload.ExecutionPayload, nil + } + + return nil, errors.New("payload did not resolve") +} + +type testBackend struct { + api *BlockValidationApi +} + +// newTestBackend creates a new backend, initializes mock relays, registers them and return the instance +func newTestBackend(t *testing.T, ethservice *eth.Ethereum) *testBackend { + t.Helper() + + api := &BlockValidationApi{ + validationApi: blockvalidation.NewBlockValidationAPI(ethservice, nil, true, true), + } + + backend := testBackend{api} + return &backend +} + +func (be *testBackend) request(t *testing.T, method, path string, payload []byte) *httptest.ResponseRecorder { + t.Helper() + var req *http.Request + var err error + + if payload == nil { + req, err = http.NewRequest(method, path, bytes.NewReader(nil)) + } else { + req, err = http.NewRequest(method, path, bytes.NewReader(payload)) + req.Header.Add("Content-Type", "application/octet-stream") + } + + require.NoError(t, err) + rr := httptest.NewRecorder() + be.api.getRouter().ServeHTTP(rr, req) + return rr +} + +func TestBlockValidation(t *testing.T) { + genesis, blocks := generateMergeChain(10) + + // Set cancun time to last block + 5 seconds + time := blocks[len(blocks)-1].Time() + 5 + genesis.Config.ShanghaiTime = &time + genesis.Config.CancunTime = &time + os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) + + n, ethservice := startEthService(t, genesis, blocks) + defer n.Close() + + backend := newTestBackend(t, ethservice) + + parent := ethservice.BlockChain().CurrentHeader() + statedb, err := ethservice.BlockChain().StateAt(parent.Root) + require.NoError(t, err) + nonce := statedb.GetNonce(testAddr) + ethservice.APIBackend.Miner().SetEtherbase(testBuilderAddr) + + tx1, err := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + require.NoError(t, err) + ethservice.TxPool().Add([]*types.Transaction{tx1}, true, true, false) + + cc, err := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + require.NoError(t, err) + ethservice.TxPool().Add([]*types.Transaction{cc}, true, true, false) + + baseFee := eip1559.CalcBaseFee(params.AllEthashProtocolChanges, parent) + tx2, err := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + require.NoError(t, err) + ethservice.TxPool().Add([]*types.Transaction{tx2}, true, true, false) + + execData, err := assembleBlock(ethservice, parent.Hash(), &engine.PayloadAttributes{ + Timestamp: parent.Time + 12, + SuggestedFeeRecipient: testValidatorAddr, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &common.Hash{42}, + }) + require.NoError(t, err) + payload, err := ExecutableDataToExecutionPayloadV3(execData) + require.NoError(t, err) + blockRequest := &BuilderBlockValidationRequestV3{ + SubmitBlockRequest: builderApiDeneb.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &builderApiV1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: bellatrix.ExecutionAddress(testValidatorAddr), + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + // This value is actual profit + 1, validation should fail + Value: uint256.NewInt(125851807635001), + }, + ExecutionPayload: payload, + BlobsBundle: &builderApiDeneb.BlobsBundle{ + Commitments: make([]deneb.KZGCommitment, 0), + Proofs: make([]deneb.KZGProof, 0), + Blobs: make([]deneb.Blob, 0), + }, + }, + RegisteredGasLimit: execData.GasLimit, + ParentBeaconBlockRoot: common.Hash{42}, + } + + payloadBytes, err := blockRequest.MarshalSSZ() + require.NoError(t, err) + rr := backend.request(t, http.MethodPost, "/validate/block_submission", payloadBytes) + require.Equal(t, `{"code":400,"message":"inaccurate payment 125851807635000, expected 125851807635001"}`+"\n", rr.Body.String()) + require.Equal(t, http.StatusBadRequest, rr.Code) + + blockRequest.Message.Value = uint256.NewInt(125851807635000) + payloadBytes, err = blockRequest.MarshalSSZ() + require.NoError(t, err) + rr = backend.request(t, http.MethodPost, "/validate/block_submission", payloadBytes) + require.Equal(t, http.StatusOK, rr.Code) +} + +func ExecutableDataToExecutionPayloadV3(data *engine.ExecutableData) (*deneb.ExecutionPayload, error) { + transactionData := make([]bellatrix.Transaction, len(data.Transactions)) + for i, tx := range data.Transactions { + transactionData[i] = bellatrix.Transaction(tx) + } + + withdrawalData := make([]*capella.Withdrawal, len(data.Withdrawals)) + for i, withdrawal := range data.Withdrawals { + withdrawalData[i] = &capella.Withdrawal{ + Index: capella.WithdrawalIndex(withdrawal.Index), + ValidatorIndex: phase0.ValidatorIndex(withdrawal.Validator), + Address: bellatrix.ExecutionAddress(withdrawal.Address), + Amount: phase0.Gwei(withdrawal.Amount), + } + } + + return &deneb.ExecutionPayload{ + ParentHash: [32]byte(data.ParentHash), + FeeRecipient: [20]byte(data.FeeRecipient), + StateRoot: [32]byte(data.StateRoot), + ReceiptsRoot: [32]byte(data.ReceiptsRoot), + LogsBloom: types.BytesToBloom(data.LogsBloom), + PrevRandao: [32]byte(data.Random), + BlockNumber: data.Number, + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Timestamp: data.Timestamp, + ExtraData: data.ExtraData, + BaseFeePerGas: uint256.MustFromBig(data.BaseFeePerGas), + BlockHash: [32]byte(data.BlockHash), + Transactions: transactionData, + Withdrawals: withdrawalData, + BlobGasUsed: *data.BlobGasUsed, + ExcessBlobGas: *data.ExcessBlobGas, + }, nil +} diff --git a/validation/config.go b/validation/config.go new file mode 100644 index 000000000..146c964a0 --- /dev/null +++ b/validation/config.go @@ -0,0 +1,18 @@ +package validation + +type Config struct { + Enabled bool `toml:",omitempty"` + ListenAddr string `toml:",omitempty"` + Blocklist string `toml:",omitempty"` + UseCoinbaseDiff bool `toml:",omitempty"` + ExcludeWithdrawals bool `toml:",omitempty"` +} + +// DefaultConfig is the default config for validation api. +var DefaultConfig = Config{ + Enabled: false, + ListenAddr: ":28546", + Blocklist: "", + UseCoinbaseDiff: false, + ExcludeWithdrawals: false, +} diff --git a/validation/service.go b/validation/service.go new file mode 100644 index 000000000..06f848309 --- /dev/null +++ b/validation/service.go @@ -0,0 +1,75 @@ +package validation + +import ( + "fmt" + "net/http" + + "github.com/ethereum/go-ethereum/eth" + validation "github.com/ethereum/go-ethereum/eth/block-validation" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" +) + +type Service struct { + srv *http.Server +} + +func NewService(addr string, validationApi *validation.BlockValidationAPI) *Service { + api := BlockValidationApi{ + validationApi: validationApi, + } + s := &Service{ + srv: &http.Server{ + Addr: addr, + Handler: api.getRouter(), + }, + } + return s +} + +func (s *Service) Start() error { + if s.srv != nil { + log.Info("Service started") + go s.srv.ListenAndServe() + } + return nil +} + +func (s *Service) Stop() error { + if s.srv != nil { + s.srv.Close() + } + return nil +} + +func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error { + if !cfg.Enabled { + return nil + } + + var err error + var accessVerifier *validation.AccessVerifier + if cfg.Blocklist != "" { + accessVerifier, err = validation.NewAccessVerifierFromFile(cfg.Blocklist) + if err != nil { + return fmt.Errorf("failed to load validation blocklist %w", err) + } + } + validationApi := validation.NewBlockValidationAPI(backend, accessVerifier, cfg.UseCoinbaseDiff, cfg.ExcludeWithdrawals) + validationService := NewService(cfg.ListenAddr, validationApi) + + stack.RegisterAPIs([]rpc.API{ + { + Namespace: "validation", + Version: "1.0", + Service: validationService, + Public: true, + Authenticated: true, + }, + }) + + stack.RegisterLifecycle(validationService) + + return nil +} diff --git a/validation/types.go b/validation/types.go new file mode 100644 index 000000000..d1cf63665 --- /dev/null +++ b/validation/types.go @@ -0,0 +1,62 @@ +package validation + +import ( + "encoding/json" + + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/ethereum/go-ethereum/common" +) + +type HTTPErrorResp struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type BuilderBlockValidationRequestV3 struct { + builderApiDeneb.SubmitBlockRequest + ParentBeaconBlockRoot common.Hash `json:"parent_beacon_block_root" ssz-size:"32"` + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` +} + +func (r *BuilderBlockValidationRequestV3) MarshalJSON() ([]byte, error) { + type denebBuilderBlockValidationRequestJSON struct { + Message *builderApiV1.BidTrace `json:"message"` + ExecutionPayload *deneb.ExecutionPayload `json:"execution_payload"` + BlobsBundle *builderApiDeneb.BlobsBundle `json:"blobs_bundle"` + Signature string `json:"signature"` + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` + ParentBeaconBlockRoot string `json:"parent_beacon_block_root"` + } + + return json.Marshal(&denebBuilderBlockValidationRequestJSON{ + Message: r.Message, + ExecutionPayload: r.ExecutionPayload, + BlobsBundle: r.BlobsBundle, + Signature: r.Signature.String(), + RegisteredGasLimit: r.RegisteredGasLimit, + ParentBeaconBlockRoot: r.ParentBeaconBlockRoot.String(), + }) +} + +func (r *BuilderBlockValidationRequestV3) UnmarshalJSON(data []byte) error { + params := &struct { + ParentBeaconBlockRoot common.Hash `json:"parent_beacon_block_root"` + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` + }{} + err := json.Unmarshal(data, params) + if err != nil { + return err + } + r.RegisteredGasLimit = params.RegisteredGasLimit + r.ParentBeaconBlockRoot = params.ParentBeaconBlockRoot + + blockRequest := new(builderApiDeneb.SubmitBlockRequest) + err = json.Unmarshal(data, &blockRequest) + if err != nil { + return err + } + r.SubmitBlockRequest = *blockRequest + return nil +} diff --git a/validation/validation_request_ssz.go b/validation/validation_request_ssz.go new file mode 100644 index 000000000..34b649b6a --- /dev/null +++ b/validation/validation_request_ssz.go @@ -0,0 +1,195 @@ +// Code generated by fastssz. DO NOT EDIT. +// Hash: cb7006db7808ee76b3c3bcadc1a063f3beffa6f9f14fcc7a3d6f91c2cd700fb1 +// Version: 0.1.3 +package validation + +import ( + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/deneb" + ssz "github.com/ferranbt/fastssz" +) + +// MarshalSSZ ssz marshals the BuilderBlockValidationRequestV3 object +func (b *BuilderBlockValidationRequestV3) MarshalSSZ() ([]byte, error) { + return ssz.MarshalSSZ(b) +} + +// MarshalSSZTo ssz marshals the BuilderBlockValidationRequestV3 object to a target array +func (b *BuilderBlockValidationRequestV3) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + offset := int(380) + + // Field (0) 'Message' + if b.Message == nil { + b.Message = new(builderApiV1.BidTrace) + } + if dst, err = b.Message.MarshalSSZTo(dst); err != nil { + return + } + + // Offset (1) 'ExecutionPayload' + dst = ssz.WriteOffset(dst, offset) + if b.ExecutionPayload == nil { + b.ExecutionPayload = new(deneb.ExecutionPayload) + } + offset += b.ExecutionPayload.SizeSSZ() + + // Offset (2) 'BlobsBundle' + dst = ssz.WriteOffset(dst, offset) + if b.BlobsBundle == nil { + b.BlobsBundle = new(builderApiDeneb.BlobsBundle) + } + offset += b.BlobsBundle.SizeSSZ() + + // Field (3) 'Signature' + dst = append(dst, b.Signature[:]...) + + // Field (4) 'ParentBeaconBlockRoot' + dst = append(dst, b.ParentBeaconBlockRoot[:]...) + + // Field (5) 'RegisteredGasLimit' + dst = ssz.MarshalUint64(dst, b.RegisteredGasLimit) + + // Field (1) 'ExecutionPayload' + if dst, err = b.ExecutionPayload.MarshalSSZTo(dst); err != nil { + return + } + + // Field (2) 'BlobsBundle' + if dst, err = b.BlobsBundle.MarshalSSZTo(dst); err != nil { + return + } + + return +} + +// UnmarshalSSZ ssz unmarshals the BuilderBlockValidationRequestV3 object +func (b *BuilderBlockValidationRequestV3) UnmarshalSSZ(buf []byte) error { + var err error + size := uint64(len(buf)) + if size < 380 { + return ssz.ErrSize + } + + tail := buf + var o1, o2 uint64 + + // Field (0) 'Message' + if b.Message == nil { + b.Message = new(builderApiV1.BidTrace) + } + if err = b.Message.UnmarshalSSZ(buf[0:236]); err != nil { + return err + } + + // Offset (1) 'ExecutionPayload' + if o1 = ssz.ReadOffset(buf[236:240]); o1 > size { + return ssz.ErrOffset + } + + if o1 < 380 { + return ssz.ErrInvalidVariableOffset + } + + // Offset (2) 'BlobsBundle' + if o2 = ssz.ReadOffset(buf[240:244]); o2 > size || o1 > o2 { + return ssz.ErrOffset + } + + // Field (3) 'Signature' + copy(b.Signature[:], buf[244:340]) + + // Field (4) 'ParentBeaconBlockRoot' + copy(b.ParentBeaconBlockRoot[:], buf[340:372]) + + // Field (5) 'RegisteredGasLimit' + b.RegisteredGasLimit = ssz.UnmarshallUint64(buf[372:380]) + + // Field (1) 'ExecutionPayload' + { + buf = tail[o1:o2] + if b.ExecutionPayload == nil { + b.ExecutionPayload = new(deneb.ExecutionPayload) + } + if err = b.ExecutionPayload.UnmarshalSSZ(buf); err != nil { + return err + } + } + + // Field (2) 'BlobsBundle' + { + buf = tail[o2:] + if b.BlobsBundle == nil { + b.BlobsBundle = new(builderApiDeneb.BlobsBundle) + } + if err = b.BlobsBundle.UnmarshalSSZ(buf); err != nil { + return err + } + } + return err +} + +// SizeSSZ returns the ssz encoded size in bytes for the BuilderBlockValidationRequestV3 object +func (b *BuilderBlockValidationRequestV3) SizeSSZ() (size int) { + size = 380 + + // Field (1) 'ExecutionPayload' + if b.ExecutionPayload == nil { + b.ExecutionPayload = new(deneb.ExecutionPayload) + } + size += b.ExecutionPayload.SizeSSZ() + + // Field (2) 'BlobsBundle' + if b.BlobsBundle == nil { + b.BlobsBundle = new(builderApiDeneb.BlobsBundle) + } + size += b.BlobsBundle.SizeSSZ() + + return +} + +// HashTreeRoot ssz hashes the BuilderBlockValidationRequestV3 object +func (b *BuilderBlockValidationRequestV3) HashTreeRoot() ([32]byte, error) { + return ssz.HashWithDefaultHasher(b) +} + +// HashTreeRootWith ssz hashes the BuilderBlockValidationRequestV3 object with a hasher +func (b *BuilderBlockValidationRequestV3) HashTreeRootWith(hh ssz.HashWalker) (err error) { + indx := hh.Index() + + // Field (0) 'Message' + if b.Message == nil { + b.Message = new(builderApiV1.BidTrace) + } + if err = b.Message.HashTreeRootWith(hh); err != nil { + return + } + + // Field (1) 'ExecutionPayload' + if err = b.ExecutionPayload.HashTreeRootWith(hh); err != nil { + return + } + + // Field (2) 'BlobsBundle' + if err = b.BlobsBundle.HashTreeRootWith(hh); err != nil { + return + } + + // Field (3) 'Signature' + hh.PutBytes(b.Signature[:]) + + // Field (4) 'ParentBeaconBlockRoot' + hh.PutBytes(b.ParentBeaconBlockRoot[:]) + + // Field (5) 'RegisteredGasLimit' + hh.PutUint64(b.RegisteredGasLimit) + + hh.Merkleize(indx) + return +} + +// GetTree ssz hashes the BuilderBlockValidationRequestV3 object +func (b *BuilderBlockValidationRequestV3) GetTree() (*ssz.Node, error) { + return ssz.ProofTree(b) +}