diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 586731ffbd..b1762c3fda 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -249,6 +249,11 @@ type Bor struct { fakeDiff bool // Skip difficulty verifications DevFakeAuthor bool + // The block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. + blockTime time.Duration + + lastMinedBlockTime time.Time + quit chan struct{} closeOnce sync.Once } @@ -268,6 +273,7 @@ func New( heimdallWSClient IHeimdallWSClient, genesisContracts GenesisContract, devFakeAuthor bool, + blockTime time.Duration, ) *Bor { // get bor config borConfig := chainConfig.Bor @@ -308,6 +314,7 @@ func New( HeimdallWSClient: heimdallWSClient, spanStore: spanStore, DevFakeAuthor: devFakeAuthor, + blockTime: blockTime, quit: make(chan struct{}), } @@ -986,7 +993,24 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header) e } } - header.Time = parent.Time + CalcProducerDelay(number, succession, c.config) + if c.blockTime > 0 && uint64(c.blockTime.Seconds()) < c.config.CalculatePeriod(number) { + return fmt.Errorf("the floor of custom mining block time (%v) is less than the consensus block time: %v < %v", c.blockTime, c.blockTime.Seconds(), c.config.CalculatePeriod(number)) + } + + if c.blockTime > 0 && c.config.IsRio(header.Number) { + // Only enable custom block time for Rio and later + parentActualTime := c.lastMinedBlockTime + if parentActualTime.IsZero() || parentActualTime.Before(time.Unix(int64(parent.Time), 0)) { + parentActualTime = time.Unix(int64(parent.Time), 0) + } + actualNewBlockTime := parentActualTime.Add(c.blockTime) + c.lastMinedBlockTime = actualNewBlockTime + header.Time = uint64(actualNewBlockTime.Unix()) + header.ActualTime = actualNewBlockTime + } else { + header.Time = parent.Time + CalcProducerDelay(number, succession, c.config) + } + if header.Time < uint64(time.Now().Unix()) { header.Time = uint64(time.Now().Unix()) } else { @@ -996,7 +1020,7 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header) e // need a check for hard fork as it doesn't change any consensus rules, we // still keep it for safety and testing. if c.config.IsBhilai(big.NewInt(int64(number))) && succession == 0 { - startTime := time.Unix(int64(header.Time-c.config.CalculatePeriod(number)), 0) + startTime := header.GetActualTime().Add(-time.Duration(c.config.CalculatePeriod(number)) * time.Second) time.Sleep(time.Until(startTime)) } } @@ -1202,7 +1226,7 @@ func (c *Bor) Seal(chain consensus.ChainHeaderReader, block *types.Block, witnes // Sweet, the protocol permits us to sign the block, wait for our time if c.config.IsBhilai(header.Number) { - delay = time.Until(time.Unix(int64(header.Time), 0)) // Wait until we reach header time for non-primary validators + delay = time.Until(header.GetActualTime()) // Wait until we reach header time for non-primary validators // Disable early block announcement // if successionNumber == 0 { // // For primary producers, set the delay to `header.Time - block time` instead of `header.Time` @@ -1210,7 +1234,7 @@ func (c *Bor) Seal(chain consensus.ChainHeaderReader, block *types.Block, witnes // delay = time.Until(time.Unix(int64(header.Time-c.config.CalculatePeriod(number)), 0)) // } } else { - delay = time.Until(time.Unix(int64(header.Time), 0)) // Wait until we reach header time + delay = time.Until(header.GetActualTime()) // Wait until we reach header time } // wiggle was already accounted for in header.Time, this is just for logging diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 718cd426cf..cf2a9ae3b3 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -4,6 +4,7 @@ import ( "context" "math/big" "testing" + "time" "github.com/holiman/uint256" "github.com/stretchr/testify/require" @@ -530,3 +531,204 @@ func TestSnapshot(t *testing.T) { }) } } + +func TestCustomBlockTimeValidation(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + + testCases := []struct { + name string + blockTime time.Duration + consensusPeriod uint64 + blockNumber uint64 + expectError bool + description string + }{ + { + name: "blockTime is zero (default) - should succeed", + blockTime: 0, + consensusPeriod: 2, + blockNumber: 1, + expectError: false, + description: "Default blockTime of 0 should use standard consensus delay", + }, + { + name: "blockTime equals consensus period - should succeed", + blockTime: 2 * time.Second, + consensusPeriod: 2, + blockNumber: 1, + expectError: false, + description: "Custom blockTime equal to consensus period should be valid", + }, + { + name: "blockTime greater than consensus period - should succeed", + blockTime: 5 * time.Second, + consensusPeriod: 2, + blockNumber: 1, + expectError: false, + description: "Custom blockTime greater than consensus period should be valid", + }, + { + name: "blockTime less than consensus period - should fail", + blockTime: 1 * time.Second, + consensusPeriod: 2, + blockNumber: 1, + expectError: true, + description: "Custom blockTime less than consensus period should be invalid", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": tc.consensusPeriod}, + RioBlock: big.NewInt(0), // Enable Rio from genesis + } + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + b.blockTime = tc.blockTime + + // Get genesis block as parent + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + header := &types.Header{ + Number: big.NewInt(int64(tc.blockNumber)), + ParentHash: genesis.Hash(), + } + + err := b.Prepare(chain.HeaderChain(), header) + + if tc.expectError { + require.Error(t, err, tc.description) + require.Contains(t, err.Error(), "less than the consensus block time", tc.description) + } else { + require.NoError(t, err, tc.description) + } + }) + } +} + +func TestCustomBlockTimeCalculation(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + + t.Run("sequential blocks with custom blockTime", func(t *testing.T) { + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + RioBlock: big.NewInt(0), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + b.blockTime = 5 * time.Second + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + baseTime := genesis.Time + + header1 := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + } + err := b.Prepare(chain.HeaderChain(), header1) + require.NoError(t, err) + + require.False(t, header1.ActualTime.IsZero(), "ActualTime should be set") + expectedTime := time.Unix(int64(baseTime), 0).Add(5 * time.Second) + require.Equal(t, expectedTime.Unix(), header1.ActualTime.Unix()) + }) + + t.Run("lastMinedBlockTime is zero (first block)", func(t *testing.T) { + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + RioBlock: big.NewInt(0), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + b.blockTime = 3 * time.Second + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + baseTime := genesis.Time + + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + } + + err := b.Prepare(chain.HeaderChain(), header) + require.NoError(t, err) + + expectedTime := time.Unix(int64(baseTime), 0).Add(3 * time.Second) + require.Equal(t, expectedTime.Unix(), header.ActualTime.Unix()) + }) + + t.Run("lastMinedBlockTime before parent time (fallback)", func(t *testing.T) { + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + RioBlock: big.NewInt(0), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + b.blockTime = 4 * time.Second + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + baseTime := genesis.Time + + if baseTime > 10 { + b.lastMinedBlockTime = time.Unix(int64(baseTime-10), 0) + } else { + b.lastMinedBlockTime = time.Unix(0, 0) + } + + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + } + + err := b.Prepare(chain.HeaderChain(), header) + require.NoError(t, err) + + expectedTime := time.Unix(int64(baseTime), 0).Add(4 * time.Second) + require.Equal(t, expectedTime.Unix(), header.ActualTime.Unix()) + }) +} + +func TestCustomBlockTimeBackwardCompatibility(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + + t.Run("blockTime is zero uses standard CalcProducerDelay", func(t *testing.T) { + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + ProducerDelay: map[string]uint64{"0": 3}, + BackupMultiplier: map[string]uint64{"0": 2}, + RioBlock: big.NewInt(0), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + b.blockTime = 0 + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + } + + err := b.Prepare(chain.HeaderChain(), header) + require.NoError(t, err) + + require.True(t, header.ActualTime.IsZero(), "ActualTime should not be set when blockTime is 0") + }) +} diff --git a/core/types/block.go b/core/types/block.go index f5db54413c..6f07abd4a3 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -98,6 +98,9 @@ type Header struct { MixDigest common.Hash `json:"mixHash"` Nonce BlockNonce `json:"nonce"` + // ActualTime is the actual time of the block. It is internally used by the miner. + ActualTime time.Time `json:"-" rlp:"-"` + // BaseFee was added by EIP-1559 and is ignored in legacy headers. BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` @@ -240,6 +243,13 @@ func (h *Header) ValidateTimestampOptionsPIP15(minTimestamp *uint64, maxTimestam return nil } +func (h *Header) GetActualTime() time.Time { + if h.ActualTime.IsZero() { + return time.Unix(int64(h.Time), 0) + } + return h.ActualTime +} + // Body is a simple (mutable, non-safe) data container for storing and moving // a block's data contents (transactions and uncles) together. type Body struct { diff --git a/core/types/block_test.go b/core/types/block_test.go index 1439035d70..821996dbf4 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -22,6 +22,7 @@ import ( "math/big" "reflect" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -598,3 +599,73 @@ func TestValidateTimestampOptionsPIP15(t *testing.T) { } } } + +func TestHeaderGetActualTime(t *testing.T) { + t.Parallel() + + t.Run("ActualTime is set - returns ActualTime", func(t *testing.T) { + actualTime := time.Unix(1234567890, 0) + header := &Header{ + Time: 1111111111, + ActualTime: actualTime, + } + + result := header.GetActualTime() + if !result.Equal(actualTime) { + t.Errorf("expected ActualTime %v, got %v", actualTime, result) + } + }) + + t.Run("ActualTime is zero - returns time.Unix(Time, 0)", func(t *testing.T) { + headerTime := uint64(1600000000) + header := &Header{ + Time: headerTime, + // ActualTime is not set (zero value) + } + + result := header.GetActualTime() + expected := time.Unix(int64(headerTime), 0) + if !result.Equal(expected) { + t.Errorf("expected time.Unix(%d, 0) = %v, got %v", headerTime, expected, result) + } + }) + + t.Run("both Time and ActualTime zero - returns Unix epoch", func(t *testing.T) { + header := &Header{ + Time: 0, + // ActualTime is not set (zero value) + } + + result := header.GetActualTime() + expected := time.Unix(0, 0) + if !result.Equal(expected) { + t.Errorf("expected Unix epoch %v, got %v", expected, result) + } + }) + + t.Run("far future time - ActualTime set", func(t *testing.T) { + farFuture := time.Unix(9999999999, 0) + header := &Header{ + Time: 5555555555, + ActualTime: farFuture, + } + + result := header.GetActualTime() + if !result.Equal(farFuture) { + t.Errorf("expected far future time %v, got %v", farFuture, result) + } + }) + + t.Run("far future time - ActualTime not set", func(t *testing.T) { + farFutureTimestamp := uint64(9999999999) + header := &Header{ + Time: farFutureTimestamp, + } + + result := header.GetActualTime() + expected := time.Unix(int64(farFutureTimestamp), 0) + if !result.Equal(expected) { + t.Errorf("expected time %v, got %v", expected, result) + } + }) +} diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 3a827f77a9..942bb14bb3 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -266,9 +266,10 @@ func CreateConsensusEngine(chainConfig *params.ChainConfig, ethConfig *Config, d spanner := span.NewChainSpanner(blockchainAPI, contract.ValidatorSet(), chainConfig, common.HexToAddress(chainConfig.Bor.ValidatorContract)) log.Info("Creating consensus engine", "withoutHeimdall", ethConfig.WithoutHeimdall) + log.Info("Using custom miner block time", "blockTime", ethConfig.Miner.BlockTime) if ethConfig.WithoutHeimdall { - return bor.New(chainConfig, db, blockchainAPI, spanner, nil, nil, genesisContractsClient, ethConfig.DevFakeAuthor), nil + return bor.New(chainConfig, db, blockchainAPI, spanner, nil, nil, genesisContractsClient, ethConfig.DevFakeAuthor, ethConfig.Miner.BlockTime), nil } else { if ethConfig.DevFakeAuthor { log.Warn("Sanitizing DevFakeAuthor", "Use DevFakeAuthor with", "--bor.withoutheimdall") @@ -294,7 +295,7 @@ func CreateConsensusEngine(chainConfig *params.ChainConfig, ethConfig *Config, d } } - return bor.New(chainConfig, db, blockchainAPI, spanner, heimdallClient, heimdallWSClient, genesisContractsClient, false), nil + return bor.New(chainConfig, db, blockchainAPI, spanner, heimdallClient, heimdallWSClient, genesisContractsClient, false, ethConfig.Miner.BlockTime), nil } } return beacon.New(ethash.NewFaker()), nil diff --git a/internal/cli/dumpconfig.go b/internal/cli/dumpconfig.go index 0597bfdec4..42f5e5633e 100644 --- a/internal/cli/dumpconfig.go +++ b/internal/cli/dumpconfig.go @@ -62,6 +62,7 @@ func (c *DumpconfigCommand) Run(args []string) int { userConfig.TxPool.LifeTimeRaw = userConfig.TxPool.LifeTime.String() userConfig.Sealer.GasPriceRaw = userConfig.Sealer.GasPrice.String() userConfig.Sealer.RecommitRaw = userConfig.Sealer.Recommit.String() + userConfig.Sealer.BlockTimeRaw = userConfig.Sealer.BlockTime.String() userConfig.Gpo.MaxPriceRaw = userConfig.Gpo.MaxPrice.String() userConfig.Gpo.IgnorePriceRaw = userConfig.Gpo.IgnorePrice.String() userConfig.Cache.TrieTimeoutRaw = userConfig.Cache.TrieTimeout.String() diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index c70450cff9..f6c6ad372c 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -387,6 +387,10 @@ type SealerConfig struct { RecommitRaw string `hcl:"recommit,optional" toml:"recommit,optional"` CommitInterruptFlag bool `hcl:"commitinterrupt,optional" toml:"commitinterrupt,optional"` + + // BlockTime is the block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. + BlockTime time.Duration `hcl:"-,optional" toml:"-"` + BlockTimeRaw string `hcl:"blocktime,optional" toml:"blocktime,optional"` } type JsonRPCConfig struct { @@ -760,6 +764,7 @@ func DefaultConfig() *Config { ExtraData: "", Recommit: 125 * time.Second, CommitInterruptFlag: true, + BlockTime: 0, }, Gpo: &GpoConfig{ Blocks: 20, @@ -952,6 +957,7 @@ func (c *Config) fillTimeDurations() error { }{ {"jsonrpc.evmtimeout", &c.JsonRPC.RPCEVMTimeout, &c.JsonRPC.RPCEVMTimeoutRaw}, {"miner.recommit", &c.Sealer.Recommit, &c.Sealer.RecommitRaw}, + {"miner.blocktime", &c.Sealer.BlockTime, &c.Sealer.BlockTimeRaw}, {"jsonrpc.timeouts.read", &c.JsonRPC.HttpTimeout.ReadTimeout, &c.JsonRPC.HttpTimeout.ReadTimeoutRaw}, {"jsonrpc.timeouts.write", &c.JsonRPC.HttpTimeout.WriteTimeout, &c.JsonRPC.HttpTimeout.WriteTimeoutRaw}, {"jsonrpc.timeouts.idle", &c.JsonRPC.HttpTimeout.IdleTimeout, &c.JsonRPC.HttpTimeout.IdleTimeoutRaw}, @@ -1092,6 +1098,7 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* n.Miner.GasCeil = c.Sealer.GasCeil n.Miner.ExtraData = []byte(c.Sealer.ExtraData) n.Miner.CommitInterruptFlag = c.Sealer.CommitInterruptFlag + n.Miner.BlockTime = c.Sealer.BlockTime if etherbase := c.Sealer.Etherbase; etherbase != "" { if !common.IsHexAddress(etherbase) { diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index 218d05dd52..6b56326c50 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -364,6 +364,13 @@ func (c *Command) Flags(config *Config) *flagset.Flagset { Default: c.cliConfig.Sealer.CommitInterruptFlag, Group: "Sealer", }) + f.DurationFlag(&flagset.DurationFlag{ + Name: "miner.blocktime", + Usage: "The block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time.", + Value: &c.cliConfig.Sealer.BlockTime, + Default: c.cliConfig.Sealer.BlockTime, + Group: "Sealer", + }) // ethstats f.StringFlag(&flagset.StringFlag{ diff --git a/miner/fake_miner.go b/miner/fake_miner.go index 20c3ae5955..11ce3a41cb 100644 --- a/miner/fake_miner.go +++ b/miner/fake_miner.go @@ -165,7 +165,7 @@ func NewFakeBor(t TensingObject, chainDB ethdb.Database, chainConfig *params.Cha chainConfig.Bor = params.BorUnittestChainConfig.Bor } - return bor.New(chainConfig, chainDB, ethAPIMock, spanner, heimdallClientMock, heimdallClientWSMock, contractMock, false) + return bor.New(chainConfig, chainDB, ethAPIMock, spanner, heimdallClientMock, heimdallClientWSMock, contractMock, false, 0) } func createMockSpanForTest(address common.Address, chainId string) borTypes.Span { diff --git a/miner/miner.go b/miner/miner.go index 91608e05b8..b833af1db5 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -52,6 +52,7 @@ type Config struct { GasPrice *big.Int // Minimum gas price for mining a transaction Recommit time.Duration // The time interval for miner to re-create mining work. CommitInterruptFlag bool // Interrupt commit when time is up ( default = true) + BlockTime time.Duration // The block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload } diff --git a/miner/worker.go b/miner/worker.go index 4762e1257b..a3cd41788d 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -281,6 +281,8 @@ type worker struct { interruptBlockBuilding atomic.Bool // A toggle to denote whether to stop block building or not mockTxDelay uint // A mock delay for transaction execution, only used in tests + blockTime time.Duration // The block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. + // noempty is the flag used to control whether the feature of pre-seal empty // block is enabled. The default value is false(pre-seal is enabled by default). // But in some special scenario the consensus engine will seal blocks instantaneously, @@ -316,6 +318,7 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus resubmitIntervalCh: make(chan time.Duration), resubmitAdjustCh: make(chan *intervalAdjust, resubmitAdjustChanSize), interruptCommitFlag: config.CommitInterruptFlag, + blockTime: config.BlockTime, makeWitness: makeWitness, } worker.noempty.Store(true) @@ -678,7 +681,7 @@ func (w *worker) mainLoop() { stopFn := func() {} if w.interruptCommitFlag { - stopFn = createInterruptTimer(w.current.header.Number.Uint64(), w.current.header.Time, &w.interruptBlockBuilding) + stopFn = createInterruptTimer(w.current.header.Number.Uint64(), w.current.header.GetActualTime(), &w.interruptBlockBuilding) } plainTxs := newTransactionsByPriceAndNonce(w.current.signer, txs, w.current.header.BaseFee, &w.interruptBlockBuilding) // Mixed bag of everrything, yolo @@ -1568,7 +1571,7 @@ func (w *worker) commitWork(interrupt *atomic.Int32, noempty bool, timestamp int if !noempty && w.interruptCommitFlag { // Start the timer for block building - stopFn = createInterruptTimer(work.header.Number.Uint64(), work.header.Time, &w.interruptBlockBuilding) + stopFn = createInterruptTimer(work.header.Number.Uint64(), work.header.GetActualTime(), &w.interruptBlockBuilding) } // Create an empty block based on temporary copied state for @@ -1626,13 +1629,14 @@ func (w *worker) commitWork(interrupt *atomic.Int32, noempty bool, timestamp int // createInterruptTimer creates and starts a timer based on the header's timestamp for block building // and toggles the flag when the timer expires. -func createInterruptTimer(number, timestamp uint64, interruptBlockBuilding *atomic.Bool) func() { - delay := time.Until(time.Unix(int64(timestamp), 0)) +func createInterruptTimer(number uint64, actualTimestamp time.Time, interruptBlockBuilding *atomic.Bool) func() { + delay := time.Until(actualTimestamp) // Reduce the timeout by 500ms to give some buffer for state root computation if delay > 1*time.Second { delay -= 500 * time.Millisecond } + interruptCtx, cancel := context.WithTimeout(context.Background(), delay) // Reset the flag when timer starts for building a new block. diff --git a/tests/bor/bor_test.go b/tests/bor/bor_test.go index 3e3c9843be..96cfff494f 100644 --- a/tests/bor/bor_test.go +++ b/tests/bor/bor_test.go @@ -1893,3 +1893,78 @@ func TestEarlyBlockAnnouncementPostBhilai_NonPrimary(t *testing.T) { bor.BlockTooSoonError{Number: 4, Succession: 2}, *err.(*bor.BlockTooSoonError)) } + +// TestCustomBlockTimeMining tests that a miner can successfully create blocks with a custom block time +// different from the consensus period. It sets consensus period to 1s and custom miner block time to 1.75s, +// then verifies that approximately 34 blocks (60s / 1.75s) are mined in 1 minute. +func TestCustomBlockTimeMining(t *testing.T) { + t.Parallel() + log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + fdlimit.Raise(2048) + + faucets := make([]*ecdsa.PrivateKey, 128) + for i := 0; i < len(faucets); i++ { + faucets[i], _ = crypto.GenerateKey() + } + + genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 16) + genesis.Config.Bor.Period = map[string]uint64{"0": 1} // Consensus period: 1s + genesis.Config.Bor.Sprint = map[string]uint64{"0": 16} // Sprint size: 16 blocks + genesis.Config.Bor.ProducerDelay = map[string]uint64{"0": 0} // No producer delay + genesis.Config.Bor.BackupMultiplier = map[string]uint64{"0": 2} + + genesis.Config.Bor.RioBlock = common.Big0 + + customBlockTime := 1750 * time.Millisecond + + stack, ethBackend, err := InitMinerWithBlockTime(genesis, keys[0], true, customBlockTime) + require.NoError(t, err) + defer stack.Close() + + for stack.Server().NodeInfo().Ports.Listener == 0 { + time.Sleep(250 * time.Millisecond) + } + + borEngine := ethBackend.Engine().(*bor.Bor) + borEngine.Authorize(crypto.PubkeyToAddress(keys[0].PublicKey), func(account accounts.Account, s string, data []byte) ([]byte, error) { + return crypto.Sign(crypto.Keccak256(data), keys[0]) + }) + + startBlock := ethBackend.BlockChain().CurrentBlock().Number.Uint64() + startTime := time.Now() + + err = ethBackend.StartMining() + require.NoError(t, err) + + testDuration := 60 * time.Second + time.Sleep(testDuration) + + ethBackend.StopMining() + + endBlock := ethBackend.BlockChain().CurrentBlock().Number.Uint64() + actualDuration := time.Since(startTime) + + blocksMinedCount := endBlock - startBlock + + expectedBlocks := float64(actualDuration.Seconds()) / customBlockTime.Seconds() + + // Allow 5% tolerance for timing variations + tolerance := 0.05 + minExpectedBlocks := uint64(expectedBlocks * (1 - tolerance)) + maxExpectedBlocks := uint64(expectedBlocks * (1 + tolerance)) + + log.Info("Custom block time mining test results", + "startBlock", startBlock, + "endBlock", endBlock, + "blocksMinedCount", blocksMinedCount, + "duration", actualDuration, + "customBlockTime", customBlockTime, + "expectedBlocks", expectedBlocks, + "minExpected", minExpectedBlocks, + "maxExpected", maxExpectedBlocks) + + require.GreaterOrEqual(t, blocksMinedCount, minExpectedBlocks, + fmt.Sprintf("Too few blocks mined. Expected at least %d, got %d", minExpectedBlocks, blocksMinedCount)) + require.LessOrEqual(t, blocksMinedCount, maxExpectedBlocks, + fmt.Sprintf("Too many blocks mined. Expected at most %d, got %d", maxExpectedBlocks, blocksMinedCount)) +} diff --git a/tests/bor/helper.go b/tests/bor/helper.go index ae3c771864..a4001a306b 100644 --- a/tests/bor/helper.go +++ b/tests/bor/helper.go @@ -570,6 +570,10 @@ func InitGenesis(t *testing.T, faucets []*ecdsa.PrivateKey, fileLocation string, } func InitMiner(genesis *core.Genesis, privKey *ecdsa.PrivateKey, withoutHeimdall bool) (*node.Node, *eth.Ethereum, error) { + return InitMinerWithBlockTime(genesis, privKey, withoutHeimdall, 0) +} + +func InitMinerWithBlockTime(genesis *core.Genesis, privKey *ecdsa.PrivateKey, withoutHeimdall bool, blockTime time.Duration) (*node.Node, *eth.Ethereum, error) { // Define the basic configurations for the Ethereum node datadir, err := os.MkdirTemp("", "InitMiner-"+uuid.New().String()) if err != nil { @@ -606,6 +610,7 @@ func InitMiner(genesis *core.Genesis, privKey *ecdsa.PrivateKey, withoutHeimdall GasCeil: genesis.GasLimit * 11 / 10, GasPrice: big.NewInt(1), Recommit: time.Second, + BlockTime: blockTime, }, WithoutHeimdall: withoutHeimdall, })