Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions consensus/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -268,6 +273,7 @@ func New(
heimdallWSClient IHeimdallWSClient,
genesisContracts GenesisContract,
devFakeAuthor bool,
blockTime time.Duration,
) *Bor {
// get bor config
borConfig := chainConfig.Bor
Expand Down Expand Up @@ -308,6 +314,7 @@ func New(
HeimdallWSClient: heimdallWSClient,
spanStore: spanStore,
DevFakeAuthor: devFakeAuthor,
blockTime: blockTime,
quit: make(chan struct{}),
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
}
Expand Down Expand Up @@ -1202,15 +1226,15 @@ 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`
// // for early block announcement instead of waiting for full block time.
// 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
Expand Down
202 changes: 202 additions & 0 deletions consensus/bor/bor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"math/big"
"testing"
"time"

"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -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 := &params.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 := &params.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 := &params.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 := &params.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 := &params.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")
})
}
10 changes: 10 additions & 0 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down Expand Up @@ -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 {
Expand Down
71 changes: 71 additions & 0 deletions core/types/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"math/big"
"reflect"
"testing"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
Expand Down Expand Up @@ -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)
}
})
}
Loading
Loading