diff --git a/CHANGELOG.md b/CHANGELOG.md index 868c3a872fcb..a59af76a6cbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/beacon-chain/core/light-client/lightclient.go b/beacon-chain/core/light-client/lightclient.go index 3c1b8a552db1..1173ec355331 100644 --- a/beacon-chain/core/light-client/lightclient.go +++ b/beacon-chain/core/light-client/lightclient.go @@ -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 ðpbv2.LightClientHeaderContainer{ Header: ðpbv2.LightClientHeaderContainer_HeaderDeneb{ @@ -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())) } @@ -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())) } @@ -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() diff --git a/beacon-chain/core/light-client/lightclient_test.go b/beacon-chain/core/light-client/lightclient_test.go index 8c5623a83825..bbfafa6a583d 100644 --- a/beacon-chain/core/light-client/lightclient_test.go +++ b/beacon-chain/core/light-client/lightclient_test.go @@ -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") + }) + }) } diff --git a/beacon-chain/rpc/eth/light-client/handlers_test.go b/beacon-chain/rpc/eth/light-client/handlers_test.go index 0ca25649ccc2..774997d336ee 100644 --- a/beacon-chain/rpc/eth/light-client/handlers_test.go +++ b/beacon-chain/rpc/eth/light-client/handlers_test.go @@ -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() diff --git a/beacon-chain/rpc/eth/light-client/helpers.go b/beacon-chain/rpc/eth/light-client/helpers.go index 251364c1d50b..ce989a7d5672 100644 --- a/beacon-chain/rpc/eth/light-client/helpers.go +++ b/beacon-chain/rpc/eth/light-client/helpers.go @@ -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())) @@ -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) } diff --git a/config/fieldparams/mainnet.go b/config/fieldparams/mainnet.go index a773da403e95..db16f6f6edf2 100644 --- a/config/fieldparams/mainnet.go +++ b/config/fieldparams/mainnet.go @@ -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. diff --git a/config/fieldparams/minimal.go b/config/fieldparams/minimal.go index 5f135f639b43..1eeadbb1b0a5 100644 --- a/config/fieldparams/minimal.go +++ b/config/fieldparams/minimal.go @@ -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. diff --git a/testing/util/lightclient.go b/testing/util/lightclient.go index 2cf668dac89b..8e6a768ae413 100644 --- a/testing/util/lightclient.go +++ b/testing/util/lightclient.go @@ -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(ðpb.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()