diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 37a7dc73ac..b1cc13444c 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -22,10 +22,12 @@ import ( "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie" + boostTypes "github.com/flashbots/go-boost-utils/types" ) //go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go @@ -141,7 +143,7 @@ func encodeTransactions(txs []*types.Transaction) [][]byte { return enc } -func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) { +func DecodeTransactions(enc [][]byte) ([]*types.Transaction, error) { var txs = make([]*types.Transaction, len(enc)) for i, encTx := range enc { var tx types.Transaction @@ -164,7 +166,7 @@ func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) { // Withdrawals value will propagate through the returned block. Empty // Withdrawals value must be passed via non-nil, length 0 value in params. func ExecutableDataToBlock(params ExecutableData) (*types.Block, error) { - txs, err := decodeTransactions(params.Transactions) + txs, err := DecodeTransactions(params.Transactions) if err != nil { return nil, err } @@ -246,7 +248,7 @@ func ExecutionPayloadToBlock(payload *bellatrix.ExecutionPayload) (*types.Block, for i, txHexBytes := range payload.Transactions { transactionBytes[i] = txHexBytes[:] } - txs, err := decodeTransactions(transactionBytes) + txs, err := DecodeTransactions(transactionBytes) if err != nil { return nil, err } @@ -286,7 +288,7 @@ func ExecutionPayloadV2ToBlock(payload *capella.ExecutionPayload) (*types.Block, for i, txHexBytes := range payload.Transactions { transactionBytes[i] = txHexBytes[:] } - txs, err := decodeTransactions(transactionBytes) + txs, err := DecodeTransactions(transactionBytes) if err != nil { return nil, err } @@ -331,3 +333,45 @@ func ExecutionPayloadV2ToBlock(payload *capella.ExecutionPayload) (*types.Block, block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */).WithWithdrawals(withdrawals) return block, nil } + +// bchain: copied this here to avoid circular dependency +func ExecutableDataToCapellaExecutionPayload(data *ExecutableData) (*capella.ExecutionPayload, error) { + transactionData := make([]bellatrix.Transaction, len(data.Transactions)) + for i, tx := range data.Transactions { + transactionData[i] = tx + } + + withdrawalData := make([]*capella.Withdrawal, len(data.Withdrawals)) + for i, wd := range data.Withdrawals { + withdrawalData[i] = &capella.Withdrawal{ + Index: capella.WithdrawalIndex(wd.Index), + ValidatorIndex: phase0.ValidatorIndex(wd.Validator), + Address: bellatrix.ExecutionAddress(wd.Address), + Amount: phase0.Gwei(wd.Amount), + } + } + + baseFeePerGas := new(boostTypes.U256Str) + err := baseFeePerGas.FromBig(data.BaseFeePerGas) + if err != nil { + return nil, err + } + + return &capella.ExecutionPayload{ + ParentHash: [32]byte(data.ParentHash), + FeeRecipient: [20]byte(data.FeeRecipient), + StateRoot: data.StateRoot, + ReceiptsRoot: data.ReceiptsRoot, + LogsBloom: types.BytesToBloom(data.LogsBloom), + PrevRandao: data.Random, + BlockNumber: data.Number, + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Timestamp: data.Timestamp, + ExtraData: data.ExtraData, + BaseFeePerGas: *baseFeePerGas, + BlockHash: [32]byte(data.BlockHash), + Transactions: transactionData, + Withdrawals: withdrawalData, + }, nil +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..c56d158d8a --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +docker build -t pepc-boost-builder .; +docker tag pepc-boost-builder:latest public.ecr.aws/t1d5h1w5/pepc-boost-builder:latest; +docker push public.ecr.aws/t1d5h1w5/pepc-boost-builder:latest; \ No newline at end of file diff --git a/builder/builder.go b/builder/builder.go index 44201bcdeb..c7032284f4 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -13,7 +13,6 @@ import ( capellaapi "github.com/attestantio/go-builder-client/api/capella" apiv1 "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/phase0" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" @@ -47,6 +46,8 @@ type ValidatorData struct { type IRelay interface { SubmitBlock(msg *bellatrixapi.SubmitBlockRequest, vd ValidatorData) error + // this is for the builder to be aware of pepc boost specific features + IsPepcRelayer() (bool, error) SubmitBlockCapella(msg *capellaapi.SubmitBlockRequest, vd ValidatorData) error GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) Config() RelayConfig @@ -282,7 +283,7 @@ func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, or commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, proposerPubkey phase0.BLSPubKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { executableData := engine.BlockToExecutableData(block, blockValue) - payload, err := executableDataToCapellaExecutionPayload(executableData.ExecutionPayload) + payload, err := engine.ExecutableDataToCapellaExecutionPayload(executableData.ExecutionPayload) if err != nil { log.Error("could not format execution payload", "err", err) return err @@ -345,9 +346,9 @@ func (b *Builder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) erro if err != nil { return fmt.Errorf("could not get validator while submitting block for slot %d - %w", attrs.Slot, err) } - attrs.SuggestedFeeRecipient = [20]byte(vd.FeeRecipient) attrs.GasLimit = vd.GasLimit + attrs.IsPepcRelayer = true proposerPubkey, err := utils.HexToPubkey(string(vd.Pubkey)) if err != nil { @@ -413,7 +414,7 @@ func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey phase0. queueBestEntry blockQueueEntry ) - log.Debug("runBuildingJob", "slot", attrs.Slot, "parent", attrs.HeadHash, "payloadTimestamp", uint64(attrs.Timestamp)) + log.Debug("runBuildingJob", "slot", attrs.Slot, "parent", attrs.HeadHash, "payloadTimestamp", uint64(attrs.Timestamp), "gasLimit", attrs.GasLimit) submitBestBlock := func() { queueMu.Lock() @@ -510,44 +511,3 @@ func executableDataToExecutionPayload(data *engine.ExecutableData) (*bellatrix.E Transactions: transactionData, }, nil } - -func executableDataToCapellaExecutionPayload(data *engine.ExecutableData) (*capella.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, wd := range data.Withdrawals { - withdrawalData[i] = &capella.Withdrawal{ - Index: capella.WithdrawalIndex(wd.Index), - ValidatorIndex: phase0.ValidatorIndex(wd.Validator), - Address: bellatrix.ExecutionAddress(wd.Address), - Amount: phase0.Gwei(wd.Amount), - } - } - - baseFeePerGas := new(boostTypes.U256Str) - err := baseFeePerGas.FromBig(data.BaseFeePerGas) - if err != nil { - return nil, err - } - - return &capella.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: *baseFeePerGas, - BlockHash: [32]byte(data.BlockHash), - Transactions: transactionData, - Withdrawals: withdrawalData, - }, nil -} diff --git a/builder/local_relay.go b/builder/local_relay.go index 9c2abef78c..26e3fbdc7c 100644 --- a/builder/local_relay.go +++ b/builder/local_relay.go @@ -115,6 +115,10 @@ func (r *LocalRelay) SubmitBlock(msg *bellatrixapi.SubmitBlockRequest, _ Validat return r.submitBlock(msg) } +func (r *LocalRelay) IsPepcRelayer() (bool, error) { + return true, nil +} + func (r *LocalRelay) SubmitBlockCapella(msg *capellaapi.SubmitBlockRequest, _ ValidatorData) error { log.Info("submitting block to local relay", "block", msg.ExecutionPayload.BlockHash.String()) diff --git a/builder/relay.go b/builder/relay.go index a28fe1e71c..b8b87789ac 100644 --- a/builder/relay.go +++ b/builder/relay.go @@ -71,7 +71,6 @@ func (r *RemoteRelay) updateValidatorsMap(currentSlot uint64, retries int) error r.validatorSyncOngoing = true r.validatorsLock.Unlock() - log.Info("requesting ", "currentSlot", currentSlot) newMap, err := r.getSlotValidatorMapFromRelay() for err != nil && retries > 0 { log.Error("could not get validators map from relay, retrying", "err", err) @@ -193,6 +192,23 @@ func (r *RemoteRelay) SubmitBlockCapella(msg *capella.SubmitBlockRequest, _ Vali return nil } +func (r *RemoteRelay) IsPepcRelayer() (bool, error) { + log.Info("submitting rob block to remote relay", "endpoint", r.config.Endpoint) + + endpoint := r.config.Endpoint + "/relay/v1/data/is_pepc_relayer" + + var isPepcRelayer bool + code, err := SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, nil, isPepcRelayer) + if err != nil { + return false, fmt.Errorf("error sending http request to relay %s. err: %w", r.config.Endpoint, err) + } + if code > 299 { + return false, fmt.Errorf("non-ok response code %d from relay %s", code, r.config.Endpoint) + } + + return isPepcRelayer, nil +} + func (r *RemoteRelay) getSlotValidatorMapFromRelay() (map[uint64]ValidatorData, error) { var dst GetValidatorRelayResponse code, err := SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, r.config.Endpoint+"/relay/v1/builder/validators", nil, &dst) diff --git a/builder/relay_aggregator.go b/builder/relay_aggregator.go index 12f6f62c13..b15c989f21 100644 --- a/builder/relay_aggregator.go +++ b/builder/relay_aggregator.go @@ -18,6 +18,10 @@ type RemoteRelayAggregator struct { registrationsCache map[ValidatorData][]IRelay } +func (r *RemoteRelayAggregator) IsPepcRelayer() (bool, error) { + return false, fmt.Errorf("not implemented") +} + func NewRemoteRelayAggregator(primary IRelay, secondary []IRelay) *RemoteRelayAggregator { relays := []IRelay{primary} return &RemoteRelayAggregator{ diff --git a/builder/relay_aggregator_test.go b/builder/relay_aggregator_test.go index d46533e49a..ab902cc028 100644 --- a/builder/relay_aggregator_test.go +++ b/builder/relay_aggregator_test.go @@ -73,6 +73,10 @@ func (r *testRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) return r.gvsVd, r.gvsErr } +func (r *testRelay) IsPepcRelayer() (bool, error) { + return false, nil +} + func (r *testRelay) Start() error { return nil } diff --git a/core/types/builder.go b/core/types/builder.go index d31da82fdb..735040d4d4 100644 --- a/core/types/builder.go +++ b/core/types/builder.go @@ -14,6 +14,7 @@ type BuilderPayloadAttributes struct { HeadHash common.Hash `json:"blockHash"` Withdrawals Withdrawals `json:"withdrawals"` GasLimit uint64 + IsPepcRelayer bool `json:"isPepcRelayer"` } func (attrs *BuilderPayloadAttributes) Equal(other *BuilderPayloadAttributes) bool { @@ -22,7 +23,8 @@ func (attrs *BuilderPayloadAttributes) Equal(other *BuilderPayloadAttributes) bo attrs.SuggestedFeeRecipient != other.SuggestedFeeRecipient || attrs.Slot != other.Slot || attrs.HeadHash != other.HeadHash || - attrs.GasLimit != other.GasLimit { + attrs.GasLimit != other.GasLimit || + attrs.IsPepcRelayer != other.IsPepcRelayer { return false } diff --git a/core/types/transaction.go b/core/types/transaction.go index 03eb018326..b5d8521b49 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -413,6 +413,10 @@ func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, e // Transactions implements DerivableList for transactions. type Transactions []*Transaction +func (s Transactions) Index(i int) *Transaction { + return s[i] +} + // Len returns the length of s. func (s Transactions) Len() int { return len(s) } diff --git a/eth/block-validation/api.go b/eth/block-validation/api.go index a51b7504b5..966903c4ef 100644 --- a/eth/block-validation/api.go +++ b/eth/block-validation/api.go @@ -9,7 +9,9 @@ import ( bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" capellaapi "github.com/attestantio/go-builder-client/api/capella" + "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" + bellatrixUtil "github.com/attestantio/go-eth2-client/util/bellatrix" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -17,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" ) @@ -228,6 +231,119 @@ func (r *BuilderBlockValidationRequestV2) UnmarshalJSON(data []byte) error { return nil } +type BlockAssemblerRequest struct { + TobTxs bellatrixUtil.ExecutionPayloadTransactions + RobPayload capellaapi.SubmitBlockRequest + RegisteredGasLimit uint64 +} + +type IntermediateBlockAssemblerRequest struct { + TobTxs []byte `json:"tob_txs"` + RobPayload capellaapi.SubmitBlockRequest `json:"rob_payload"` + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` +} + +func (b *BlockAssemblerRequest) UnmarshalJSON(data []byte) error { + var intermediateJson IntermediateBlockAssemblerRequest + err := json.Unmarshal(data, &intermediateJson) + if err != nil { + return err + } + err = b.TobTxs.UnmarshalSSZ(intermediateJson.TobTxs) + if err != nil { + return err + } + b.RegisteredGasLimit = intermediateJson.RegisteredGasLimit + b.RobPayload = intermediateJson.RobPayload + + return nil +} + +func (api *BlockValidationAPI) BlockAssembler(params *BlockAssemblerRequest) (*capella.ExecutionPayload, error) { + log.Info("BlockAssembler", "tobTxs", len(params.TobTxs.Transactions), "robPayload", params.RobPayload) + transactionBytes := make([][]byte, len(params.TobTxs.Transactions)) + for i, txHexBytes := range params.TobTxs.Transactions { + transactionBytes[i] = txHexBytes[:] + } + decodedTobTxs, err := engine.DecodeTransactions(transactionBytes) + if err != nil { + return nil, err + } + + robBlock, err := engine.ExecutionPayloadV2ToBlock(params.RobPayload.ExecutionPayload) + if err != nil { + return nil, err + } + + tobTxs := types.Transactions(decodedTobTxs) + + // TODO - check for gas limits + // TODO - support for payouts + + // TODO - if there are no TOB txs then we can just simulate the block rather then re-assembling it. + + // check if there are any duplicate txs + // we can error out if there is a nonce gap + // TODO - don't error out, but drop the duplicate tx in the ROB block + seenTxMap := make(map[common.Hash]struct{}) + for _, tx := range tobTxs { + // If we see nonce reuse in TOB then fail + if _, ok := seenTxMap[tx.Hash()]; ok { + return nil, errors.New("duplicate tx") + } + seenTxMap[tx.Hash()] = struct{}{} + } + for _, tx := range robBlock.Transactions() { + // if we see nonce re-use in TOB vs ROB then drop txs the txs in ROB + if _, ok := seenTxMap[tx.Hash()]; ok { + return nil, errors.New("duplicate tx") + } + seenTxMap[tx.Hash()] = struct{}{} + } + + withdrawals := make(types.Withdrawals, len(params.RobPayload.ExecutionPayload.Withdrawals)) + for i, withdrawal := range params.RobPayload.ExecutionPayload.Withdrawals { + withdrawals[i] = &types.Withdrawal{ + Index: uint64(withdrawal.Index), + Validator: uint64(withdrawal.ValidatorIndex), + Address: common.Address(withdrawal.Address), + Amount: uint64(withdrawal.Amount), + } + } + + // assemble the txs in map[sender]txs format and pass it in the BuildPayload call + + robTxs := robBlock.Transactions() + block, err := api.eth.Miner().PayloadAssembler(&miner.BuildPayloadArgs{ + Parent: common.Hash(params.RobPayload.ExecutionPayload.ParentHash), + Timestamp: params.RobPayload.ExecutionPayload.Timestamp, + // TODO - this should be relayer fee recipient. We will implement payouts later + FeeRecipient: common.Address(params.RobPayload.Message.ProposerFeeRecipient), + GasLimit: params.RegisteredGasLimit, + Random: params.RobPayload.ExecutionPayload.PrevRandao, + Withdrawals: withdrawals, + BlockHook: nil, + AssemblerTxs: miner.AssemblerTxLists{ + TobTxs: &tobTxs, + RobTxs: &robTxs, + }, + }) + if err != nil { + return nil, err + } + resolvedBlock := block.ResolveFull() + if resolvedBlock == nil { + return nil, errors.New("unable to resolve block") + } + if resolvedBlock.ExecutionPayload == nil { + return nil, errors.New("nil execution payload") + } + + finalPayload, err := engine.ExecutableDataToCapellaExecutionPayload(resolvedBlock.ExecutionPayload) + + return finalPayload, nil +} + func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockValidationRequestV2) error { // TODO: fuzztest, make sure the validation is sound // TODO: handle context! diff --git a/eth/block-validation/api_test.go b/eth/block-validation/api_test.go index f7cd3d0b90..36568eed64 100644 --- a/eth/block-validation/api_test.go +++ b/eth/block-validation/api_test.go @@ -15,6 +15,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" + bellatrixUtil "github.com/attestantio/go-eth2-client/util/bellatrix" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -44,10 +45,12 @@ import ( var ( // testKey is a private key to use for funding a tester account. - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testSearcherKey, _ = crypto.HexToECDSA("b61c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") // testAddr is the Ethereum address of the tester account. - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + testSearcherAddr = crypto.PubkeyToAddress(testSearcherKey.PublicKey) testValidatorKey, _ = crypto.HexToECDSA("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0") testValidatorAddr = crypto.PubkeyToAddress(testValidatorKey.PublicKey) @@ -170,6 +173,313 @@ func TestValidateBuilderSubmissionV1(t *testing.T) { require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "could not apply tx 4", "insufficient funds for gas * price + value") } +func TestBlockAssemblerWithNoRobTxs(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) + time := preMergeBlocks[len(preMergeBlocks)-1].Time() + 5 + genesis.Config.ShanghaiTime = &time + + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, false) + parent := preMergeBlocks[len(preMergeBlocks)-1] + + api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr) + + statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) + searcherNonce := statedb.GetNonce(testSearcherAddr) + + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + tobTx1, _ := types.SignTx(types.NewTransaction(searcherNonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testSearcherKey) + tobTx2, _ := types.SignTx(types.NewTransaction(searcherNonce+1, common.Address{0x17}, big.NewInt(20), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testSearcherKey) + + tobTx1Bytes, err := tobTx1.MarshalBinary() + require.NoError(t, err) + tobTx2Bytes, err := tobTx2.MarshalBinary() + require.NoError(t, err) + + tobTxs := bellatrixUtil.ExecutionPayloadTransactions{ + Transactions: []bellatrix.Transaction{tobTx1Bytes, tobTx2Bytes}, + } + + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + { + Index: 1, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + } + withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ + Timestamp: parent.Time() + 5, + Withdrawals: withdrawals, + SuggestedFeeRecipient: testValidatorAddr, + }) + fmt.Printf("DEBUG: Exec data random is %x\n", execData.Random) + require.NoError(t, err) + require.EqualValues(t, len(execData.Withdrawals), 2) + require.EqualValues(t, len(execData.Transactions), 0) + + payload, err := ExecutableDataToExecutionPayloadV2(execData) + require.NoError(t, err) + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], testValidatorAddr.Bytes()) + + blockRequest := &BuilderBlockValidationRequestV2{ + SubmitBlockRequest: capellaapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + // This value is actual profit + 1, validation should fail + Value: uint256.NewInt(149842511727213), + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + WithdrawalsRoot: withdrawalsRoot, + } + + assemblyRequest := BlockAssemblerRequest{ + TobTxs: tobTxs, + RobPayload: blockRequest.SubmitBlockRequest, + RegisteredGasLimit: execData.GasLimit, + } + + block, err := api.BlockAssembler(&assemblyRequest) + require.NoError(t, err) + require.Equal(t, len(block.Transactions), 2) + + // check tob txs + actualTobTx1 := block.Transactions[0] + actualTobTx2 := block.Transactions[1] + require.Equal(t, bellatrixUtil.ExecutionPayloadTransactions{Transactions: []bellatrix.Transaction{actualTobTx1, actualTobTx2}}, tobTxs) +} + +func TestBlockAssemblerWithNoTobTxs(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) + time := preMergeBlocks[len(preMergeBlocks)-1].Time() + 5 + genesis.Config.ShanghaiTime = &time + + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, false) + parent := preMergeBlocks[len(preMergeBlocks)-1] + + api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr) + + statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) + nonce := statedb.GetNonce(testAddr) + + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + robTx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x18}, big.NewInt(30), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testKey) + robTx2, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x19}, big.NewInt(40), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testKey) + + ethservice.TxPool().AddLocal(robTx1) + ethservice.TxPool().AddLocal(robTx2) + + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + { + Index: 1, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + } + withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ + Timestamp: parent.Time() + 5, + Withdrawals: withdrawals, + SuggestedFeeRecipient: testValidatorAddr, + }) + fmt.Printf("DEBUG: Exec data random is %x\n", execData.Random) + require.NoError(t, err) + require.EqualValues(t, len(execData.Withdrawals), 2) + require.EqualValues(t, len(execData.Transactions), 2) + + payload, err := ExecutableDataToExecutionPayloadV2(execData) + require.NoError(t, err) + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], testValidatorAddr.Bytes()) + + blockRequest := &BuilderBlockValidationRequestV2{ + SubmitBlockRequest: capellaapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + // This value is actual profit + 1, validation should fail + Value: uint256.NewInt(149842511727213), + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + WithdrawalsRoot: withdrawalsRoot, + } + + assemblyRequest := BlockAssemblerRequest{ + TobTxs: bellatrixUtil.ExecutionPayloadTransactions{ + Transactions: []bellatrix.Transaction{}, + }, + RobPayload: blockRequest.SubmitBlockRequest, + RegisteredGasLimit: execData.GasLimit, + } + + block, err := api.BlockAssembler(&assemblyRequest) + require.NoError(t, err) + require.Equal(t, len(block.Transactions), 2) + + // check rob txs + actualRobTx1 := block.Transactions[0] + actualRobTx2 := block.Transactions[1] + execDataTx1 := bellatrix.Transaction(execData.Transactions[0]) + execDataTx2 := bellatrix.Transaction(execData.Transactions[1]) + + require.Equal(t, execDataTx1, actualRobTx1) + require.Equal(t, execDataTx2, actualRobTx2) +} + +func TestBlockAssemblerWithTobAndRobTxs(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) + time := preMergeBlocks[len(preMergeBlocks)-1].Time() + 5 + genesis.Config.ShanghaiTime = &time + + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, false) + parent := preMergeBlocks[len(preMergeBlocks)-1] + + api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr) + + statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) + nonce := statedb.GetNonce(testAddr) + searcherNonce := statedb.GetNonce(testSearcherAddr) + + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + tobTx1, _ := types.SignTx(types.NewTransaction(searcherNonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testSearcherKey) + tobTx2, _ := types.SignTx(types.NewTransaction(searcherNonce+1, common.Address{0x17}, big.NewInt(20), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testSearcherKey) + + tobTx1Bytes, err := tobTx1.MarshalBinary() + require.NoError(t, err) + tobTx2Bytes, err := tobTx2.MarshalBinary() + require.NoError(t, err) + + tobTxs := bellatrixUtil.ExecutionPayloadTransactions{ + Transactions: []bellatrix.Transaction{tobTx1Bytes, tobTx2Bytes}, + } + + robTx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x18}, big.NewInt(30), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testKey) + robTx2, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x19}, big.NewInt(40), 21000, big.NewInt(2*params.InitialBaseFee), nil), signer, testKey) + + ethservice.TxPool().AddLocal(robTx1) + ethservice.TxPool().AddLocal(robTx2) + + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + { + Index: 1, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + } + withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ + Timestamp: parent.Time() + 5, + Withdrawals: withdrawals, + SuggestedFeeRecipient: testValidatorAddr, + }) + fmt.Printf("DEBUG: Exec data random is %x\n", execData.Random) + require.NoError(t, err) + require.EqualValues(t, len(execData.Withdrawals), 2) + require.EqualValues(t, len(execData.Transactions), 2) + + payload, err := ExecutableDataToExecutionPayloadV2(execData) + require.NoError(t, err) + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], testValidatorAddr.Bytes()) + + blockRequest := &BuilderBlockValidationRequestV2{ + SubmitBlockRequest: capellaapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + // This value is actual profit + 1, validation should fail + Value: uint256.NewInt(149842511727213), + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + WithdrawalsRoot: withdrawalsRoot, + } + + assemblyRequest := BlockAssemblerRequest{ + TobTxs: tobTxs, + RobPayload: blockRequest.SubmitBlockRequest, + RegisteredGasLimit: execData.GasLimit, + } + + block, err := api.BlockAssembler(&assemblyRequest) + require.NoError(t, err) + require.Equal(t, len(block.Transactions), 4) + + // check tob txs + actualTobTx1 := block.Transactions[0] + actualTobTx2 := block.Transactions[1] + require.Equal(t, bellatrixUtil.ExecutionPayloadTransactions{Transactions: []bellatrix.Transaction{actualTobTx1, actualTobTx2}}, tobTxs) + + // check rob txs + actualRobTx1 := block.Transactions[2] + actualRobTx2 := block.Transactions[3] + execDataTx1 := bellatrix.Transaction(execData.Transactions[0]) + execDataTx2 := bellatrix.Transaction(execData.Transactions[1]) + + require.Equal(t, execDataTx1, actualRobTx1) + require.Equal(t, execDataTx2, actualRobTx2) +} + func TestValidateBuilderSubmissionV2(t *testing.T) { genesis, preMergeBlocks := generatePreMergeChain(20) os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) @@ -317,7 +627,7 @@ func generatePreMergeChain(n int) (*core.Genesis, []*types.Block) { config := params.AllEthashProtocolChanges genesis := &core.Genesis{ Config: config, - Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}, testValidatorAddr: {Balance: testBalance}, testBuilderAddr: {Balance: testBalance}}, + Alloc: core.GenesisAlloc{testSearcherAddr: {Balance: testBalance}, testAddr: {Balance: testBalance}, testValidatorAddr: {Balance: testBalance}, testBuilderAddr: {Balance: testBalance}}, ExtraData: []byte("test genesis"), Timestamp: 9000, BaseFee: big.NewInt(params.InitialBaseFee), diff --git a/miner/contract_simulator_test.go b/miner/contract_simulator_test.go index 973053fb83..a917325f06 100644 --- a/miner/contract_simulator_test.go +++ b/miner/contract_simulator_test.go @@ -201,7 +201,7 @@ func TestSimulatorState(t *testing.T) { require.NoError(t, err) } - block, _, err := w.getSealingBlock(b.chain.CurrentBlock().Hash(), b.chain.CurrentHeader().Time+12, testAddress1, 0, common.Hash{}, nil, false, nil) + block, _, err := w.getSealingBlock(b.chain.CurrentBlock().Hash(), b.chain.CurrentHeader().Time+12, testAddress1, 0, common.Hash{}, nil, false, nil, AssemblerTxLists{}) require.NoError(t, err) require.NotNil(t, block) if requireTx != -1 { diff --git a/miner/miner.go b/miner/miner.go index a01aa5d88f..ecd5c0855b 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -308,6 +308,11 @@ func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscript // Accepts the block, time at which orders were taken, bundles which were used to build the block and all bundles that were considered for the block type BlockHookFn = func(*types.Block, *big.Int, time.Time, []types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle) +// BuildPayload builds the payload according to the provided parameters. +func (miner *Miner) PayloadAssembler(args *BuildPayloadArgs) (*Payload, error) { + return miner.worker.payloadAssembler(args) +} + // BuildPayload builds the payload according to the provided parameters. func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { return miner.worker.buildPayload(args) diff --git a/miner/multi_worker.go b/miner/multi_worker.go index 93cb8aadae..44ff09acf9 100644 --- a/miner/multi_worker.go +++ b/miner/multi_worker.go @@ -86,6 +86,52 @@ func (w *multiWorker) disablePreseal() { } } +// buildPayload builds the payload according to the provided parameters. +func (w *multiWorker) payloadAssembler(args *BuildPayloadArgs) (*Payload, error) { + // Build the initial version with no transaction included. It should be fast + // enough to run. The empty payload can at least make sure there is something + // to deliver for not missing slot. + empty, _, err := w.regularWorker.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, args.BlockHook, args.AssemblerTxs) + if err != nil { + return nil, err + } + // Construct a payload object for return. + payload := newPayload(empty, args.Id()) + + // Spin up a routine for updating the payload in background. This strategy + // can maximum the revenue for including transactions with highest fee. + go func() { + // Setup the timer for re-building the payload. The initial clock is kept + // for triggering process immediately. + timer := time.NewTimer(0) + defer timer.Stop() + + // Setup the timer for terminating the process if SECONDS_PER_SLOT (12s in + // the Mainnet configuration) have passed since the point in time identified + // by the timestamp parameter. + endTimer := time.NewTimer(time.Second * 12) + + for { + select { + case <-timer.C: + start := time.Now() + block, fees, err := w.regularWorker.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook, args.AssemblerTxs) + if err == nil { + payload.update(block, fees, time.Since(start)) + } + timer.Reset(w.regularWorker.recommit) + case <-payload.stop: + log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery") + return + case <-endTimer.C: + log.Info("Stopping work on payload", "id", payload.id, "reason", "timeout") + return + } + } + }() + return payload, nil +} + func (w *multiWorker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { // Build the initial version with no transaction included. It should be fast // enough to run. The empty payload can at least make sure there is something @@ -93,7 +139,7 @@ func (w *multiWorker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { var empty *types.Block for _, worker := range w.workers { var err error - empty, _, err = worker.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, nil) + empty, _, err = worker.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, nil, AssemblerTxLists{}) if err != nil { log.Error("could not start async block construction", "isFlashbotsWorker", worker.flashbots.isFlashbots, "#bundles", worker.flashbots.maxMergedBundles) continue @@ -122,7 +168,7 @@ func (w *multiWorker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { go func(w *worker) { // Update routine done elsewhere! start := time.Now() - block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook) + block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook, AssemblerTxLists{}) if err == nil { workerPayload.update(block, fees, time.Since(start)) } else { diff --git a/miner/payload_building.go b/miner/payload_building.go index 5ed15c1dd3..9cc89aabf0 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -31,6 +31,11 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +type AssemblerTxLists struct { + TobTxs *types.Transactions + RobTxs *types.Transactions +} + // BuildPayloadArgs contains the provided parameters for building payload. // Check engine-api specification for more details. // https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md#payloadattributesv1 @@ -42,6 +47,7 @@ type BuildPayloadArgs struct { Random common.Hash // The provided randomness value Withdrawals types.Withdrawals // The provided withdrawals BlockHook BlockHookFn + AssemblerTxs AssemblerTxLists // The txs to be assembled } // Id computes an 8-byte identifier by hashing the components of the payload arguments. @@ -221,7 +227,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { // Build the initial version with no transaction included. It should be fast // enough to run. The empty payload can at least make sure there is something // to deliver for not missing slot. - empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, args.BlockHook) + empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, args.BlockHook, AssemblerTxLists{}) if err != nil { return nil, err } @@ -245,7 +251,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { select { case <-timer.C: start := time.Now() - block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook) + block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook, AssemblerTxLists{}) if err == nil { payload.update(block, fees, time.Since(start)) } diff --git a/miner/worker.go b/miner/worker.go index d9fb0e465b..86bead5597 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -95,13 +95,15 @@ var ( type environment struct { signer types.Signer - state *state.StateDB // apply state changes here - ancestors mapset.Set[common.Hash] // ancestor set (used for checking uncle parent validity) - family mapset.Set[common.Hash] // family set (used for checking uncle invalidity) - tcount int // tx count in cycle - gasPool *core.GasPool // available gas used to pack transactions - coinbase common.Address - profit *big.Int + state *state.StateDB // apply state changes here + ancestors mapset.Set[common.Hash] // ancestor set (used for checking uncle parent validity) + family mapset.Set[common.Hash] // family set (used for checking uncle invalidity) + tcount int // tx count in cycle + gasPool *core.GasPool // available gas used to pack transactions + coinbase common.Address + profit *big.Int + isAssembler bool + assemblerTxs AssemblerTxLists header *types.Header txs []*types.Transaction @@ -883,7 +885,7 @@ func (w *worker) resultLoop() { } // makeEnv creates a new environment for the sealing block. -func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address) (*environment, error) { +func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, assemblerTxs AssemblerTxLists) (*environment, error) { // Retrieve the parent state to execute on top and start a prefetcher for // the miner to speed block sealing up a bit. state, err := w.chain.StateAt(parent.Root) @@ -894,14 +896,18 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co // Note the passed coinbase may be different with header.Coinbase. env := &environment{ - signer: types.MakeSigner(w.chainConfig, header.Number), - state: state, - coinbase: coinbase, - ancestors: mapset.NewSet[common.Hash](), - family: mapset.NewSet[common.Hash](), - header: header, - uncles: make(map[common.Hash]*types.Header), - profit: new(big.Int), + signer: types.MakeSigner(w.chainConfig, header.Number), + state: state, + coinbase: coinbase, + ancestors: mapset.NewSet[common.Hash](), + family: mapset.NewSet[common.Hash](), + header: header, + uncles: make(map[common.Hash]*types.Header), + profit: new(big.Int), + assemblerTxs: assemblerTxs, + } + if (assemblerTxs.TobTxs != nil && assemblerTxs.TobTxs.Len() > 0) || (assemblerTxs.RobTxs != nil && assemblerTxs.RobTxs.Len() > 0) { + env.isAssembler = true } // when 08 is processed ancestors contain 07 (quick block) for _, ancestor := range w.chain.GetBlocksFromHash(parent.Hash(), 7) { @@ -1095,6 +1101,95 @@ func (w *worker) commitBundle(env *environment, txs types.Transactions, interrup return nil } +func (w *worker) commitTransactionWrapper(env *environment, interrupt *int32, tx *types.Transaction, coalescedLogs []*types.Log) error { + // Check interruption signal and abort building if it's fired. + if interrupt != nil { + if signal := atomic.LoadInt32(interrupt); signal != commitInterruptNone { + return signalToErr(signal) + } + } + // If we don't have enough gas for any further transactions then we're done. + if env.gasPool.Gas() < params.TxGas { + log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas) + return nil + } + + // Error may be ignored here. The error has already been checked + // during transaction acceptance is the transaction pool. + from, _ := types.Sender(env.signer, tx) + logs, err := w.commitTransaction(env, tx) + switch { + case errors.Is(err, core.ErrGasLimitReached): + // Pop the current out-of-gas transaction without shifting in the next from the account + log.Trace("Gas limit exceeded for current block", "sender", from) + + case errors.Is(err, core.ErrNonceTooLow): + // New head notification data race between the transaction pool and miner, shift + log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) + + case errors.Is(err, core.ErrNonceTooHigh): + // Reorg notification data race between the transaction pool and miner, skip account = + log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce()) + + case errors.Is(err, nil): + // Everything ok, collect the logs and shift in the next transaction from the same account + coalescedLogs = append(coalescedLogs, logs...) + env.tcount++ + + case errors.Is(err, types.ErrTxTypeNotSupported): + // Pop the unsupported transaction without shifting in the next from the account + log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type()) + + default: + // Strange error, discard the transaction and get the next in line (note, the + // nonce-too-high clause will prevent us from executing in vain). + log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) + } + + return nil +} + +func (w *worker) commitAssemblyTransactions(env *environment, assemblerTxs AssemblerTxLists, interrupt *int32) error { + gasLimit := env.header.GasLimit + if env.gasPool == nil { + env.gasPool = new(core.GasPool).AddGas(gasLimit) + } + var coalescedLogs []*types.Log + + // first go thru TOB txs + for _, tx := range *assemblerTxs.TobTxs { + err := w.commitTransactionWrapper(env, interrupt, tx, coalescedLogs) + if err != nil { + return err + } + } + + // now go thru ROB txs + for _, tx := range *assemblerTxs.RobTxs { + err := w.commitTransactionWrapper(env, interrupt, tx, coalescedLogs) + if err != nil { + return err + } + } + + if !w.isRunning() && len(coalescedLogs) > 0 { + // We don't push the pendingLogsEvent while we are sealing. The reason is that + // when we are sealing, the worker will regenerate a sealing block every 3 seconds. + // In order to avoid pushing the repeated pendingLog, we disable the pending log pushing. + + // make a copy, the state caches the logs and these logs get "upgraded" from pending to mined + // logs by filling in the block hash when the block was mined by the local miner. This can + // cause a race condition if a log was "upgraded" before the PendingLogsEvent is processed. + cpy := make([]*types.Log, len(coalescedLogs)) + for i, l := range coalescedLogs { + cpy[i] = new(types.Log) + *cpy[i] = *l + } + w.pendingLogsFeed.Send(cpy) + } + return nil +} + func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByPriceAndNonce, interrupt *int32) error { gasLimit := env.header.GasLimit if env.gasPool == nil { @@ -1191,16 +1286,17 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP // generateParams wraps various of settings for generating sealing task. type generateParams struct { - timestamp uint64 // The timstamp for sealing task - forceTime bool // Flag whether the given timestamp is immutable or not - parentHash common.Hash // Parent block hash, empty means the latest chain head - coinbase common.Address // The fee recipient address for including transaction - gasLimit uint64 // The validator's requested gas limit target - random common.Hash // The randomness generated by beacon chain, empty before the merge - withdrawals types.Withdrawals // List of withdrawals to include in block. - noUncle bool // Flag whether the uncle block inclusion is allowed - noTxs bool // Flag whether an empty block without any transaction is expected - onBlock BlockHookFn // Callback to call for each produced block + timestamp uint64 // The timstamp for sealing task + forceTime bool // Flag whether the given timestamp is immutable or not + parentHash common.Hash // Parent block hash, empty means the latest chain head + coinbase common.Address // The fee recipient address for including transaction + gasLimit uint64 // The validator's requested gas limit target + random common.Hash // The randomness generated by beacon chain, empty before the merge + withdrawals types.Withdrawals // List of withdrawals to include in block. + noUncle bool // Flag whether the uncle block inclusion is allowed + noTxs bool // Flag whether an empty block without any transaction is expected + onBlock BlockHookFn // Callback to call for each produced block + assemblerTxs AssemblerTxLists // The transactions that the assembler wants to make a block out of } func doPrepareHeader(genParams *generateParams, chain *core.BlockChain, config *Config, chainConfig *params.ChainConfig, extra []byte, engine consensus.Engine) (*types.Header, *types.Header, error) { @@ -1277,7 +1373,7 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { // Could potentially happen if starting to mine in an odd state. // Note genParams.coinbase can be different with header.Coinbase // since clique algorithm can modify the coinbase field in header. - env, err := w.makeEnv(parent, header, genParams.coinbase) + env, err := w.makeEnv(parent, header, genParams.coinbase, genParams.assemblerTxs) if err != nil { log.Error("Failed to create sealing context", "err", err) return nil, err @@ -1311,6 +1407,10 @@ func (w *worker) fillTransactionsSelectAlgo(interrupt *int32, env *environment) mempoolTxHashes map[common.Hash]struct{} err error ) + if env.isAssembler { + blockBundles, allBundles, mempoolTxHashes, err = w.fillAssemblerTransactions(interrupt, env) + return blockBundles, allBundles, usedSbundles, mempoolTxHashes, err + } switch w.flashbots.algoType { case ALGO_GREEDY, ALGO_GREEDY_BUCKETS: blockBundles, allBundles, usedSbundles, mempoolTxHashes, err = w.fillTransactionsAlgoWorker(interrupt, env) @@ -1322,6 +1422,32 @@ func (w *worker) fillTransactionsSelectAlgo(interrupt *int32, env *environment) return blockBundles, allBundles, usedSbundles, mempoolTxHashes, err } +// fillTransactions retrieves the pending transactions from the txpool and fills them +// into the given sealing block. The transaction selection and ordering strategy can +// be customized with the plugin in the future. +// Returns error if any, otherwise the bundles that made it into the block and all bundles that passed simulation +func (w *worker) fillAssemblerTransactions(interrupt *int32, env *environment) ([]types.SimulatedBundle, []types.SimulatedBundle, map[common.Hash]struct{}, error) { + + assemblerTxs := env.assemblerTxs + totalTxsToAssemble := assemblerTxs.RobTxs.Len() + assemblerTxs.TobTxs.Len() + + mempoolHashes := make(map[common.Hash]struct{}, totalTxsToAssemble) + for _, tx := range *assemblerTxs.TobTxs { + mempoolHashes[tx.Hash()] = struct{}{} + } + for _, tx := range *assemblerTxs.RobTxs { + mempoolHashes[tx.Hash()] = struct{}{} + } + + if totalTxsToAssemble > 0 { + if err := w.commitAssemblyTransactions(env, env.assemblerTxs, interrupt); err != nil { + return nil, nil, nil, err + } + } + + return []types.SimulatedBundle{}, []types.SimulatedBundle{}, mempoolHashes, nil +} + // fillTransactions retrieves the pending transactions from the txpool and fills them // into the given sealing block. The transaction selection and ordering strategy can // be customized with the plugin in the future. @@ -1346,6 +1472,7 @@ func (w *worker) fillTransactions(interrupt *int32, env *environment) ([]types.S var blockBundles []types.SimulatedBundle var allBundles []types.SimulatedBundle + if w.flashbots.isFlashbots { bundles, ccBundleCh := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time) bundles = append(bundles, <-ccBundleCh...) @@ -1528,8 +1655,10 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e gasUsedGauge.Update(int64(block.GasUsed())) transactionNumGauge.Update(int64(len(env.txs))) } - if params.onBlock != nil { - go params.onBlock(block, profit, orderCloseTime, blockBundles, allBundles, usedSbundles) + if !work.isAssembler { + if params.onBlock != nil { + go params.onBlock(block, profit, orderCloseTime, blockBundles, allBundles, usedSbundles) + } } return block, profit, nil @@ -1539,9 +1668,13 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e return finalizeFn(work, time.Now(), nil, nil, nil, true) } - paymentTxReserve, err := w.proposerTxPrepare(work, &validatorCoinbase) - if err != nil { - return nil, nil, err + var paymentTxReserve *proposerTxReservation + // there won't be any additional payments for a block assembled by the assembler + if !work.isAssembler { + paymentTxReserve, err = w.proposerTxPrepare(work, &validatorCoinbase) + if err != nil { + return nil, nil, err + } } orderCloseTime := time.Now() @@ -1563,6 +1696,8 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e } } + // TODO - bchain - maybe we can avoid this check for the assembler since we can assume that the ROB block + // should be valid err = VerifyBundlesAtomicity(work, blockBundles, allBundles, usedSbundles, mempoolTxHashes) if err != nil { log.Error("Bundle invariant is violated for built block", "block", work.header.Number, "err", err) @@ -1574,9 +1709,11 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e return finalizeFn(work, orderCloseTime, blockBundles, allBundles, usedSbundles, true) } - err = w.proposerTxCommit(work, &validatorCoinbase, paymentTxReserve) - if err != nil { - return nil, nil, err + if !work.isAssembler { + err = w.proposerTxCommit(work, &validatorCoinbase, paymentTxReserve) + if err != nil { + return nil, nil, err + } } return finalizeFn(work, orderCloseTime, blockBundles, allBundles, usedSbundles, false) @@ -1596,9 +1733,12 @@ func (w *worker) finalizeBlock(work *environment, withdrawals types.Withdrawals, return block, big.NewInt(0), nil } - blockProfit, err := w.checkProposerPayment(work, validatorCoinbase) - if err != nil { - return nil, nil, err + blockProfit := big.NewInt(0) + if !work.isAssembler { + blockProfit, err = w.checkProposerPayment(work, validatorCoinbase) + if err != nil { + return nil, nil, err + } } return block, blockProfit, nil @@ -1731,19 +1871,20 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti // getSealingBlock generates the sealing block based on the given parameters. // The generation result will be passed back via the given channel no matter // the generation itself succeeds or not. -func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, withdrawals types.Withdrawals, noTxs bool, blockHook BlockHookFn) (*types.Block, *big.Int, error) { +func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, withdrawals types.Withdrawals, noTxs bool, blockHook BlockHookFn, assemblerTxs AssemblerTxLists) (*types.Block, *big.Int, error) { req := &getWorkReq{ params: &generateParams{ - timestamp: timestamp, - forceTime: true, - parentHash: parent, - coinbase: coinbase, - gasLimit: gasLimit, - random: random, - withdrawals: withdrawals, - noUncle: true, - noTxs: noTxs, - onBlock: blockHook, + timestamp: timestamp, + forceTime: true, + parentHash: parent, + coinbase: coinbase, + gasLimit: gasLimit, + random: random, + withdrawals: withdrawals, + noUncle: true, + noTxs: noTxs, + onBlock: blockHook, + assemblerTxs: assemblerTxs, }, result: make(chan *newPayloadResult, 1), } diff --git a/miner/worker_test.go b/miner/worker_test.go index bc57593f38..93b746920a 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -658,7 +658,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co // This API should work even when the automatic sealing is not enabled for _, c := range cases { - block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, 0, c.random, nil, true, nil) + block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, 0, c.random, nil, true, nil, AssemblerTxLists{}) if c.expectErr { if err == nil { t.Error("Expect error but get nil") @@ -674,7 +674,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co // This API should work even when the automatic sealing is enabled w.start() for _, c := range cases { - block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, 0, c.random, nil, false, nil) + block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, 0, c.random, nil, false, nil, AssemblerTxLists{}) if c.expectErr { if err == nil { t.Error("Expect error but get nil") @@ -822,7 +822,7 @@ func testBundles(t *testing.T) { require.NoError(t, err) } - block, _, err := w.getSealingBlock(w.chain.CurrentBlock().Hash(), w.chain.CurrentHeader().Time+12, testUserAddress, 0, common.Hash{}, nil, false, nil) + block, _, err := w.getSealingBlock(w.chain.CurrentBlock().Hash(), w.chain.CurrentHeader().Time+12, testUserAddress, 0, common.Hash{}, nil, false, nil, AssemblerTxLists{}) require.NoError(t, err) state, err := w.chain.State() @@ -837,3 +837,120 @@ func testBundles(t *testing.T) { t.Log("Balances", balancePre, balancePost) } } + +func TestBlockAssembly(t *testing.T) { + db := rawdb.NewMemoryDatabase() + chainConfig := params.AllEthashProtocolChanges + engine := ethash.NewFaker() + + chainConfig.LondonBlock = big.NewInt(0) + + genesisAlloc := core.GenesisAlloc{testBankAddress: {Balance: testBankFunds}} + + nExtraKeys := 5 + extraKeys := make([]*ecdsa.PrivateKey, nExtraKeys) + for i := 0; i < nExtraKeys; i++ { + pk, _ := crypto.GenerateKey() + address := crypto.PubkeyToAddress(pk.PublicKey) + extraKeys[i] = pk + genesisAlloc[address] = core.GenesisAccount{Balance: testBankFunds} + } + + nSearchers := 5 + searcherPrivateKeys := make([]*ecdsa.PrivateKey, nSearchers) + for i := 0; i < nSearchers; i++ { + pk, _ := crypto.GenerateKey() + address := crypto.PubkeyToAddress(pk.PublicKey) + searcherPrivateKeys[i] = pk + genesisAlloc[address] = core.GenesisAccount{Balance: testBankFunds} + } + + for _, address := range []common.Address{testAddress1, testAddress2, testAddress3} { + genesisAlloc[address] = core.GenesisAccount{Balance: testBankFunds} + } + + w, b := newTestWorker(t, chainConfig, engine, db, genesisAlloc, 0) + defer w.close() + + // Ignore empty commit here for less noise. + w.skipSealHook = func(task *task) bool { + return len(task.receipts) == 0 + } + + // Test 1 + tobTxs := []*types.Transaction{ + b.newRandomTx(false, testBankAddress, 1e15, testAddress1Key, 0, big.NewInt(100*params.InitialBaseFee)), + b.newRandomTx(false, testBankAddress, 1e15, testAddress2Key, 0, big.NewInt(110*params.InitialBaseFee)), + b.newRandomTx(false, testBankAddress, 1e15, testAddress3Key, 0, big.NewInt(120*params.InitialBaseFee)), + } + + robTxs := make([]*types.Transaction, len(searcherPrivateKeys)) + for _, pk := range searcherPrivateKeys { + robTxs = append(robTxs, b.newRandomTx(false, testBankAddress, 1, pk, 0, big.NewInt(150*params.InitialBaseFee))) + } + finalRobTxs := types.Transactions{} + for _, tx := range robTxs { + if tx != nil { + finalRobTxs = append(finalRobTxs, tx) + } + } + + aTxs := AssemblerTxLists{ + TobTxs: tobTxs, + RobTxs: &finalRobTxs, + } + + block, _, err := w.getSealingBlock(w.chain.CurrentBlock().Hash(), w.chain.CurrentHeader().Time+12, testUserAddress, 0, common.Hash{}, nil, false, nil, aTxs) + require.NoError(t, err) + txs := block.Transactions() + mergedTobAndRobTxs := types.Transactions{} + for _, tx := range aTxs.TobTxs { + mergedTobAndRobTxs = append(mergedTobAndRobTxs, tx) + } + for _, tx := range *aTxs.RobTxs { + mergedTobAndRobTxs = append(mergedTobAndRobTxs, tx) + } + require.Equal(t, txs, mergedTobAndRobTxs) + + // Test 2 - No TOB txs but only ROB txs + robTxs = make([]*types.Transaction, len(searcherPrivateKeys)) + for _, pk := range searcherPrivateKeys { + robTxs = append(robTxs, b.newRandomTx(false, testBankAddress, 1, pk, 0, big.NewInt(150*params.InitialBaseFee))) + } + finalRobTxs = types.Transactions{} + for _, tx := range robTxs { + if tx != nil { + finalRobTxs = append(finalRobTxs, tx) + } + } + + aTxs = AssemblerTxLists{ + TobTxs: []*types.Transaction{}, + RobTxs: &finalRobTxs, + } + block, _, err = w.getSealingBlock(w.chain.CurrentBlock().Hash(), w.chain.CurrentHeader().Time+12, testUserAddress, 0, common.Hash{}, nil, false, nil, aTxs) + require.NoError(t, err) + txs = block.Transactions() + require.Equal(t, txs, finalRobTxs) + + // Test 3 - no ROB txs but only TOB txs + tobTxs = []*types.Transaction{ + b.newRandomTx(false, testBankAddress, 1e15, testAddress1Key, 0, big.NewInt(100*params.InitialBaseFee)), + b.newRandomTx(false, testBankAddress, 1e15, testAddress2Key, 0, big.NewInt(110*params.InitialBaseFee)), + b.newRandomTx(false, testBankAddress, 1e15, testAddress3Key, 0, big.NewInt(120*params.InitialBaseFee)), + } + + aTxs = AssemblerTxLists{ + TobTxs: tobTxs, + RobTxs: &types.Transactions{}, + } + block, _, err = w.getSealingBlock(w.chain.CurrentBlock().Hash(), w.chain.CurrentHeader().Time+12, testUserAddress, 0, common.Hash{}, nil, false, nil, aTxs) + require.NoError(t, err) + txs = block.Transactions() + mergedTobTxs := types.Transactions{} + for _, tx := range aTxs.TobTxs { + mergedTobTxs = append(mergedTobTxs, tx) + } + require.Equal(t, txs, mergedTobTxs) + +}