diff --git a/eth/api_miner.go b/eth/api_miner.go index 00fceaf170..9baa7796c3 100644 --- a/eth/api_miner.go +++ b/eth/api_miner.go @@ -152,3 +152,22 @@ func (api *MinerAPI) SetLastBlockMiningTime(time uint64) minerconfig.MBConfig { api.e.Miner().SetLastBlockMiningTime(time) return api.e.Miner().MBConfig() } + +// SetForceBlobOnNonEligible sets whether to force blob txs on non-eligible blocks. +// BEP-657 chaos testing: simulate malicious validator packing blobs when N % 5 != 0. +func (api *MinerAPI) SetForceBlobOnNonEligible(on bool) minerconfig.MBConfig { + api.e.Miner().SetForceBlobOnNonEligible(on) + return api.e.Miner().MBConfig() +} + +// SetCorruptBlobSidecar sets whether to corrupt blob sidecar data during P2P broadcast. +func (api *MinerAPI) SetCorruptBlobSidecar(on bool) minerconfig.MBConfig { + api.e.Miner().SetCorruptBlobSidecar(on) + return api.e.Miner().MBConfig() +} + +// SetDropBlobSidecar sets whether to drop blob sidecars during P2P broadcast. +func (api *MinerAPI) SetDropBlobSidecar(on bool) minerconfig.MBConfig { + api.e.Miner().SetDropBlobSidecar(on) + return api.e.Miner().MBConfig() +} diff --git a/eth/backend.go b/eth/backend.go index ab66ae6b97..6a42f75454 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -484,6 +484,12 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) eth.miner.SetPrioAddresses(config.TxPool.Locals) + // Set up malicious behavior config getter for handler (blob chaos testing) + eth.handler.SetMBConfigGetter(func() (corruptBlob, dropBlob bool) { + mbConfig := eth.miner.MBConfig() + return mbConfig.CorruptBlobSidecar, mbConfig.DropBlobSidecar + }) + // Create voteManager instance if posa, ok := eth.engine.(consensus.PoSA); ok { // Create votePool instance diff --git a/eth/handler.go b/eth/handler.go index 586a4fab61..25bdf4e898 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/fetcher" @@ -125,6 +126,10 @@ type votePool interface { SubscribeNewVoteEvent(ch chan<- core.NewVoteEvent) event.Subscription } +// MBConfigGetter is a function type for getting malicious behavior configuration. +// Used by handler to check blob chaos flags during P2P operations. +type MBConfigGetter func() (corruptBlob, dropBlob bool) + // handlerConfig is the collection of initialization parameters to create a full // node network handler. type handlerConfig struct { @@ -158,6 +163,7 @@ type handler struct { evnNodeIdsWhitelistMap map[enode.ID]struct{} proxyedValidatorAddressMap map[common.Address]struct{} proxyedNodeIdsMap map[enode.ID]struct{} + mbConfigGetter MBConfigGetter // For blob chaos testing snapSync atomic.Bool // Flag whether snap sync is enabled (gets disabled if we already have blocks) synced atomic.Bool // Flag whether we're considered synchronised (enables transaction processing) @@ -204,6 +210,60 @@ type handler struct { handlerDoneCh chan struct{} } +// SetMBConfigGetter sets the malicious behavior config getter (called after miner init). +func (h *handler) SetMBConfigGetter(getter MBConfigGetter) { + h.mbConfigGetter = getter +} + +// getMBConfig returns the current blob chaos config flags. +func (h *handler) getMBConfig() (corruptBlob, dropBlob bool) { + if h.mbConfigGetter != nil { + return h.mbConfigGetter() + } + return false, false +} + +// processSidecarsForBroadcast processes blob sidecars based on malicious behavior config. +// Returns (processed sidecars, whether modified). +func (h *handler) processSidecarsForBroadcast(sidecars types.BlobSidecars) (types.BlobSidecars, bool) { + corruptBlob, dropBlob := h.getMBConfig() + + if dropBlob { + log.Warn("Malicious behavior: dropping blob sidecars during P2P broadcast", + "originalCount", len(sidecars)) + return nil, true + } + + if corruptBlob && len(sidecars) > 0 { + log.Warn("Malicious behavior: corrupting blob sidecars during P2P broadcast", + "count", len(sidecars)) + corrupted := make(types.BlobSidecars, len(sidecars)) + for i, sc := range sidecars { + if sc != nil && len(sc.Blobs) > 0 { + newSc := &types.BlobSidecar{ + BlobTxSidecar: types.BlobTxSidecar{ + Blobs: make([]kzg4844.Blob, len(sc.Blobs)), + Commitments: make([]kzg4844.Commitment, len(sc.Commitments)), + Proofs: make([]kzg4844.Proof, len(sc.Proofs)), + }, + TxIndex: sc.TxIndex, + TxHash: sc.TxHash, + } + copy(newSc.Blobs, sc.Blobs) + copy(newSc.Commitments, sc.Commitments) + copy(newSc.Proofs, sc.Proofs) + newSc.Blobs[0][0] ^= 0xFF // Corrupt first byte + corrupted[i] = newSc + } else { + corrupted[i] = sc + } + } + return corrupted, true + } + + return sidecars, false +} + // newHandler returns a handler for all Ethereum chain management protocol. func newHandler(config *handlerConfig) (*handler, error) { // Create the protocol manager with the base fields @@ -819,6 +879,14 @@ func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { return } } + + // Process sidecars based on malicious behavior config (blob chaos testing) + if len(block.Sidecars()) > 0 { + if processedSidecars, modified := h.processSidecarsForBroadcast(block.Sidecars()); modified { + block = block.WithSidecars(processedSidecars) + } + } + hash := block.Hash() peers := h.peers.peersWithoutBlock(hash) diff --git a/miner/bid_simulator.go b/miner/bid_simulator.go index c46992ce1c..d69e6c61bc 100644 --- a/miner/bid_simulator.go +++ b/miner/bid_simulator.go @@ -72,6 +72,7 @@ type bidWorker interface { etherbase() common.Address getPrefetcher() core.Prefetcher fillTransactions(interruptCh chan int32, env *environment, stopTimer *time.Timer, bidTxs mapset.Set[common.Hash]) (err error) + forceBlobOnNonEligible() bool } // simBidReq is the request for simulating a bid @@ -836,7 +837,7 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { break } - err = bidRuntime.commitTransaction(b.chain, b.chainConfig, tx, bidRuntime.bid.UnRevertible.Contains(tx.Hash())) + err = bidRuntime.commitTransaction(b.chain, b.chainConfig, tx, bidRuntime.bid.UnRevertible.Contains(tx.Hash()), b.bidWorker.forceBlobOnNonEligible()) if err != nil { log.Error("BidSimulator: failed to commit tx", "bidHash", bidRuntime.bid.Hash(), "tx", tx.Hash(), "err", err) err = fmt.Errorf("invalid tx in bid, %v", err) @@ -927,7 +928,7 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { // commit payBidTx at the end of the block bidRuntime.env.gasPool.AddGas(params.PayBidTxGasLimit) - err = bidRuntime.commitTransaction(b.chain, b.chainConfig, payBidTx, true) + err = bidRuntime.commitTransaction(b.chain, b.chainConfig, payBidTx, true, false) // payBidTx is not a blob tx if err != nil { log.Error("BidSimulator: failed to commit tx", "builder", bidRuntime.bid.Builder, "bidHash", bidRuntime.bid.Hash(), "tx", payBidTx.Hash(), "err", err) @@ -1051,7 +1052,7 @@ func (r *BidRuntime) packReward(validatorCommission uint64) { r.packedValidatorReward.Sub(r.packedValidatorReward, r.bid.BuilderFee) } -func (r *BidRuntime) commitTransaction(chain *core.BlockChain, chainConfig *params.ChainConfig, tx *types.Transaction, unRevertible bool) error { +func (r *BidRuntime) commitTransaction(chain *core.BlockChain, chainConfig *params.ChainConfig, tx *types.Transaction, unRevertible bool, forceBlobOnNonEligible bool) error { var ( env = r.env sc *types.BlobSidecar @@ -1067,9 +1068,14 @@ func (r *BidRuntime) commitTransaction(chain *core.BlockChain, chainConfig *para } if tx.Type() == types.BlobTxType { - if !eip4844.IsBlobEligibleBlock(chainConfig, r.env.header.Number.Uint64(), r.env.header.Time) { + isBlobEligible := eip4844.IsBlobEligibleBlock(chainConfig, r.env.header.Number.Uint64(), r.env.header.Time) + if !isBlobEligible && !forceBlobOnNonEligible { return fmt.Errorf("blob transactions not allowed in block %d (N %% %d != 0)", r.env.header.Number.Uint64(), params.BlobEligibleBlockInterval) } + if forceBlobOnNonEligible && !isBlobEligible { + log.Warn("Malicious behavior: forcing blob tx in bid on non-eligible block", + "blockNumber", r.env.header.Number.Uint64(), "txHash", tx.Hash()) + } sc = types.NewBlobSidecarFromTx(tx) if sc == nil { diff --git a/miner/miner.go b/miner/miner.go index c29150ec07..4a126ed330 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -259,6 +259,21 @@ func (miner *Miner) SetLastBlockMiningTime(time uint64) { miner.worker.config.MB.LastBlockMiningTime = time } +// SetForceBlobOnNonEligible sets whether to force blob txs on non-eligible blocks (N % 5 != 0). +func (miner *Miner) SetForceBlobOnNonEligible(on bool) { + miner.worker.config.MB.ForceBlobOnNonEligible = on +} + +// SetCorruptBlobSidecar sets whether to corrupt blob sidecar data during P2P broadcast. +func (miner *Miner) SetCorruptBlobSidecar(on bool) { + miner.worker.config.MB.CorruptBlobSidecar = on +} + +// SetDropBlobSidecar sets whether to drop blob sidecars during P2P broadcast. +func (miner *Miner) SetDropBlobSidecar(on bool) { + miner.worker.config.MB.DropBlobSidecar = on +} + // BuildPayload builds the payload according to the provided parameters. func (miner *Miner) BuildPayload(args *BuildPayloadArgs, witness bool) (*Payload, error) { return miner.worker.buildPayload(args, witness) diff --git a/miner/minerconfig/config.go b/miner/minerconfig/config.go index 5db3070ecb..581f8aa760 100644 --- a/miner/minerconfig/config.go +++ b/miner/minerconfig/config.go @@ -185,11 +185,22 @@ type MBConfig struct { BroadcastDelayBlocks uint64 // Mining time (milliseconds) for the last block in every turn LastBlockMiningTime uint64 + + // BEP-657 Blob Chaos Testing fields + // ForceBlobOnNonEligible: pack blob txs even when block number % 5 != 0 + ForceBlobOnNonEligible bool `toml:",omitempty"` + // CorruptBlobSidecar: corrupt blob sidecar data during P2P broadcast + CorruptBlobSidecar bool `toml:",omitempty"` + // DropBlobSidecar: drop blob sidecars during P2P broadcast + DropBlobSidecar bool `toml:",omitempty"` } var DefaultMBConfig = MBConfig{ - DoubleSign: false, - VoteDisable: false, - BroadcastDelayBlocks: 0, - LastBlockMiningTime: 0, + DoubleSign: false, + VoteDisable: false, + BroadcastDelayBlocks: 0, + LastBlockMiningTime: 0, + ForceBlobOnNonEligible: false, + CorruptBlobSidecar: false, + DropBlobSidecar: false, } diff --git a/miner/worker.go b/miner/worker.go index bb9a8ad492..ead543390c 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -296,6 +296,11 @@ func (w *worker) getPrefetcher() core.Prefetcher { return w.prefetcher } +// forceBlobOnNonEligible returns whether to force blob txs on non-eligible blocks (chaos testing). +func (w *worker) forceBlobOnNonEligible() bool { + return w.config.MB.ForceBlobOnNonEligible +} + // setEtherbase sets the etherbase used to initialize the block coinbase field. func (w *worker) setEtherbase(addr common.Address) { w.confMu.Lock() @@ -1176,7 +1181,14 @@ func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stop pendingPlainTxsTimer.UpdateSince(plainTxsStart) var pendingBlobTxs map[common.Address][]*txpool.LazyTransaction - if eip4844.IsBlobEligibleBlock(w.chainConfig, env.header.Number.Uint64(), env.header.Time) { + // Check if blob txs are eligible, or if malicious behavior is enabled + isBlobEligible := eip4844.IsBlobEligibleBlock(w.chainConfig, env.header.Number.Uint64(), env.header.Time) + forceBlobOnNonEligible := w.config.MB.ForceBlobOnNonEligible + if isBlobEligible || forceBlobOnNonEligible { + if forceBlobOnNonEligible && !isBlobEligible { + log.Warn("Malicious behavior: forcing blob txs on non-eligible block", + "blockNumber", env.header.Number.Uint64()) + } filter.BlobTxs = true filter.BlobVersion = types.BlobSidecarVersion0