Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
15 changes: 15 additions & 0 deletions chain/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ type SpecData struct {
ElectraForkTime uint64 `mapstructure:"electra-fork-time"`
// Electra1ForkTime is the time at which the Electra1 fork is activated.
Electra1ForkTime uint64 `mapstructure:"electra-one-fork-time"`
// FuluForkTime is the time at which the Fulu fork is activated (Fusaka CL fork).
FuluForkTime uint64 `mapstructure:"fulu-fork-time"`

// State list lengths
//
Expand Down Expand Up @@ -170,4 +172,17 @@ type SpecData struct {
// MinValidatorWithdrawabilityDelay is defined in the Electra spec and introduces
// withdrawability delays to allow for slashing.
MinValidatorWithdrawabilityDelay uint64 `mapstructure:"min-validator-withdrawability-delay"`

// Fulu Value Changes
//
// HysteresisQuotientFulu is the hysteresis quotient for the Fulu fork (BRIP-0008).
HysteresisQuotientFulu uint64 `mapstructure:"hysteresis-quotient-fulu"`
// HysteresisUpwardMultiplierFulu is the hysteresis upward multiplier for the Fulu fork.
HysteresisUpwardMultiplierFulu uint64 `mapstructure:"hysteresis-upward-multiplier-fulu"`
// EVMInflationAddressFulu is the address on the EVM which will receive the
// inflation amount of native EVM balance through a withdrawal every block in the Fulu fork.
EVMInflationAddressFulu common.ExecutionAddress `mapstructure:"evm-inflation-address-fulu"`
// EVMInflationPerBlockFulu is the amount of native EVM balance (in Gwei) to be
// minted to the EVMInflationAddressFulu via a withdrawal every block in the Fulu fork.
EVMInflationPerBlockFulu uint64 `mapstructure:"evm-inflation-per-block-fulu"`
}
3 changes: 3 additions & 0 deletions chain/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import (
// ActiveForkVersionForTimestamp returns the active fork version for a given timestamp.
func (s spec) ActiveForkVersionForTimestamp(timestamp math.U64) common.Version {
time := timestamp.Unwrap()
if time >= s.FuluForkTime() {
return version.Fulu()
}
if time >= s.Electra1ForkTime() {
return version.Electra1()
}
Expand Down
1 change: 1 addition & 0 deletions chain/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var spec, _ = chain.NewSpec(
Deneb1ForkTime: 9 * 32 * 2,
ElectraForkTime: 10 * 32 * 2,
Electra1ForkTime: 11 * 32 * 2,
FuluForkTime: 12 * 32 * 2,
SlotsPerEpoch: 32,
MinEpochsForBlobsSidecarsRequest: 5,
MaxWithdrawalsPerPayload: 2,
Expand Down
43 changes: 33 additions & 10 deletions chain/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,17 @@ type HysteresisSpec interface {
// HysteresisQuotient returns the quotient used in effective balance
// calculations to create hysteresis. This provides resistance to small
// balance changes triggering effective balance updates.
HysteresisQuotient() math.U64
// The value is fork-gated by timestamp (updated in Fulu per BRIP-0008).
HysteresisQuotient(timestamp math.U64) math.U64

// HysteresisDownwardMultiplier returns the multiplier used when checking
// if the effective balance should be decreased.
HysteresisDownwardMultiplier() math.U64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code its unclear why HysteresisUpwardMultiplier takes an argument but HysteresisDownwardMultiplier does not. Since HysteresisDownwardMultiplier does not change in the fork there is no need to, but maybe add an inline comment (or pass a unused timetsamp)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment in 3cdc138


// HysteresisUpwardMultiplier returns the multiplier used when checking
// if the effective balance should be increased.
HysteresisUpwardMultiplier() math.U64
// The value is fork-gated by timestamp (updated in Fulu per BRIP-0008).
HysteresisUpwardMultiplier(timestamp math.U64) math.U64
}

type DepositSpec interface {
Expand Down Expand Up @@ -109,6 +111,9 @@ type ForkSpec interface {

// Electra1ForkTime returns the time at which the Electra1 fork takes effect.
Electra1ForkTime() uint64

// FuluForkTime returns the time at which the Fulu fork takes effect.
FuluForkTime() uint64
}

type BlobSpec interface {
Expand Down Expand Up @@ -269,6 +274,7 @@ func (s spec) validate() error {
s.Data.Deneb1ForkTime,
s.Data.ElectraForkTime,
s.Data.Electra1ForkTime,
s.Data.FuluForkTime,
}
for i := 1; i < len(orderedForkTimes); i++ {
prev, cur := orderedForkTimes[i-1], orderedForkTimes[i]
Expand Down Expand Up @@ -332,15 +338,23 @@ func (s spec) EffectiveBalanceIncrement() math.Gwei {
return math.Gwei(s.Data.EffectiveBalanceIncrement)
}

func (s spec) HysteresisQuotient() math.U64 {
func (s spec) HysteresisQuotient(timestamp math.U64) math.U64 {
fv := s.ActiveForkVersionForTimestamp(timestamp)
if version.EqualsOrIsAfter(fv, version.Fulu()) {
return math.U64(s.Data.HysteresisQuotientFulu)
}
return math.U64(s.Data.HysteresisQuotient)
}

func (s spec) HysteresisDownwardMultiplier() math.U64 {
return math.U64(s.Data.HysteresisDownwardMultiplier)
}

func (s spec) HysteresisUpwardMultiplier() math.U64 {
func (s spec) HysteresisUpwardMultiplier(timestamp math.U64) math.U64 {
fv := s.ActiveForkVersionForTimestamp(timestamp)
if version.EqualsOrIsAfter(fv, version.Fulu()) {
return math.U64(s.Data.HysteresisUpwardMultiplierFulu)
}
return math.U64(s.Data.HysteresisUpwardMultiplier)
}

Expand Down Expand Up @@ -447,6 +461,11 @@ func (s spec) Electra1ForkTime() uint64 {
return s.Data.Electra1ForkTime
}

// FuluForkTime returns the timestamp of the Fulu fork.
func (s spec) FuluForkTime() uint64 {
return s.Data.FuluForkTime
}

// EpochsPerHistoricalVector returns the number of epochs per historical vector.
func (s spec) EpochsPerHistoricalVector() uint64 {
return s.Data.EpochsPerHistoricalVector
Expand Down Expand Up @@ -518,10 +537,12 @@ func (s spec) ValidatorSetCap() uint64 {
// inflation amount of native EVM balance through a withdrawal every block.
func (s spec) EVMInflationAddress(timestamp math.U64) common.ExecutionAddress {
fv := s.ActiveForkVersionForTimestamp(timestamp)
switch fv {
case version.Deneb1(), version.Electra(), version.Electra1():
switch {
case version.EqualsOrIsAfter(fv, version.Fulu()):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not check for exact version here? If we add Fulu1 (or future fork) then we would silently return here instead of panicking which is how the switch was written explicitly before.

Suggested change
case version.EqualsOrIsAfter(fv, version.Fulu()):
case version.Equals(fv, version.Fulu()):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I'd argue it's more correct now. For example the value we set in Deneb1 EVMInflationAddressDeneb1 is actually valid from fork Deneb1 and onwards (including Electra and Electra1) until its overriden with a new value. Which is now EVMInflationAddressFulu; this value is valid until overriden again. So for Fulu1 it would also be used by default.

Also adjusted other functions to work like this in 4d88d47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that, but I am concerned there may be a slight chance that we miss updating all call sites when adding a non compatible fork. Before this PR, we would see a panic immedietly and then have to evaluate if its valid to current handling (adding the fork to the case) , or whether it needs a separate handling (like here for fulu).

Copy link
Copy Markdown
Contributor Author

@calbera calbera Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a chain spec value needs to be updated via hard fork, there needs to be an explicit change via a PR. Hence why we don't create a new variable for every value of the chain spec in every new fork.

If a chain spec value is being called on an unsupported fork version, then we should panic.

IMO this convention means we actually always implicitly "evaluate" the necessary conditions.

all that being said, its pretty nitpicky. We can make this change in another PR throughout the repo to maintain the same standard everywhere. Will revert back to old way on this PR for now.

return s.Data.EVMInflationAddressFulu
case version.EqualsOrIsAfter(fv, version.Deneb1()):
return s.Data.EVMInflationAddressDeneb1
case version.Deneb():
case version.Equals(fv, version.Deneb()):
return s.Data.EVMInflationAddressGenesis
default:
panic(fmt.Sprintf("EVMInflationAddress not supported for this fork version: %d", fv))
Expand All @@ -532,10 +553,12 @@ func (s spec) EVMInflationAddress(timestamp math.U64) common.ExecutionAddress {
// be minted to the EVMInflationAddress via a withdrawal every block.
func (s spec) EVMInflationPerBlock(timestamp math.U64) math.Gwei {
fv := s.ActiveForkVersionForTimestamp(timestamp)
switch fv {
case version.Deneb1(), version.Electra(), version.Electra1():
switch {
case version.EqualsOrIsAfter(fv, version.Fulu()):
return math.Gwei(s.Data.EVMInflationPerBlockFulu)
case version.EqualsOrIsAfter(fv, version.Deneb1()):
return math.Gwei(s.Data.EVMInflationPerBlockDeneb1)
case version.Deneb():
case version.Equals(fv, version.Deneb()):
return math.Gwei(s.Data.EVMInflationPerBlockGenesis)
default:
panic(fmt.Sprintf("EVMInflationPerBlock not supported for this fork version: %d", fv))
Expand Down
3 changes: 3 additions & 0 deletions chain/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestValidate_ForkOrder_Success(t *testing.T) {
data.Deneb1ForkTime = 20
data.ElectraForkTime = 30
data.Electra1ForkTime = 40
data.FuluForkTime = 50

_, err := chain.NewSpec(data)
require.NoError(t, err)
Expand All @@ -55,6 +56,7 @@ func TestValidate_ForkOrder_GenesisAfterDeneb(t *testing.T) {
data.Deneb1ForkTime = 20
data.ElectraForkTime = 60
data.Electra1ForkTime = 70
data.FuluForkTime = 80

_, err := chain.NewSpec(data)
require.Error(t, err)
Expand All @@ -68,6 +70,7 @@ func TestValidate_ForkOrder_DenebAfterElectra(t *testing.T) {
data.Deneb1ForkTime = 80
data.ElectraForkTime = 40
data.Electra1ForkTime = 50
data.FuluForkTime = 90

_, err := chain.NewSpec(data)
require.Error(t, err)
Expand Down
4 changes: 2 additions & 2 deletions cli/commands/genesis/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ func executableDataToExecutionPayloadHeader(
) (*types.ExecutionPayloadHeader, error) {
eph := &types.ExecutionPayloadHeader{}

// We do not support fork versions before Deneb and after Electra1.
if version.IsAfter(forkVersion, version.Electra1()) ||
// We do not support fork versions before Deneb and after Fulu.
if version.IsAfter(forkVersion, version.Fulu()) ||
version.IsBefore(forkVersion, version.Deneb()) {
return nil, types.ErrForkVersionNotSupported
}
Expand Down
13 changes: 13 additions & 0 deletions config/spec/devnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ const (

// devnetMinValidatorWithdrawabilityDelay is the delay (in epochs) before a validator can withdraw their stake.
devnetMinValidatorWithdrawabilityDelay = 32

// devnetFuluForkTime is the timestamp at which the Fulu fork occurs on devnet.
// Set to 0 so devnet starts with Fulu active.
devnetFuluForkTime = 0

// devnetEVMInflationPerBlockFulu is the amount of native EVM balance (in units
// of Gwei) to be minted per EL block after the Fulu fork on devnet.
devnetEVMInflationPerBlockFulu = 12 * params.GWei
)

// DevnetChainSpecData is the chain.SpecData for a devnet. It is similar to mainnet but
Expand All @@ -79,6 +87,7 @@ func DevnetChainSpecData() *chain.SpecData {
specData.Deneb1ForkTime = devnetDeneb1ForkTime
specData.ElectraForkTime = devnetElectraForkTime
specData.Electra1ForkTime = devnetElectra1ForkTime
specData.FuluForkTime = devnetFuluForkTime

// EVM inflation is different from mainnet to test.
specData.EVMInflationAddressGenesis = common.MustNewExecutionAddressFromHex(devnetEVMInflationAddress)
Expand All @@ -95,6 +104,10 @@ func DevnetChainSpecData() *chain.SpecData {
specData.SlotsPerEpoch = defaultSlotsPerEpoch
specData.MinValidatorWithdrawabilityDelay = devnetMinValidatorWithdrawabilityDelay

// EVM inflation for the Fulu fork on devnet. The address remains the same as the Deneb1 fork.
specData.EVMInflationAddressFulu = common.MustNewExecutionAddressFromHex(devnetEVMInflationAddressDeneb1)
specData.EVMInflationPerBlockFulu = devnetEVMInflationPerBlockFulu

return specData
}

Expand Down
27 changes: 27 additions & 0 deletions config/spec/mainnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ const (
// These are the heights at which SBT is activated on mainnet.
mainnetSBTConsensusUpdateHeight = 9_983_085
mainnetSBTConsensusEnableHeight = 9_983_086

// mainnetFuluForkTime is the timestamp at which the Fulu fork occurs.
// TODO: Set to actual fork time before Fulu activation.
mainnetFuluForkTime = 9_999_999_999_999_999

// mainnetHysteresisQuotientFulu is the hysteresis quotient for the Fulu fork (BRIP-0008).
mainnetHysteresisQuotientFulu = 100

// mainnetHysteresisUpwardMultiplierFulu is the hysteresis upward multiplier for the Fulu fork.
mainnetHysteresisUpwardMultiplierFulu = 10

// mainnetEVMInflationAddressFulu is the address on the EVM which will receive the
// inflation amount of native EVM balance through a withdrawal every block in the Fulu fork.
// TODO: Set to actual address before Fulu activation.
mainnetEVMInflationAddressFulu = "0x0000000000000000000000000000000000000000"

// mainnetEVMInflationPerBlockFulu is the amount of native EVM balance (in Gwei) to be
// minted to the EVMInflationAddressFulu via a withdrawal every block in the Fulu fork.
// TODO: Set to actual value before Fulu activation.
mainnetEVMInflationPerBlockFulu = 0
)

// MainnetChainSpecData is the chain.SpecData for the Berachain mainnet.
Expand Down Expand Up @@ -144,6 +164,7 @@ func MainnetChainSpecData() *chain.SpecData {
Deneb1ForkTime: mainnetDeneb1ForkTime,
ElectraForkTime: mainnetElectraForkTime,
Electra1ForkTime: mainnetElectra1ForkTime,
FuluForkTime: mainnetFuluForkTime,

// State list length constants.
EpochsPerHistoricalVector: defaultEpochsPerHistoricalVector,
Expand Down Expand Up @@ -174,6 +195,12 @@ func MainnetChainSpecData() *chain.SpecData {
// Electra values.
MinActivationBalance: mainnetMinActivationBalance,
MinValidatorWithdrawabilityDelay: mainnetMinValidatorWithdrawabilityDelay,

// Fulu values (BRIP-0008 hysteresis + PoL vNext).
HysteresisQuotientFulu: mainnetHysteresisQuotientFulu,
HysteresisUpwardMultiplierFulu: mainnetHysteresisUpwardMultiplierFulu,
EVMInflationAddressFulu: common.MustNewExecutionAddressFromHex(mainnetEVMInflationAddressFulu),
EVMInflationPerBlockFulu: mainnetEVMInflationPerBlockFulu,
}

specData.Config.ConsensusUpdateHeight = mainnetSBTConsensusUpdateHeight
Expand Down
4 changes: 4 additions & 0 deletions config/spec/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func TestnetChainSpecData() *chain.SpecData {
// Timestamp of the Electra1 fork on Bepolia.
specData.Electra1ForkTime = 1_754_496_000

// Timestamp of the Fulu fork on Bepolia.
// TODO: Set to actual fork time before Fulu activation.
specData.FuluForkTime = 9_999_999_999_999_999

return specData
}

Expand Down
2 changes: 1 addition & 1 deletion consensus-types/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func NewBeaconBlockWithVersion(
forkVersion common.Version,
) (*BeaconBlock, error) {
switch forkVersion {
case version.Deneb(), version.Deneb1(), version.Electra(), version.Electra1():
case version.Deneb(), version.Deneb1(), version.Electra(), version.Electra1(), version.Fulu():
block := NewEmptyBeaconBlockWithVersion(forkVersion)
block.Slot = slot
block.ProposerIndex = proposerIndex
Expand Down
2 changes: 1 addition & 1 deletion consensus-types/types/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ func (p *ExecutionPayload) GetExcessBlobGas() math.U64 {
// ToHeader converts the ExecutionPayload to an ExecutionPayloadHeader.
func (p *ExecutionPayload) ToHeader() (*ExecutionPayloadHeader, error) {
switch p.GetForkVersion() {
case version.Deneb(), version.Deneb1(), version.Electra(), version.Electra1():
case version.Deneb(), version.Deneb1(), version.Electra(), version.Electra1(), version.Fulu():
return &ExecutionPayloadHeader{
Versionable: p.Versionable,
ParentHash: p.GetParentHash(),
Expand Down
2 changes: 1 addition & 1 deletion consensus-types/types/signed_beacon_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func NewSignedBeaconBlock(

func NewEmptySignedBeaconBlockWithVersion(forkVersion common.Version) (*SignedBeaconBlock, error) {
switch forkVersion {
case version.Deneb(), version.Deneb1(), version.Electra(), version.Electra1():
case version.Deneb(), version.Deneb1(), version.Electra(), version.Electra1(), version.Fulu():
return &SignedBeaconBlock{
BeaconBlock: NewEmptyBeaconBlockWithVersion(forkVersion),
}, nil
Expand Down
13 changes: 8 additions & 5 deletions execution/client/ethclient/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ func (s *Client) NewPayload(
executionRequests,
)

case version.Equals(forkVersion, version.Electra1()):
// Use V4P11 for Electra1 versions.
case version.Equals(forkVersion, version.Electra1()),
version.Equals(forkVersion, version.Fulu()):
// Use V4P11 for Electra1 and Fulu versions.
executionRequests, err := req.GetEncodedExecutionRequests()
if err != nil {
return nil, err
Expand Down Expand Up @@ -164,8 +165,9 @@ func (s *Client) ForkchoiceUpdated(
// Deneb versions and Electra use ForkchoiceUpdatedV3.
return s.ForkchoiceUpdatedV3(ctx, state, attrs)

case version.Equals(forkVersion, version.Electra1()):
// Electra1 uses ForkchoiceUpdatedV3P11.
case version.Equals(forkVersion, version.Electra1()),
version.Equals(forkVersion, version.Fulu()):
// Electra1 and Fulu use ForkchoiceUpdatedV3P11.
return s.ForkchoiceUpdatedV3P11(ctx, state, attrs)

default:
Expand Down Expand Up @@ -234,7 +236,8 @@ func (s *Client) GetPayload(
case version.Equals(forkVersion, version.Electra()):
return s.GetPayloadV4(ctx, payloadID, forkVersion)

case version.Equals(forkVersion, version.Electra1()):
case version.Equals(forkVersion, version.Electra1()),
version.Equals(forkVersion, version.Fulu()):
return s.GetPayloadV4P11(ctx, payloadID, forkVersion)

default:
Expand Down
2 changes: 1 addition & 1 deletion execution/client/ethclient/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestNewPayloadWithInvalidVersion(t *testing.T) {
ctx := t.Context()

n := mocks.NewPayloadRequest{}
n.On("GetForkVersion").Return(version.Electra2())
n.On("GetForkVersion").Return(version.Capella())
_, err := c.NewPayload(ctx, &n)
require.ErrorIs(t, err, ethclient.ErrInvalidVersion)
}
Expand Down
2 changes: 2 additions & 0 deletions primitives/version/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func Name(v common.Version) string {
return "electra"
case electra1:
return "electra1"
case fulu:
return "fulu"
default:
return "unknown"
}
Expand Down
1 change: 1 addition & 0 deletions primitives/version/supported.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var supportedVersions = []common.Version{
deneb1,
electra,
electra1,
fulu,
}

// GetSupportedVersions returns the supported versions of beacon-kit.
Expand Down
11 changes: 6 additions & 5 deletions primitives/version/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ var (
electra = common.Version{0x05, 0x00, 0x00, 0x00}
// electra1 is the first hardfork of Electra on Berachain mainnet.
electra1 = common.Version{0x05, 0x01, 0x00, 0x00}
// TBD if used but kept as an example.
electra2 = common.Version{0x05, 0x02, 0x00, 0x00}
// fulu is the first version of the Fulu hardfork on Berachain mainnet.
fulu = common.Version{0x06, 0x00, 0x00, 0x00}
)

// Phase0 returns phase0 as a common.Version.
Expand Down Expand Up @@ -89,7 +89,8 @@ func Electra1() common.Version {
return electra1
}

// Electra2 returns electra2 as a common.Version.
func Electra2() common.Version {
return electra2
// Fulu returns fulu as a common.Version. Fulu is the CL component of the
// Fusaka hardfork on Berachain mainnet.
func Fulu() common.Version {
return fulu
}
Loading
Loading