diff --git a/cl/beacon/builder/types.go b/cl/beacon/builder/types.go index 8f5058b3507..2dde59d3b1c 100644 --- a/cl/beacon/builder/types.go +++ b/cl/beacon/builder/types.go @@ -48,10 +48,14 @@ func (h ExecutionHeader) BlockValue() *big.Int { if h.Data.Message.Value == "" { return nil } - //blockValue := binary.LittleEndian.Uint64([]byte(h.Data.Message.Value)) blockValue, ok := new(big.Int).SetString(h.Data.Message.Value, 10) if !ok { log.Warn("cannot parse block value", "value", h.Data.Message.Value) + return nil + } + if blockValue.Sign() < 0 || blockValue.BitLen() > 256 { + log.Warn("builder block value outside uint256 range", "value", h.Data.Message.Value) + return nil } return blockValue } diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index 4ec8d9073a9..bc37e69509f 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -675,16 +675,14 @@ func (a *ApiHandler) GetEthV3ValidatorBlock( startConsensusProcessing := time.Now() - blockBuldingMachine := ð2.Impl{} - blockBuldingMachine.BlockRewardsCollector = ð2.BlockRewardsCollector{} - // do state transition - if err := machine.ProcessBlock(blockBuldingMachine, baseState, block.ToGeneric()); err != nil { + postState, blockBuildingMachine, err := a.processProducedBlock(baseState, block, builderBoostFactor) + if err != nil { log.Warn("Failed to process execution block", "err", err, "slot", targetSlot) return nil, err } log.Info("[Beacon API] Built block consensus-state", "slot", targetSlot, "duration", time.Since(startConsensusProcessing)) startConsensusProcessing = time.Now() - block.StateRoot, err = baseState.HashSSZ() + block.StateRoot, err = postState.HashSSZ() if err != nil { log.Warn("Failed to get state root", "err", err) return nil, err @@ -696,15 +694,14 @@ func (a *ApiHandler) GetEthV3ValidatorBlock( "proposerIndex", block.ProposerIndex, "slot", targetSlot, "state_root", block.StateRoot, - "execution_value", block.GetExecutionValue().Uint64(), + "execution_value", block.GetExecutionValue().String(), "version", block.Version(), "blinded", block.IsBlinded(), "took", time.Since(start), ) - // todo: consensusValue - rewardsCollector := blockBuldingMachine.BlockRewardsCollector - consensusValue := rewardsCollector.Attestations + rewardsCollector.ProposerSlashings + rewardsCollector.AttesterSlashings + rewardsCollector.SyncAggregate + rewardsCollector := blockBuildingMachine.BlockRewardsCollector + consensusValue := consensusBlockValueWei(rewardsCollector) var resp *beaconhttp.BeaconResponse switch { case block.IsBlinded(): @@ -718,8 +715,8 @@ func (a *ApiHandler) GetEthV3ValidatorBlock( resp = newBeaconResponse(block.ToExecution()) } resp = resp.WithVersion(block.Version()).With("execution_payload_blinded", block.IsBlinded()). - With("execution_payload_value", strconv.FormatUint(block.GetExecutionValue().Uint64(), 10)). - With("consensus_block_value", strconv.FormatUint(consensusValue, 10)) + With("execution_payload_value", block.GetExecutionValue().String()). + With("consensus_block_value", consensusValue.String()) executionPayloadIncluded := false // [New in Gloas:EIP7732] For self-build blocks, compute the unsigned ExecutionPayloadEnvelope @@ -771,7 +768,7 @@ func (a *ApiHandler) GetEthV3ValidatorBlock( block.Version(), block.IsBlinded(), executionPayloadIncluded, - block.GetExecutionValue().Uint64(), + block.GetExecutionValue(), consensusValue, ) @@ -789,12 +786,13 @@ func (a *ApiHandler) produceBlock( graffiti common.Hash, ) (block *cltypes.BlindOrExecutionBeaconBlock, err error) { defer func() { reportProductionFailure(err, targetSlot) }() + stateVersion := a.beaconChainCfg.GetCurrentStateVersion(targetSlot / a.beaconChainCfg.SlotsPerEpoch) var wg sync.WaitGroup // produce beacon body var ( beaconBody *cltypes.BeaconBody - localExecValue uint64 + localExecValue *big.Int localErr error blobs []*cltypes.Blob kzgProofs []common.Bytes48 @@ -843,7 +841,7 @@ func (a *ApiHandler) produceBlock( defer func() { a.logger.Debug("MevBoost", "slot", targetSlot, "duration", time.Since(start)) }() - if a.routerCfg.Builder && a.builderClient != nil { + if shouldRequestBuilderHeader(stateVersion, a.routerCfg.Builder, a.builderClient != nil) { builderHeader, builderErr = a.getBuilderPayload(ctx, baseState, targetSlot) if builderErr != nil && !errors.Is(builderErr, errBuilderNotEnabled) { log.Warn("Failed to get builder payload", "err", builderErr) @@ -872,55 +870,29 @@ func (a *ApiHandler) produceBlock( ParentRoot: baseBlockRoot, Cfg: a.beaconChainCfg, } - stateVersion := a.beaconChainCfg.GetCurrentStateVersion(targetSlot / a.beaconChainCfg.SlotsPerEpoch) - if !a.routerCfg.Builder || builderErr != nil || stateVersion.AfterOrEqual(clparams.GloasVersion) { - // directly return the block if: - // 1. builder is not enabled - // 2. failed to get builder payload - // 3. GLOAS: MEV-Boost blinded blocks not supported; builders use ePBS gossip bids - - // GLOAS: check epbsPool for an external builder bid that beats the local value. - if stateVersion.AfterOrEqual(clparams.GloasVersion) && a.epbsPool != nil { - selfBid := beaconBody.SignedExecutionPayloadBid.Message - bidKey := pool.HighestBidKey{ - Slot: targetSlot, - ParentBlockHash: selfBid.ParentBlockHash, - ParentBlockRoot: selfBid.ParentBlockRoot, - } - if externalBid, found := a.epbsPool.HighestBids.Get(bidKey); found && - externalBid != nil && externalBid.Message != nil && - externalBid.Message.Value > localExecValue { - log.Info("GLOAS: selected external builder bid over self-build", - "slot", targetSlot, - "builderIndex", externalBid.Message.BuilderIndex, - "bidValue", externalBid.Message.Value, - "localValue", localExecValue) - beaconBody.SignedExecutionPayloadBid = externalBid - localExecValue = externalBid.Message.Value - } - } - + // A nil header covers disabled or unavailable legacy builders. Gloas also skips + // this request because its external bids are selected from ePBS after assembly. + if builderHeader == nil { block.BeaconBody = beaconBody block.Blobs = blobs block.KzgProofs = kzgProofs - block.ExecutionValue = new(big.Int).SetUint64(localExecValue) + + block.ExecutionValue = localExecValue return block, nil } // determine whether to use local execution node or builder // if exec_node_payload_value >= builder_boost_factor * (builder_payload_value // 100), then return a full (unblinded) block containing the execution node payload. // otherwise, return a blinded block containing the builder payload header. - execValue := new(big.Int).SetUint64(localExecValue) builderValue := builderHeader.BlockValue() - boostFactorBig := new(big.Int).SetUint64(boostFactor) - useLocalExec := new(big.Int).Mul(execValue, big.NewInt(100)).Cmp(new(big.Int).Mul(builderValue, boostFactorBig)) >= 0 - log.Info("Check mev bid", "useLocalExec", useLocalExec, "execValue", execValue, "builderValue", builderValue, "boostFactor", boostFactor, "targetSlot", targetSlot) + useLocalExec := preferLocalExecutionValue(localExecValue, builderValue, boostFactor) + log.Info("Check mev bid", "useLocalExec", useLocalExec, "execValue", localExecValue, "builderValue", builderValue, "boostFactor", boostFactor, "targetSlot", targetSlot) if useLocalExec { block.BeaconBody = beaconBody block.Blobs = blobs block.KzgProofs = kzgProofs - block.ExecutionValue = execValue + block.ExecutionValue = localExecValue } else { // prepare blinded block blindedBody, err := beaconBody.Blinded() @@ -945,6 +917,140 @@ func (a *ApiHandler) produceBlock( return block, nil } +func shouldRequestBuilderHeader(stateVersion clparams.StateVersion, builderEnabled, clientAvailable bool) bool { + // Gloas receives external builder bids through ePBS gossip instead of the legacy Builder API. + return stateVersion.Before(clparams.GloasVersion) && builderEnabled && clientAvailable +} + +func preferLocalExecutionValue(localValueWei, builderValueWei *big.Int, boostFactor uint64) bool { + if builderValueWei == nil { + return true + } + localWeightedValue := new(big.Int).Mul(localValueWei, big.NewInt(100)) + builderWeightedValue := new(big.Int).Mul(builderValueWei, new(big.Int).SetUint64(boostFactor)) + return localWeightedValue.Cmp(builderWeightedValue) >= 0 +} + +func selectHigherGloasBidValue( + localValueWei *big.Int, + externalBid *cltypes.SignedExecutionPayloadBid, + boostFactor uint64, +) (*big.Int, bool) { + if externalBid == nil || externalBid.Message == nil { + return localValueWei, false + } + externalValueWei := gweiToWei(new(big.Int).SetUint64(externalBid.Message.Value)) + if preferLocalExecutionValue(localValueWei, externalValueWei, boostFactor) { + return localValueWei, false + } + return externalValueWei, true +} + +func (a *ApiHandler) processProducedBlock( + baseState *state.CachingBeaconState, + block *cltypes.BlindOrExecutionBeaconBlock, + boostFactor uint64, +) (*state.CachingBeaconState, *eth2.Impl, error) { + if block == nil { + return baseState, nil, errors.New("cannot process nil block") + } + // Gloas carries its signed builder bid in the full body and has no blinded block form. + if block.Version().AfterOrEqual(clparams.GloasVersion) && block.BeaconBody == nil { + return baseState, nil, errors.New("cannot process blinded Gloas block") + } + if block.Version().Before(clparams.GloasVersion) || a.epbsPool == nil { + blockMachine, err := processBlockForProduction(baseState, block) + return baseState, blockMachine, err + } + + selfBid := block.BeaconBody.GetSignedExecutionPayloadBid() + if selfBid == nil || selfBid.Message == nil { + blockMachine, err := processBlockForProduction(baseState, block) + return baseState, blockMachine, err + } + bidKey := pool.HighestBidKey{ + Slot: block.Slot, + ParentBlockHash: selfBid.Message.ParentBlockHash, + ParentBlockRoot: selfBid.Message.ParentBlockRoot, + } + externalBid, found := a.epbsPool.HighestBids.Get(bidKey) + selectedValueWei, selected := selectHigherGloasBidValue(block.ExecutionValue, externalBid, boostFactor) + if !found || !selected { + blockMachine, err := processBlockForProduction(baseState, block) + return baseState, blockMachine, err + } + + // ProcessBlock mutates state, so keep baseState untouched for a possible self-build fallback. + candidateState, err := baseState.Copy() + if err != nil { + log.Warn("GLOAS: failed to copy state for external bid; using self-build", + "slot", block.Slot, + "builderIndex", externalBid.Message.BuilderIndex, + "bidValueGwei", externalBid.Message.Value, + "err", err) + blockMachine, processErr := processBlockForProduction(baseState, block) + return baseState, blockMachine, processErr + } + + selfBlobs := block.Blobs + selfKzgProofs := block.KzgProofs + selfExecutionValue := block.ExecutionValue + block.BeaconBody.SignedExecutionPayloadBid = externalBid + block.Blobs = nil + block.KzgProofs = nil + block.ExecutionValue = selectedValueWei + + candidateMachine, candidateErr := processBlockForProduction(candidateState, block) + if candidateErr == nil { + log.Info("GLOAS: selected external builder bid over self-build", + "slot", block.Slot, + "builderIndex", externalBid.Message.BuilderIndex, + "bidValueWei", selectedValueWei, + "localValueWei", selfExecutionValue) + return candidateState, candidateMachine, nil + } + + block.BeaconBody.SignedExecutionPayloadBid = selfBid + block.Blobs = selfBlobs + block.KzgProofs = selfKzgProofs + block.ExecutionValue = selfExecutionValue + // Evict only after the same base state accepts the self-build, isolating the failure to the external bid. + selfBuildMachine, selfBuildErr := processBlockForProduction(baseState, block) + if selfBuildErr != nil { + return baseState, selfBuildMachine, fmt.Errorf( + "external builder transition failed: %w; self-build fallback failed: %w", + candidateErr, + selfBuildErr, + ) + } + + removed := a.epbsPool.RemoveHighestBid(bidKey, externalBid) + log.Warn("GLOAS: external builder bid failed production transition; using self-build", + "slot", block.Slot, + "builderIndex", externalBid.Message.BuilderIndex, + "bidValueGwei", externalBid.Message.Value, + "evicted", removed, + "err", candidateErr) + return baseState, selfBuildMachine, nil +} + +func processBlockForProduction(productionState *state.CachingBeaconState, block *cltypes.BlindOrExecutionBeaconBlock) (*eth2.Impl, error) { + blockMachine := ð2.Impl{BlockRewardsCollector: ð2.BlockRewardsCollector{}} + return blockMachine, machine.ProcessBlock(blockMachine, productionState, block.ToGeneric()) +} + +func gweiToWei(value *big.Int) *big.Int { + return new(big.Int).Mul(value, big.NewInt(common.GWei)) +} + +func consensusBlockValueWei(rewards *eth2.BlockRewardsCollector) *big.Int { + total := new(big.Int).SetUint64(rewards.Attestations) + total.Add(total, new(big.Int).SetUint64(rewards.ProposerSlashings)) + total.Add(total, new(big.Int).SetUint64(rewards.AttesterSlashings)) + total.Add(total, new(big.Int).SetUint64(rewards.SyncAggregate)) + return gweiToWei(total) +} + func (a *ApiHandler) getBuilderPayload( ctx context.Context, baseState *state.CachingBeaconState, @@ -984,6 +1090,9 @@ func (a *ApiHandler) getBuilderPayload( if !strings.EqualFold(header.Version, curVersion) { return nil, fmt.Errorf("invalid version %s, expected %s", header.Version, curVersion) } + if header.BlockValue() == nil { + return nil, fmt.Errorf("invalid builder block value %q", header.Data.Message.Value) + } if ethHeader := header.Data.Message.Header; ethHeader != nil { ethHeader.SetVersion(baseState.Version()) } @@ -1028,9 +1137,9 @@ func (a *ApiHandler) produceBeaconBody( targetSlot uint64, randaoReveal common.Bytes96, graffiti common.Hash, -) (*cltypes.BeaconBody, uint64, error) { +) (*cltypes.BeaconBody, *big.Int, error) { if targetSlot <= baseBlockSlot { - return nil, 0, fmt.Errorf( + return nil, nil, fmt.Errorf( "target slot %d must be greater than base block slot %d", targetSlot, baseBlockSlot, @@ -1072,18 +1181,18 @@ func (a *ApiHandler) produceBeaconBody( // Copy state and apply parent execution payload to compute correct withdrawals stateCopy, err := baseState.Copy() if err != nil { - return nil, 0, fmt.Errorf("produceBeaconBody: failed to copy state for FULL payload: %w", err) + return nil, nil, fmt.Errorf("produceBeaconBody: failed to copy state for FULL payload: %w", err) } envelope, err := a.forkchoiceStore.ReadEnvelopeFromDisk(baseBlockRoot) if err != nil { - return nil, 0, fmt.Errorf("produceBeaconBody: failed to read envelope for FULL payload: %w", err) + return nil, nil, fmt.Errorf("produceBeaconBody: failed to read envelope for FULL payload: %w", err) } if envelope == nil || envelope.Message == nil || envelope.Message.ExecutionRequests == nil { - return nil, 0, fmt.Errorf("produceBeaconBody: head is FULL but envelope/requests missing for root %x", baseBlockRoot) + return nil, nil, fmt.Errorf("produceBeaconBody: head is FULL but envelope/requests missing for root %x", baseBlockRoot) } stfMachine := ð2.Impl{} if err := stfMachine.ApplyParentExecutionPayload(stateCopy, envelope.Message.ExecutionRequests); err != nil { - return nil, 0, fmt.Errorf("produceBeaconBody: failed to apply parent execution payload: %w", err) + return nil, nil, fmt.Errorf("produceBeaconBody: failed to apply parent execution payload: %w", err) } gloasWithdrawalsState = stateCopy // Populate the block body's ParentExecutionRequests so @@ -1107,7 +1216,7 @@ func (a *ApiHandler) produceBeaconBody( } proposerIndex, err := baseState.GetBeaconProposerIndexForSlot(targetSlot) if err != nil { - return nil, 0, err + return nil, nil, err } var targetGasLimit *hexutil.Uint64 if stateVersion.AfterOrEqual(clparams.GloasVersion) { @@ -1127,7 +1236,8 @@ func (a *ApiHandler) produceBeaconBody( } } var executionPayload *cltypes.Eth1Block - var executionValue uint64 + // Keep the produced block's value independent from the engine-owned value. + executionValue := new(big.Int) // One collector per concurrent body step. Sharing one would be a write-write race whenever // two steps fail together. var executionErr, syncAggregateErr error @@ -1194,10 +1304,8 @@ func (a *ApiHandler) produceBeaconBody( bundles = &engine_types.BlobsBundle{} } // Determine block value - if blockValue == nil { - executionValue = 0 - } else { - executionValue = blockValue.Uint64() + if blockValue != nil { + executionValue.Set(blockValue) } if stateVersion.Before(clparams.FuluVersion) { @@ -1388,13 +1496,13 @@ func (a *ApiHandler) produceBeaconBody( } wg.Wait() if executionErr != nil { - return nil, 0, executionErr + return nil, nil, executionErr } if syncAggregateErr != nil { - return nil, 0, syncAggregateErr + return nil, nil, syncAggregateErr } if executionPayload == nil { - return nil, 0, errors.New("failed to produce execution payload") + return nil, nil, errors.New("failed to produce execution payload") } if stateVersion.AfterOrEqual(clparams.GloasVersion) { @@ -1551,10 +1659,11 @@ func (a *ApiHandler) setupHeaderReponseForBlockProduction( consensusVersion clparams.StateVersion, blinded bool, executionPayloadIncluded bool, - executionBlockValue, consensusBlockValue uint64, + executionBlockValue *big.Int, + consensusBlockValue *big.Int, ) { - w.Header().Set("Eth-Execution-Payload-Value", strconv.FormatUint(executionBlockValue, 10)) - w.Header().Set("Eth-Consensus-Block-Value", strconv.FormatUint(consensusBlockValue, 10)) + w.Header().Set("Eth-Execution-Payload-Value", executionBlockValue.String()) + w.Header().Set("Eth-Consensus-Block-Value", consensusBlockValue.String()) w.Header().Set("Eth-Consensus-Version", clparams.ClVersionToString(consensusVersion)) w.Header().Set("Eth-Execution-Payload-Blinded", strconv.FormatBool(blinded)) if consensusVersion >= clparams.GloasVersion { @@ -1926,9 +2035,8 @@ func (a *ApiHandler) broadcastBlock(ctx context.Context, blk *cltypes.SignedBeac blobsSidecars := make([]*cltypes.BlobSidecar, 0, blkCommitmentsLen) var columnsSidecars []*cltypes.DataColumnSidecar - header := blk.SignedBeaconBlockHeader() - if blk.Version() >= clparams.DenebVersion && blk.Version() < clparams.FuluVersion { + header := blk.SignedBeaconBlockHeader() for i := 0; i < blk.Block.Body.BlobKzgCommitments.Len(); i++ { blobSidecar := &cltypes.BlobSidecar{} commitment := blk.Block.Body.BlobKzgCommitments.Get(i) @@ -1969,8 +2077,10 @@ func (a *ApiHandler) broadcastBlock(ctx context.Context, blk *cltypes.SignedBeac isGloas := blk.Version() >= clparams.GloasVersion if isGloas { - // [New in Gloas:EIP7732] Get from signed_execution_payload_bid - if bid := blk.Block.Body.GetSignedExecutionPayloadBid(); bid != nil && bid.Message != nil { + // External builders publish their payload data. Only a self-built bid has + // blob data in the proposer's local cache. + if bid := blk.Block.Body.GetSignedExecutionPayloadBid(); bid != nil && bid.Message != nil && + bid.Message.BuilderIndex == clparams.BuilderIndexSelfBuild { kzgCommitments = &bid.Message.BlobKzgCommitments } } else { @@ -2012,6 +2122,7 @@ func (a *ApiHandler) broadcastBlock(ctx context.Context, blk *cltypes.SignedBeac } } else { // Fulu needs inclusion proof + header := blk.SignedBeaconBlockHeader() inclusionProofRaw, err := blk.Block.Body.KzgCommitmentsInclusionProof() if err != nil { return err @@ -2034,19 +2145,14 @@ func (a *ApiHandler) broadcastBlock(ctx context.Context, blk *cltypes.SignedBeac } }() - lenBlobs := 0 - if blk.Version() >= clparams.DenebVersion { - if c := blk.Block.Body.GetBlobKzgCommitments(); c != nil { - lenBlobs = c.Len() - } - } - log.Info( - "BlockPublishing: publishing block and blobs", + "BlockPublishing: publishing block and sidecars", "slot", blk.Block.Slot, - "blobs", - lenBlobs, + "blobSidecars", + len(blobsSidecars), + "columnSidecars", + len(columnsSidecars), ) // Broadcast the block and its blobs if err := a.gossipManager.Publish(ctx, gossip.TopicNameBeaconBlock, blkSSZ); err != nil { diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go index 91efe3db754..211a3874ad8 100644 --- a/cl/beacon/handler/block_production_test.go +++ b/cl/beacon/handler/block_production_test.go @@ -35,19 +35,26 @@ import ( "go.uber.org/mock/gomock" "github.com/erigontech/erigon/cl/beacon/beaconhttp" + "github.com/erigontech/erigon/cl/beacon/builder" builder_mock "github.com/erigontech/erigon/cl/beacon/builder/mock_services" "github.com/erigontech/erigon/cl/clparams" "github.com/erigontech/erigon/cl/cltypes" "github.com/erigontech/erigon/cl/cltypes/solid" + "github.com/erigontech/erigon/cl/fork" "github.com/erigontech/erigon/cl/phase1/core/state" "github.com/erigontech/erigon/cl/phase1/core/state/lru" "github.com/erigontech/erigon/cl/phase1/execution_client" + "github.com/erigontech/erigon/cl/pool" + "github.com/erigontech/erigon/cl/transition/impl/eth2" + "github.com/erigontech/erigon/cl/utils" + "github.com/erigontech/erigon/cl/utils/bls" sync_pool_mock "github.com/erigontech/erigon/cl/validator/sync_contribution_pool/mock_services" "github.com/erigontech/erigon/cl/validator/validator_params" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/common/length" "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/kv" "github.com/erigontech/erigon/execution/chain" "github.com/erigontech/erigon/execution/engineapi/engine_helpers" "github.com/erigontech/erigon/execution/engineapi/engine_types" @@ -60,6 +67,14 @@ import ( "github.com/erigontech/erigon/node/gointerfaces/typesproto" ) +type updateFailingDB struct { + kv.RwDB +} + +func (db updateFailingDB) Update(context.Context, func(kv.RwTx) error) error { + return errors.New("stop after persistence") +} + func TestBlockBuilderWindowPreGloas(t *testing.T) { cfg := &clparams.BeaconChainConfig{ SecondsPerSlot: 12, @@ -544,7 +559,7 @@ func TestSetupHeaderResponseForBlockProductionGloasPayloadIncluded(t *testing.T) h := &ApiHandler{} rr := httptest.NewRecorder() - h.setupHeaderReponseForBlockProduction(rr, clparams.GloasVersion, false, true, 123, 456) + h.setupHeaderReponseForBlockProduction(rr, clparams.GloasVersion, false, true, big.NewInt(123), big.NewInt(456)) require.Equal(t, "gloas", rr.Header().Get("Eth-Consensus-Version")) require.Equal(t, "123", rr.Header().Get("Eth-Execution-Payload-Value")) @@ -557,7 +572,7 @@ func TestSetupHeaderResponseForBlockProductionPreGloasOmitsPayloadIncluded(t *te h := &ApiHandler{} rr := httptest.NewRecorder() - h.setupHeaderReponseForBlockProduction(rr, clparams.ElectraVersion, false, true, 123, 456) + h.setupHeaderReponseForBlockProduction(rr, clparams.ElectraVersion, false, true, big.NewInt(123), big.NewInt(456)) require.Empty(t, rr.Header().Get("Eth-Execution-Payload-Included")) } @@ -703,6 +718,454 @@ func TestProduceBeaconBodyRejectsMissingBlobsBundleAtDeneb(t *testing.T) { require.ErrorContains(t, err, "missing blobs bundle") } +func TestSelectHigherGloasBidValueUsesWei(t *testing.T) { + t.Run("higher bid", func(t *testing.T) { + localValueWei := gweiToWei(big.NewInt(2)) + externalBid := &cltypes.SignedExecutionPayloadBid{ + Message: &cltypes.ExecutionPayloadBid{Value: 3}, + } + + selectedValueWei, selected := selectHigherGloasBidValue(localValueWei, externalBid, 100) + + require.True(t, selected) + require.Equal(t, "3000000000", selectedValueWei.String()) + }) + + t.Run("equal bid", func(t *testing.T) { + localValueWei := gweiToWei(big.NewInt(2)) + externalBid := &cltypes.SignedExecutionPayloadBid{ + Message: &cltypes.ExecutionPayloadBid{Value: 2}, + } + + selectedValueWei, selected := selectHigherGloasBidValue(localValueWei, externalBid, 100) + + require.False(t, selected) + require.Same(t, localValueWei, selectedValueWei) + }) + + t.Run("maximum bid", func(t *testing.T) { + externalBid := &cltypes.SignedExecutionPayloadBid{ + Message: &cltypes.ExecutionPayloadBid{Value: ^uint64(0)}, + } + wantWei := gweiToWei(new(big.Int).SetUint64(^uint64(0))) + + selectedValueWei, selected := selectHigherGloasBidValue(new(big.Int), externalBid, 100) + + require.True(t, selected) + require.Equal(t, wantWei, selectedValueWei) + }) +} + +func TestSelectHigherGloasBidValueHonorsBoostFactor(t *testing.T) { + localValueWei := gweiToWei(big.NewInt(2)) + externalBid := &cltypes.SignedExecutionPayloadBid{ + Message: &cltypes.ExecutionPayloadBid{Value: 3}, + } + + selectedValueWei, selected := selectHigherGloasBidValue(localValueWei, externalBid, 0) + + require.False(t, selected) + require.Same(t, localValueWei, selectedValueWei) +} + +func TestPreferLocalExecutionValueRejectsNilBuilderValue(t *testing.T) { + require.True(t, preferLocalExecutionValue(big.NewInt(1), nil, 100)) +} + +func TestShouldRequestBuilderHeader(t *testing.T) { + require.True(t, shouldRequestBuilderHeader(clparams.FuluVersion, true, true)) + require.False(t, shouldRequestBuilderHeader(clparams.GloasVersion, true, true)) + require.False(t, shouldRequestBuilderHeader(clparams.FuluVersion, false, true)) + require.False(t, shouldRequestBuilderHeader(clparams.FuluVersion, true, false)) +} + +func TestGetBuilderPayloadRejectsInvalidBlockValue(t *testing.T) { + for _, test := range []struct { + name string + value string + }{ + {name: "empty"}, + {name: "not_a_number", value: "not-a-number"}, + {name: "negative", value: "-1"}, + {name: "over_uint256", value: new(big.Int).Lsh(big.NewInt(1), 256).String()}, + } { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + _, _, _, _, postState, handler, _, _, _, _ := setupTestingHandler(t, clparams.ElectraVersion, log.Root(), false) + builderClient := builder_mock.NewMockBuilderClient(ctrl) + builderClient.EXPECT().GetHeader(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&builder.ExecutionHeader{ + Version: postState.Version().String(), + Data: builder.ExecutionHeaderData{Message: builder.ExecutionHeaderMessage{ + Value: test.value, + }}, + }, nil) + handler.builderClient = builderClient + + _, err := handler.getBuilderPayload(t.Context(), postState, postState.Slot()+1) + + require.ErrorContains(t, err, "invalid builder block value") + }) + } +} + +func TestProcessProducedBlockFallsBackWithoutCandidateStateLeak(t *testing.T) { + fixture := newGloasBidSelectionFixture(t, gloasBidSelectionOptions{exitBuilder: true}) + selfBid := fixture.block.BeaconBody.SignedExecutionPayloadBid + expectedState, err := fixture.productionState.Copy() + require.NoError(t, err) + _, err = processBlockForProduction(expectedState, fixture.block) + require.NoError(t, err) + expectedRoot, err := expectedState.HashSSZ() + require.NoError(t, err) + logs := captureProductionLogs(t) + handler := &ApiHandler{epbsPool: pool.NewEpbsPool()} + handler.epbsPool.StoreHighestBid(fixture.bidKey, fixture.externalBid) + + selectedState, _, err := handler.processProducedBlock(fixture.productionState, fixture.block, 100) + + require.NoError(t, err) + require.Same(t, fixture.productionState, selectedState) + require.Same(t, selfBid, fixture.block.BeaconBody.SignedExecutionPayloadBid) + require.Equal(t, "1000000000", fixture.block.ExecutionValue.String()) + require.Len(t, fixture.block.Blobs, 1) + require.Len(t, fixture.block.KzgProofs, 1) + selectedRoot, err := selectedState.HashSSZ() + require.NoError(t, err) + require.Equal(t, expectedRoot, selectedRoot) + _, found := handler.epbsPool.HighestBids.Get(fixture.bidKey) + require.False(t, found) + require.Contains(t, logs(), "builderIndex=0") + require.Contains(t, logs(), "bidValueGwei=3") +} + +func TestProcessProducedBlockSelectsExternalBidWithoutMutatingBaseState(t *testing.T) { + fixture := newGloasBidSelectionFixture(t, gloasBidSelectionOptions{}) + originalRoot, err := fixture.productionState.HashSSZ() + require.NoError(t, err) + handler := &ApiHandler{epbsPool: pool.NewEpbsPool()} + handler.epbsPool.StoreHighestBid(fixture.bidKey, fixture.externalBid) + + selectedState, blockMachine, err := handler.processProducedBlock(fixture.productionState, fixture.block, 100) + + require.NoError(t, err) + require.NotSame(t, fixture.productionState, selectedState) + require.NotNil(t, blockMachine.BlockRewardsCollector) + require.Same(t, fixture.externalBid, fixture.block.BeaconBody.SignedExecutionPayloadBid) + require.Equal(t, "3000000000", fixture.block.ExecutionValue.String()) + require.Nil(t, fixture.block.Blobs) + require.Nil(t, fixture.block.KzgProofs) + afterRoot, err := fixture.productionState.HashSSZ() + require.NoError(t, err) + require.Equal(t, originalRoot, afterRoot) + require.Equal(t, fixture.externalBid.Message.BlockHash, selectedState.GetLatestExecutionPayloadBid().BlockHash) +} + +func TestProcessProducedBlockRejectsBlindedGloasBlock(t *testing.T) { + fixture := newGloasBidSelectionFixture(t, gloasBidSelectionOptions{}) + block := &cltypes.BlindOrExecutionBeaconBlock{ + BlindedBeaconBody: cltypes.NewBlindedBeaconBody(fixture.block.Cfg, clparams.GloasVersion), + Cfg: fixture.block.Cfg, + } + handler := &ApiHandler{epbsPool: pool.NewEpbsPool()} + + _, _, err := handler.processProducedBlock(fixture.productionState, block, 100) + + require.ErrorContains(t, err, "cannot process blinded Gloas block") +} + +func TestProcessProducedBlockRejectsNilBlock(t *testing.T) { + fixture := newGloasBidSelectionFixture(t, gloasBidSelectionOptions{}) + handler := &ApiHandler{epbsPool: pool.NewEpbsPool()} + + _, _, err := handler.processProducedBlock(fixture.productionState, nil, 100) + + require.ErrorContains(t, err, "cannot process nil block") +} + +func TestProcessProducedBlockRejectsInvalidExternalBidGuards(t *testing.T) { + tests := []struct { + name string + options gloasBidSelectionOptions + }{ + { + name: "randao mismatch", + options: gloasBidSelectionOptions{mutateBid: func(bid *cltypes.ExecutionPayloadBid) { + bid.PrevRandao[0] ^= 0xff + }}, + }, + { + name: "builder version mismatch", + options: gloasBidSelectionOptions{mutateBuilder: func(builder *cltypes.Builder) { + builder.Version++ + }}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fixture := newGloasBidSelectionFixture(t, test.options) + selfBid := fixture.block.BeaconBody.SignedExecutionPayloadBid + handler := &ApiHandler{epbsPool: pool.NewEpbsPool()} + handler.epbsPool.StoreHighestBid(fixture.bidKey, fixture.externalBid) + + _, _, err := handler.processProducedBlock(fixture.productionState, fixture.block, 100) + + require.NoError(t, err) + require.Same(t, selfBid, fixture.block.BeaconBody.SignedExecutionPayloadBid) + _, found := handler.epbsPool.HighestBids.Get(fixture.bidKey) + require.False(t, found) + }) + } +} + +func TestConsensusBlockValueUsesWeiWithoutOverflow(t *testing.T) { + rewards := ð2.BlockRewardsCollector{ + Attestations: ^uint64(0), + AttesterSlashings: 2, + ProposerSlashings: 3, + SyncAggregate: 4, + } + wantGwei := new(big.Int).Add(new(big.Int).SetUint64(^uint64(0)), big.NewInt(9)) + wantWei := gweiToWei(wantGwei) + + require.Equal(t, wantWei, consensusBlockValueWei(rewards)) +} + +type gloasBidSelectionOptions struct { + exitBuilder bool + mutateBuilder func(*cltypes.Builder) + mutateBid func(*cltypes.ExecutionPayloadBid) +} + +type gloasBidSelectionFixture struct { + productionState *state.CachingBeaconState + block *cltypes.BlindOrExecutionBeaconBlock + externalBid *cltypes.SignedExecutionPayloadBid + bidKey pool.HighestBidKey +} + +func newGloasBidSelectionFixture(t *testing.T, options gloasBidSelectionOptions) gloasBidSelectionFixture { + t.Helper() + cfg := clparams.MainnetBeaconConfig + clparams.ApplyMinimalPreset(&cfg) + cfg.PayloadBuilderVersion = 7 + productionState := state.New(&cfg) + productionState.SetVersion(clparams.GloasVersion) + slot := cfg.SlotsPerEpoch + require.NoError(t, productionState.SetSlot(slot)) + productionState.SetFinalizedCheckpoint(solid.Checkpoint{Epoch: 1}) + productionState.SetGenesisValidatorsRoot(common.Hash{0x91}) + productionState.SetFork(&cltypes.Fork{ + PreviousVersion: utils.Uint32ToBytes4(uint32(cfg.FuluForkVersion)), + CurrentVersion: utils.Uint32ToBytes4(uint32(cfg.GloasForkVersion)), + Epoch: state.Epoch(productionState), + }) + require.NoError(t, productionState.SetRandaoMixAt( + int(state.Epoch(productionState)%cfg.EpochsPerHistoricalVector), + common.Hash{0xa1}, + )) + + privateKey, err := bls.GenerateKey() + require.NoError(t, err) + pubkey := common.Bytes48(bls.CompressPublicKey(privateKey.PublicKey())) + require.NoError(t, productionState.AddValidator(solid.NewValidatorFromParameters( + pubkey, + common.Hash{}, + cfg.MaxEffectiveBalance, + false, + 0, + 0, + cfg.FarFutureEpoch, + cfg.FarFutureEpoch, + ), cfg.MaxEffectiveBalance)) + committee := make([]common.Bytes48, int(cfg.SyncCommitteeSize)) + for i := range committee { + committee[i] = pubkey + } + require.NoError(t, productionState.SetCurrentSyncCommittee( + solid.NewSyncCommitteeFromParameters(committee, pubkey), + )) + + executionAddress := common.Address{0x42} + builders := solid.NewStaticListSSZ[*cltypes.Builder](int(cfg.BuilderRegistryLimit), new(cltypes.Builder).EncodingSizeSSZ()) + payloadBuilder := &cltypes.Builder{ + Pubkey: pubkey, + Version: cfg.PayloadBuilderVersion, + ExecutionAddress: executionAddress, + Balance: cfg.MinDepositAmount + 100, + DepositEpoch: 0, + WithdrawableEpoch: cfg.FarFutureEpoch, + } + if options.mutateBuilder != nil { + options.mutateBuilder(payloadBuilder) + } + builders.Append(payloadBuilder) + productionState.SetBuilders(builders) + + parentHeader := productionState.LatestBlockHeader() + parentRootRaw, err := (&parentHeader).HashSSZ() + require.NoError(t, err) + parentRoot := common.Hash(parentRootRaw) + parentHash := common.Hash{0x22} + require.NoError(t, productionState.SetBlockRootAt(int((slot-1)%cfg.SlotsPerHistoricalRoot), parentRoot)) + parentRequests := cltypes.NewExecutionRequestsWithVersion(&cfg, clparams.GloasVersion) + if options.exitBuilder { + parentRequests.BuilderExits.Append(&solid.BuilderExitRequest{ + SourceAddress: executionAddress, + PubKey: pubkey, + }) + } + parentRequestsRoot, err := parentRequests.HashSSZ() + require.NoError(t, err) + productionState.SetLatestExecutionPayloadBid(&cltypes.ExecutionPayloadBid{ + BlockHash: parentHash, + Slot: slot - 1, + ExecutionRequestsRoot: parentRequestsRoot, + }) + productionState.SetLatestBlockHash(parentHash) + + commitments := solid.NewStaticProgressiveListSSZ[*cltypes.KZGCommitment](cltypes.MaxBlobsCommittmentsPerBlock, 48) + commitments.Append(&cltypes.KZGCommitment{0x33}) + externalBid := &cltypes.SignedExecutionPayloadBid{Message: &cltypes.ExecutionPayloadBid{ + ParentBlockHash: parentHash, + ParentBlockRoot: parentRoot, + BlockHash: common.Hash{0x44}, + PrevRandao: productionState.GetRandaoMixes(state.Epoch(productionState)), + FeeRecipient: common.Address{0x55}, + BuilderIndex: 0, + Slot: slot, + Value: 3, + BlobKzgCommitments: *commitments, + }} + if options.mutateBid != nil { + options.mutateBid(externalBid.Message) + } + domain, err := productionState.GetDomain(cfg.DomainBeaconBuilder, state.Epoch(productionState)) + require.NoError(t, err) + signingRoot, err := fork.ComputeSigningRoot(externalBid.Message, domain) + require.NoError(t, err) + copy(externalBid.Signature[:], privateKey.Sign(signingRoot[:]).Bytes()) + + selfCommitments := solid.NewStaticProgressiveListSSZ[*cltypes.KZGCommitment](cltypes.MaxBlobsCommittmentsPerBlock, 48) + body := cltypes.NewBeaconBody(&cfg, clparams.GloasVersion) + body.SignedExecutionPayloadBid = &cltypes.SignedExecutionPayloadBid{ + Message: &cltypes.ExecutionPayloadBid{ + ParentBlockHash: parentHash, + ParentBlockRoot: parentRoot, + BlockHash: common.Hash{0x66}, + PrevRandao: productionState.GetRandaoMixes(state.Epoch(productionState)), + BuilderIndex: clparams.BuilderIndexSelfBuild, + Slot: slot, + BlobKzgCommitments: *selfCommitments, + }, + Signature: common.Bytes96(bls.InfiniteSignature), + } + body.ParentExecutionRequests = parentRequests + + block := &cltypes.BlindOrExecutionBeaconBlock{ + Slot: slot, + ProposerIndex: 0, + ParentRoot: parentRoot, + BeaconBody: body, + Blobs: []*cltypes.Blob{{0x77}}, + KzgProofs: []common.Bytes48{{0x88}}, + ExecutionValue: gweiToWei(big.NewInt(1)), + Cfg: &cfg, + } + return gloasBidSelectionFixture{ + productionState: productionState, + block: block, + externalBid: externalBid, + bidKey: pool.HighestBidKey{ + Slot: slot, + ParentBlockHash: parentHash, + ParentBlockRoot: parentRoot, + }, + } +} + +func TestSetupHeaderResponsePreservesLargeExecutionValue(t *testing.T) { + h := &ApiHandler{} + rr := httptest.NewRecorder() + valueWei := gweiToWei(new(big.Int).SetUint64(^uint64(0))) + + h.setupHeaderReponseForBlockProduction(rr, clparams.GloasVersion, false, true, valueWei, new(big.Int)) + + require.Equal(t, valueWei.String(), rr.Header().Get("Eth-Execution-Payload-Value")) +} + +func TestProduceBlockPreservesLargeExecutionValue(t *testing.T) { + ctrl := gomock.NewController(t) + _, _, _, _, postState, h, _, _, _, _ := setupTestingHandler(t, clparams.ElectraVersion, log.Root(), false) + h.routerCfg.Builder = false + payload := cltypes.NewEth1Block(clparams.ElectraVersion, h.beaconChainCfg) + payload.Transactions = &solid.TransactionsSSZ{} + payload.Withdrawals = solid.NewStaticListSSZ[*cltypes.Withdrawal](int(h.beaconChainCfg.MaxWithdrawalsPerPayload), 44) + valueWei := new(big.Int).Add(new(big.Int).SetUint64(^uint64(0)), big.NewInt(1)) + wantValueWei := valueWei.String() + + engine := execution_client.NewMockExecutionEngine(ctrl) + engine.EXPECT().ForkChoiceUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return([]byte{1, 2, 3, 4, 5, 6, 7, 8}, nil).AnyTimes() + engine.EXPECT().GetAssembledBlock(gomock.Any(), gomock.Any(), gomock.Any()). + Return(payload, &engine_types.BlobsBundle{}, nil, valueWei, nil).AnyTimes() + engine.EXPECT().SupportInsertion().Return(true).AnyTimes() + h.engine = engine + + block, err := h.produceBlock(t.Context(), 1, postState.Slot(), common.Hash{0x41}, postState, + postState.Slot()+1, common.Bytes96{}, common.Hash{}) + + require.NoError(t, err) + require.Equal(t, wantValueWei, block.ExecutionValue.String()) +} + +func TestProduceBlockUsesLocalPayloadWithoutBuilderClient(t *testing.T) { + ctrl := gomock.NewController(t) + _, _, _, _, postState, h, _, _, _, _ := setupTestingHandler(t, clparams.ElectraVersion, log.Root(), false) + require.True(t, h.routerCfg.Builder) + require.Nil(t, h.builderClient) + payload := cltypes.NewEth1Block(clparams.ElectraVersion, h.beaconChainCfg) + payload.Transactions = &solid.TransactionsSSZ{} + payload.Withdrawals = solid.NewStaticListSSZ[*cltypes.Withdrawal](int(h.beaconChainCfg.MaxWithdrawalsPerPayload), 44) + valueWei := big.NewInt(1) + + engine := execution_client.NewMockExecutionEngine(ctrl) + engine.EXPECT().ForkChoiceUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return([]byte{1, 2, 3, 4, 5, 6, 7, 8}, nil).AnyTimes() + engine.EXPECT().GetAssembledBlock(gomock.Any(), gomock.Any(), gomock.Any()). + Return(payload, &engine_types.BlobsBundle{}, nil, valueWei, nil).AnyTimes() + engine.EXPECT().SupportInsertion().Return(true).AnyTimes() + h.engine = engine + + block, err := h.produceBlock(t.Context(), 1, postState.Slot(), common.Hash{0x41}, postState, + postState.Slot()+1, common.Bytes96{}, common.Hash{}) + + require.NoError(t, err) + require.False(t, block.IsBlinded()) + require.Equal(t, valueWei, block.ExecutionValue) +} + +func TestBroadcastExternalGloasBidDoesNotRequireLocalBlobBundles(t *testing.T) { + logs := captureAllProductionLogs(t) + _, _, _, _, _, h, _, _, _, _ := setupTestingHandler(t, clparams.ElectraVersion, log.Root(), false) + h.indiciesDB = updateFailingDB{RwDB: h.indiciesDB} + block := cltypes.NewSignedBeaconBlock(h.beaconChainCfg, clparams.GloasVersion) + bid := block.Block.Body.GetSignedExecutionPayloadBid() + require.NotNil(t, bid) + require.NotNil(t, bid.Message) + bid.Message.BuilderIndex = 1 + bid.Message.BlobKzgCommitments.Append(&cltypes.KZGCommitment{0x01}) + + require.NoError(t, h.broadcastBlock(t.Context(), block)) + + // The persistence error is logged at the end of the background store goroutine. + require.Eventually(t, func() bool { + return strings.Contains(logs(), "stop after persistence") + }, 5*time.Second, 10*time.Millisecond) + require.Contains(t, logs(), "blobSidecars=0") + require.Contains(t, logs(), "columnSidecars=0") + require.NotContains(t, logs(), "blobs=1") +} + // TestCaplinBlockProductionWithWithdrawalRequest tests Caplin's produceBeaconBody // against a real Erigon execution layer. A withdrawal request transaction is // submitted to the EIP-7002 system contract, and then Caplin's actual block @@ -810,7 +1273,7 @@ func TestCaplinBlockProductionWithWithdrawalRequest(t *testing.T) { ) require.NoError(t, err) require.NotNil(t, beaconBody) - require.NotZero(t, execValue) + require.Positive(t, execValue.Sign()) // --- Verify execution requests were decoded by Caplin --- @@ -1055,18 +1518,24 @@ func (s *syncedBuffer) String() string { return s.buf.String() } -// captureProductionLogs redirects the root logger for one test and returns everything written at -// warning level or above. It deliberately does not filter by message: a record this package emits -// under another name is exactly what a test asserting silence needs to see. -func captureProductionLogs(t *testing.T) func() string { +func captureAllProductionLogs(t *testing.T) func() string { t.Helper() output := &syncedBuffer{} previous := log.Root().GetHandler() log.Root().SetHandler(log.StreamHandler(output, log.LogfmtFormat())) t.Cleanup(func() { log.Root().SetHandler(previous) }) + return output.String +} + +// captureProductionLogs redirects the root logger for one test and returns everything written at +// warning level or above. It deliberately does not filter by message: a record this package emits +// under another name is exactly what a test asserting silence needs to see. +func captureProductionLogs(t *testing.T) func() string { + t.Helper() + allLogs := captureAllProductionLogs(t) return func() string { var loud []string - for line := range strings.SplitSeq(output.String(), "\n") { + for line := range strings.SplitSeq(allLogs(), "\n") { if strings.Contains(line, "lvl=eror") || strings.Contains(line, "lvl=warn") { loud = append(loud, line) } diff --git a/cl/cltypes/block_production.go b/cl/cltypes/block_production.go index b73f895ddcc..9ee32286023 100644 --- a/cl/cltypes/block_production.go +++ b/cl/cltypes/block_production.go @@ -38,6 +38,7 @@ type BlindOrExecutionBeaconBlock struct { // Blinded body BlindedBeaconBody *BlindedBeaconBody `json:"-"` + // ExecutionValue is the execution payload value in Wei. ExecutionValue *big.Int `json:"-"` Cfg *clparams.BeaconChainConfig } diff --git a/cl/cltypes/epbs_payload.go b/cl/cltypes/epbs_payload.go index 857b003e234..99abc8d1ce8 100644 --- a/cl/cltypes/epbs_payload.go +++ b/cl/cltypes/epbs_payload.go @@ -272,8 +272,8 @@ type ExecutionPayloadBid struct { GasLimit uint64 `json:"gas_limit,string"` BuilderIndex uint64 `json:"builder_index,string"` Slot uint64 `json:"slot,string"` - Value uint64 `json:"value,string"` - ExecutionPayment uint64 `json:"execution_payment,string"` + Value uint64 `json:"value,string"` // Gwei + ExecutionPayment uint64 `json:"execution_payment,string"` // Gwei BlobKzgCommitments solid.ListSSZ[*KZGCommitment] `json:"blob_kzg_commitments"` ExecutionRequestsRoot common.Hash `json:"execution_requests_root"` } diff --git a/cl/phase1/network/services/execution_payload_bid_service.go b/cl/phase1/network/services/execution_payload_bid_service.go index 0d2c413a9c5..f089676fa8b 100644 --- a/cl/phase1/network/services/execution_payload_bid_service.go +++ b/cl/phase1/network/services/execution_payload_bid_service.go @@ -368,7 +368,7 @@ func (s *executionPayloadBidService) storeValidBid(msg *cltypes.SignedExecutionP } s.seenCache.Add(seenKey, struct{}{}) bidKey := pool.HighestBidKey{Slot: bid.Slot, ParentBlockHash: bid.ParentBlockHash, ParentBlockRoot: bid.ParentBlockRoot} - s.epbsPool.HighestBids.Add(bidKey, msg) + s.epbsPool.StoreHighestBid(bidKey, msg) return nil } diff --git a/cl/pool/epbs_pool.go b/cl/pool/epbs_pool.go index c85a3d24708..3bf34d0a41c 100644 --- a/cl/pool/epbs_pool.go +++ b/cl/pool/epbs_pool.go @@ -1,6 +1,8 @@ package pool import ( + "sync" + "github.com/erigontech/erigon/cl/cltypes" "github.com/erigontech/erigon/cl/phase1/core/state/lru" "github.com/erigontech/erigon/common" @@ -37,6 +39,8 @@ type HighestBidKey struct { // EpbsPool holds EPBS-related gossip data caches. // [New in Gloas:EIP7732] type EpbsPool struct { + highestBidsMu sync.Mutex + // ProposerPreferences stores validated SignedProposerPreferences keyed by (slot, dependent_root). // Written by the proposer_preferences gossip service, read by the execution_payload_bid service. ProposerPreferences *lru.Cache[ProposerPreferencesKey, *cltypes.SignedProposerPreferences] @@ -50,6 +54,24 @@ type EpbsPool struct { PayloadAttestations *lru.Cache[PayloadAttestationKey, *cltypes.PayloadAttestationMessage] } +// StoreHighestBid replaces the current entry for key. +func (p *EpbsPool) StoreHighestBid(key HighestBidKey, bid *cltypes.SignedExecutionPayloadBid) { + p.highestBidsMu.Lock() + defer p.highestBidsMu.Unlock() + p.HighestBids.Add(key, bid) +} + +// RemoveHighestBid preserves a concurrently stored replacement for the same key. +func (p *EpbsPool) RemoveHighestBid(key HighestBidKey, bid *cltypes.SignedExecutionPayloadBid) bool { + p.highestBidsMu.Lock() + defer p.highestBidsMu.Unlock() + current, found := p.HighestBids.Get(key) + if !found || current != bid { + return false + } + return p.HighestBids.Remove(key) +} + func NewEpbsPool() *EpbsPool { preferencesCache, err := lru.New[ProposerPreferencesKey, *cltypes.SignedProposerPreferences]("proposerPreferencesPool", epbsPreferencesPoolSize) if err != nil { diff --git a/cl/pool/epbs_pool_test.go b/cl/pool/epbs_pool_test.go new file mode 100644 index 00000000000..139316a7fcf --- /dev/null +++ b/cl/pool/epbs_pool_test.go @@ -0,0 +1,32 @@ +package pool + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/cl/cltypes" +) + +func TestRemoveHighestBidOnlyRemovesMatchingBid(t *testing.T) { + pool := NewEpbsPool() + key := HighestBidKey{Slot: 1} + rejected := &cltypes.SignedExecutionPayloadBid{Message: &cltypes.ExecutionPayloadBid{Value: 1}} + replacement := &cltypes.SignedExecutionPayloadBid{Message: &cltypes.ExecutionPayloadBid{Value: 2}} + + pool.StoreHighestBid(key, rejected) + require.False(t, pool.RemoveHighestBid(key, replacement)) + stored, found := pool.HighestBids.Get(key) + require.True(t, found) + require.Same(t, rejected, stored) + + pool.StoreHighestBid(key, replacement) + require.False(t, pool.RemoveHighestBid(key, rejected)) + stored, found = pool.HighestBids.Get(key) + require.True(t, found) + require.Same(t, replacement, stored) + + require.True(t, pool.RemoveHighestBid(key, replacement)) + _, found = pool.HighestBids.Get(key) + require.False(t, found) +}