Skip to content

Commit

Permalink
feat: add Electra support to light client functions (#14506)
Browse files Browse the repository at this point in the history
* add Electra to switch case in light client functions

* replace `!=` with `<` in `blockToLightClientHeaderXXX`

* add Electra tests

* update `CHANGELOG.md`

* add constant for Electra

* add constant to `minimal.go`

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
  • Loading branch information
rupam-04 and rkapka authored Oct 8, 2024
1 parent 7fc5c71 commit 56f0eb1
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- Tests to ensure sepolia config matches the official upstream yaml
- HTTP endpoint for PublishBlobs
- GetBlockV2, GetBlindedBlock, ProduceBlockV2, ProduceBlockV3: add Electra case.
- Add Electra support and tests for light client functions
- SSE implementation that sheds stuck clients. [pr](https://github.com/prysmaticlabs/prysm/pull/14413)

### Changed
Expand Down
12 changes: 6 additions & 6 deletions beacon-chain/core/light-client/lightclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,10 @@ func BlockToLightClientHeader(block interfaces.ReadOnlySignedBeaconBlock) (*ethp
HeaderCapella: capellaHeader,
},
}, nil
case version.Deneb:
case version.Deneb, version.Electra:
denebHeader, err := blockToLightClientHeaderDeneb(context.Background(), block)
if err != nil {
return nil, errors.Wrap(err, "could not get deneb header")
return nil, errors.Wrap(err, "could not get header")
}
return &ethpbv2.LightClientHeaderContainer{
Header: &ethpbv2.LightClientHeaderContainer_HeaderDeneb{
Expand All @@ -337,7 +337,7 @@ func BlockToLightClientHeader(block interfaces.ReadOnlySignedBeaconBlock) (*ethp
}

func blockToLightClientHeaderAltair(block interfaces.ReadOnlySignedBeaconBlock) (*ethpbv2.LightClientHeader, error) {
if block.Version() != version.Altair {
if block.Version() < version.Altair {
return nil, fmt.Errorf("block version is %s instead of Altair", version.String(block.Version()))
}

Expand All @@ -360,7 +360,7 @@ func blockToLightClientHeaderAltair(block interfaces.ReadOnlySignedBeaconBlock)
}

func blockToLightClientHeaderCapella(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock) (*ethpbv2.LightClientHeaderCapella, error) {
if block.Version() != version.Capella {
if block.Version() < version.Capella {
return nil, fmt.Errorf("block version is %s instead of Capella", version.String(block.Version()))
}

Expand Down Expand Up @@ -422,8 +422,8 @@ func blockToLightClientHeaderCapella(ctx context.Context, block interfaces.ReadO
}

func blockToLightClientHeaderDeneb(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock) (*ethpbv2.LightClientHeaderDeneb, error) {
if block.Version() != version.Deneb {
return nil, fmt.Errorf("block version is %s instead of Deneb", version.String(block.Version()))
if block.Version() < version.Deneb {
return nil, fmt.Errorf("block version is %s instead of Deneb/Electra", version.String(block.Version()))
}

payload, err := block.Block().Body().Execution()
Expand Down
126 changes: 126 additions & 0 deletions beacon-chain/core/light-client/lightclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,4 +599,130 @@ func TestLightClient_BlockToLightClientHeader(t *testing.T) {
require.DeepSSZEqual(t, executionPayloadProof, header.ExecutionBranch, "Execution payload proofs are not equal")
})
})

t.Run("Electra", func(t *testing.T) {
t.Run("Non-Blinded Beacon Block", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestElectra(false)

container, err := lightClient.BlockToLightClientHeader(l.Block)
require.NoError(t, err)
header := container.GetHeaderDeneb()
require.NotNil(t, header, "header is nil")

parentRoot := l.Block.Block().ParentRoot()
stateRoot := l.Block.Block().StateRoot()
bodyRoot, err := l.Block.Block().Body().HashTreeRoot()
require.NoError(t, err)

payload, err := l.Block.Block().Body().Execution()
require.NoError(t, err)

transactionsRoot, err := lightClient.ComputeTransactionsRoot(payload)
require.NoError(t, err)

withdrawalsRoot, err := lightClient.ComputeWithdrawalsRoot(payload)
require.NoError(t, err)

blobGasUsed, err := payload.BlobGasUsed()
require.NoError(t, err)

excessBlobGas, err := payload.ExcessBlobGas()
require.NoError(t, err)

executionHeader := &v11.ExecutionPayloadHeaderElectra{
ParentHash: payload.ParentHash(),
FeeRecipient: payload.FeeRecipient(),
StateRoot: payload.StateRoot(),
ReceiptsRoot: payload.ReceiptsRoot(),
LogsBloom: payload.LogsBloom(),
PrevRandao: payload.PrevRandao(),
BlockNumber: payload.BlockNumber(),
GasLimit: payload.GasLimit(),
GasUsed: payload.GasUsed(),
Timestamp: payload.Timestamp(),
ExtraData: payload.ExtraData(),
BaseFeePerGas: payload.BaseFeePerGas(),
BlockHash: payload.BlockHash(),
TransactionsRoot: transactionsRoot,
WithdrawalsRoot: withdrawalsRoot,
BlobGasUsed: blobGasUsed,
ExcessBlobGas: excessBlobGas,
}

executionPayloadProof, err := blocks.PayloadProof(l.Ctx, l.Block.Block())
require.NoError(t, err)

require.Equal(t, l.Block.Block().Slot(), header.Beacon.Slot, "Slot is not equal")
require.Equal(t, l.Block.Block().ProposerIndex(), header.Beacon.ProposerIndex, "Proposer index is not equal")
require.DeepSSZEqual(t, parentRoot[:], header.Beacon.ParentRoot, "Parent root is not equal")
require.DeepSSZEqual(t, stateRoot[:], header.Beacon.StateRoot, "State root is not equal")
require.DeepSSZEqual(t, bodyRoot[:], header.Beacon.BodyRoot, "Body root is not equal")

require.DeepSSZEqual(t, executionHeader, header.Execution, "Execution headers are not equal")

require.DeepSSZEqual(t, executionPayloadProof, header.ExecutionBranch, "Execution payload proofs are not equal")
})

t.Run("Blinded Beacon Block", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestElectra(true)

container, err := lightClient.BlockToLightClientHeader(l.Block)
require.NoError(t, err)
header := container.GetHeaderDeneb()
require.NotNil(t, header, "header is nil")

parentRoot := l.Block.Block().ParentRoot()
stateRoot := l.Block.Block().StateRoot()
bodyRoot, err := l.Block.Block().Body().HashTreeRoot()
require.NoError(t, err)

payload, err := l.Block.Block().Body().Execution()
require.NoError(t, err)

transactionsRoot, err := payload.TransactionsRoot()
require.NoError(t, err)

withdrawalsRoot, err := payload.WithdrawalsRoot()
require.NoError(t, err)

blobGasUsed, err := payload.BlobGasUsed()
require.NoError(t, err)

excessBlobGas, err := payload.ExcessBlobGas()
require.NoError(t, err)

executionHeader := &v11.ExecutionPayloadHeaderElectra{
ParentHash: payload.ParentHash(),
FeeRecipient: payload.FeeRecipient(),
StateRoot: payload.StateRoot(),
ReceiptsRoot: payload.ReceiptsRoot(),
LogsBloom: payload.LogsBloom(),
PrevRandao: payload.PrevRandao(),
BlockNumber: payload.BlockNumber(),
GasLimit: payload.GasLimit(),
GasUsed: payload.GasUsed(),
Timestamp: payload.Timestamp(),
ExtraData: payload.ExtraData(),
BaseFeePerGas: payload.BaseFeePerGas(),
BlockHash: payload.BlockHash(),
TransactionsRoot: transactionsRoot,
WithdrawalsRoot: withdrawalsRoot,
BlobGasUsed: blobGasUsed,
ExcessBlobGas: excessBlobGas,
}

executionPayloadProof, err := blocks.PayloadProof(l.Ctx, l.Block.Block())
require.NoError(t, err)

require.Equal(t, l.Block.Block().Slot(), header.Beacon.Slot, "Slot is not equal")
require.Equal(t, l.Block.Block().ProposerIndex(), header.Beacon.ProposerIndex, "Proposer index is not equal")
require.DeepSSZEqual(t, parentRoot[:], header.Beacon.ParentRoot, "Parent root is not equal")
require.DeepSSZEqual(t, stateRoot[:], header.Beacon.StateRoot, "State root is not equal")
require.DeepSSZEqual(t, bodyRoot[:], header.Beacon.BodyRoot, "Body root is not equal")

require.DeepSSZEqual(t, executionHeader, header.Execution, "Execution headers are not equal")

require.DeepSSZEqual(t, executionPayloadProof, header.ExecutionBranch, "Execution payload proofs are not equal")
})
})
}
40 changes: 40 additions & 0 deletions beacon-chain/rpc/eth/light-client/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,46 @@ func TestLightClientHandler_GetLightClientBootstrap_Deneb(t *testing.T) {
require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch)
}

func TestLightClientHandler_GetLightClientBootstrap_Electra(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestElectra(false) // result is same for true and false

slot := l.State.Slot()
stateRoot, err := l.State.HashTreeRoot(l.Ctx)
require.NoError(t, err)

mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block}
mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot}
s := &Server{
Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{
slot: l.State,
}},
Blocker: mockBlocker,
HeadFetcher: mockChainService,
}
request := httptest.NewRequest("GET", "http://foo.com/", nil)
request.SetPathValue("block_root", hexutil.Encode(stateRoot[:]))
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}

s.GetLightClientBootstrap(writer, request)
require.Equal(t, http.StatusOK, writer.Code)
var resp structs.LightClientBootstrapResponse
err = json.Unmarshal(writer.Body.Bytes(), &resp)
require.NoError(t, err)
var respHeader structs.LightClientHeader
err = json.Unmarshal(resp.Data.Header, &respHeader)
require.NoError(t, err)
require.Equal(t, "electra", resp.Version)

blockHeader, err := l.Block.Header()
require.NoError(t, err)
require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot)
require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot)

require.NotNil(t, resp.Data.CurrentSyncCommittee)
require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch)
}

func TestLightClientHandler_GetLightClientUpdatesByRangeAltair(t *testing.T) {
helpers.ClearCache()
ctx := context.Background()
Expand Down
11 changes: 8 additions & 3 deletions beacon-chain/rpc/eth/light-client/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func createLightClientBootstrap(ctx context.Context, state state.BeaconState, bl
return createLightClientBootstrapAltair(ctx, state, blk)
case version.Capella:
return createLightClientBootstrapCapella(ctx, state, blk)
case version.Deneb:
case version.Deneb, version.Electra:
return createLightClientBootstrapDeneb(ctx, state, blk)
}
return nil, fmt.Errorf("unsupported block version %s", version.String(blk.Version()))
Expand Down Expand Up @@ -226,8 +226,13 @@ func createLightClientBootstrapDeneb(ctx context.Context, state state.BeaconStat
if err != nil {
return nil, errors.Wrap(err, "could not get current sync committee proof")
}

branch := make([]string, fieldparams.SyncCommitteeBranchDepth)
var branch []string
switch block.Version() {
case version.Deneb:
branch = make([]string, fieldparams.SyncCommitteeBranchDepth)
case version.Electra:
branch = make([]string, fieldparams.SyncCommitteeBranchDepthElectra)
}
for i, proof := range currentSyncCommitteeProof {
branch[i] = hexutil.Encode(proof)
}
Expand Down
1 change: 1 addition & 0 deletions config/fieldparams/mainnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
KzgCommitmentInclusionProofDepth = 17 // Merkle proof depth for blob_kzg_commitments list item
ExecutionBranchDepth = 4 // ExecutionBranchDepth defines the number of leaves in a merkle proof of the execution payload header.
SyncCommitteeBranchDepth = 5 // SyncCommitteeBranchDepth defines the number of leaves in a merkle proof of a sync committee.
SyncCommitteeBranchDepthElectra = 6 // SyncCommitteeBranchDepthElectra defines the number of leaves in a merkle proof of a sync committee.
FinalityBranchDepth = 6 // FinalityBranchDepth defines the number of leaves in a merkle proof of the finalized checkpoint root.
PendingBalanceDepositsLimit = 134217728 // Maximum number of pending balance deposits in the beacon state.
PendingPartialWithdrawalsLimit = 134217728 // Maximum number of pending partial withdrawals in the beacon state.
Expand Down
1 change: 1 addition & 0 deletions config/fieldparams/minimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
KzgCommitmentInclusionProofDepth = 17 // Merkle proof depth for blob_kzg_commitments list item
ExecutionBranchDepth = 4 // ExecutionBranchDepth defines the number of leaves in a merkle proof of the execution payload header.
SyncCommitteeBranchDepth = 5 // SyncCommitteeBranchDepth defines the number of leaves in a merkle proof of a sync committee.
SyncCommitteeBranchDepthElectra = 6 // SyncCommitteeBranchDepthElectra defines the number of leaves in a merkle proof of a sync committee.
FinalityBranchDepth = 6 // FinalityBranchDepth defines the number of leaves in a merkle proof of the finalized checkpoint root.
PendingBalanceDepositsLimit = 134217728 // Maximum number of pending balance deposits in the beacon state.
PendingPartialWithdrawalsLimit = 64 // Maximum number of pending partial withdrawals in the beacon state.
Expand Down
112 changes: 112 additions & 0 deletions testing/util/lightclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,118 @@ func (l *TestLightClient) SetupTestDeneb(blinded bool) *TestLightClient {
return l
}

func (l *TestLightClient) SetupTestElectra(blinded bool) *TestLightClient {
ctx := context.Background()

slot := primitives.Slot(params.BeaconConfig().ElectraForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1)

attestedState, err := NewBeaconStateElectra()
require.NoError(l.T, err)
err = attestedState.SetSlot(slot)
require.NoError(l.T, err)

finalizedBlock, err := blocks.NewSignedBeaconBlock(NewBeaconBlockElectra())
require.NoError(l.T, err)
finalizedBlock.SetSlot(1)
finalizedHeader, err := finalizedBlock.Header()
require.NoError(l.T, err)
finalizedRoot, err := finalizedHeader.Header.HashTreeRoot()
require.NoError(l.T, err)

require.NoError(l.T, attestedState.SetFinalizedCheckpoint(&ethpb.Checkpoint{
Epoch: params.BeaconConfig().ElectraForkEpoch - 10,
Root: finalizedRoot[:],
}))

parent := NewBeaconBlockElectra()
parent.Block.Slot = slot

signedParent, err := blocks.NewSignedBeaconBlock(parent)
require.NoError(l.T, err)

parentHeader, err := signedParent.Header()
require.NoError(l.T, err)
attestedHeader := parentHeader.Header

err = attestedState.SetLatestBlockHeader(attestedHeader)
require.NoError(l.T, err)
attestedStateRoot, err := attestedState.HashTreeRoot(ctx)
require.NoError(l.T, err)

// get a new signed block so the root is updated with the new state root
parent.Block.StateRoot = attestedStateRoot[:]
signedParent, err = blocks.NewSignedBeaconBlock(parent)
require.NoError(l.T, err)

state, err := NewBeaconStateElectra()
require.NoError(l.T, err)
err = state.SetSlot(slot)
require.NoError(l.T, err)

parentRoot, err := signedParent.Block().HashTreeRoot()
require.NoError(l.T, err)

var signedBlock interfaces.SignedBeaconBlock
if blinded {
block := NewBlindedBeaconBlockElectra()
block.Message.Slot = slot
block.Message.ParentRoot = parentRoot[:]

for i := uint64(0); i < params.BeaconConfig().MinSyncCommitteeParticipants; i++ {
block.Message.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true)
}

signedBlock, err = blocks.NewSignedBeaconBlock(block)
require.NoError(l.T, err)

h, err := signedBlock.Header()
require.NoError(l.T, err)

err = state.SetLatestBlockHeader(h.Header)
require.NoError(l.T, err)
stateRoot, err := state.HashTreeRoot(ctx)
require.NoError(l.T, err)

// get a new signed block so the root is updated with the new state root
block.Message.StateRoot = stateRoot[:]
signedBlock, err = blocks.NewSignedBeaconBlock(block)
require.NoError(l.T, err)
} else {
block := NewBeaconBlockElectra()
block.Block.Slot = slot
block.Block.ParentRoot = parentRoot[:]

for i := uint64(0); i < params.BeaconConfig().MinSyncCommitteeParticipants; i++ {
block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true)
}

signedBlock, err = blocks.NewSignedBeaconBlock(block)
require.NoError(l.T, err)

h, err := signedBlock.Header()
require.NoError(l.T, err)

err = state.SetLatestBlockHeader(h.Header)
require.NoError(l.T, err)
stateRoot, err := state.HashTreeRoot(ctx)
require.NoError(l.T, err)

// get a new signed block so the root is updated with the new state root
block.Block.StateRoot = stateRoot[:]
signedBlock, err = blocks.NewSignedBeaconBlock(block)
require.NoError(l.T, err)
}

l.State = state
l.AttestedState = attestedState
l.AttestedBlock = signedParent
l.Block = signedBlock
l.Ctx = ctx
l.FinalizedBlock = finalizedBlock

return l
}

func (l *TestLightClient) SetupTestDenebFinalizedBlockCapella(blinded bool) *TestLightClient {
ctx := context.Background()

Expand Down

0 comments on commit 56f0eb1

Please sign in to comment.