From 8369056027532fe5be632115fa22bb3166e7de36 Mon Sep 17 00:00:00 2001 From: Bastin <43618253+Inspector-Butters@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:09:28 +0100 Subject: [PATCH 1/5] SSZ Support for LC finality and optimistic update APIs (#14836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bundle handlers test * ssz support for optimistic and finality updates APIs * changelog PR link * delete helpers --------- Co-authored-by: RadosÅ‚aw Kapka --- beacon-chain/rpc/eth/light-client/BUILD.bazel | 3 - beacon-chain/rpc/eth/light-client/handlers.go | 51 +- .../rpc/eth/light-client/handlers_test.go | 656 +++++++++++++++--- beacon-chain/rpc/eth/light-client/helpers.go | 45 -- ...tin_lightclient-finality-optimistic-ssz.md | 3 + 5 files changed, 587 insertions(+), 171 deletions(-) delete mode 100644 beacon-chain/rpc/eth/light-client/helpers.go create mode 100644 changelog/bastin_lightclient-finality-optimistic-ssz.md diff --git a/beacon-chain/rpc/eth/light-client/BUILD.bazel b/beacon-chain/rpc/eth/light-client/BUILD.bazel index be1c31a0e3d0..cde053e5f13d 100644 --- a/beacon-chain/rpc/eth/light-client/BUILD.bazel +++ b/beacon-chain/rpc/eth/light-client/BUILD.bazel @@ -4,7 +4,6 @@ go_library( name = "go_default_library", srcs = [ "handlers.go", - "helpers.go", "server.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/light-client", @@ -17,11 +16,9 @@ go_library( "//beacon-chain/db:go_default_library", "//beacon-chain/rpc/eth/shared:go_default_library", "//beacon-chain/rpc/lookup:go_default_library", - "//beacon-chain/state:go_default_library", "//config/features:go_default_library", "//config/params:go_default_library", "//consensus-types/interfaces:go_default_library", - "//consensus-types/primitives:go_default_library", "//monitoring/tracing/trace:go_default_library", "//network/httputil:go_default_library", "//runtime/version:go_default_library", diff --git a/beacon-chain/rpc/eth/light-client/handlers.go b/beacon-chain/rpc/eth/light-client/handlers.go index 84db74dc067b..3c48a5f956ab 100644 --- a/beacon-chain/rpc/eth/light-client/handlers.go +++ b/beacon-chain/rpc/eth/light-client/handlers.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api" "github.com/prysmaticlabs/prysm/v5/api/server/structs" + lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/shared" "github.com/prysmaticlabs/prysm/v5/config/features" "github.com/prysmaticlabs/prysm/v5/config/params" @@ -182,18 +183,31 @@ func (s *Server) GetLightClientFinalityUpdate(w http.ResponseWriter, req *http.R return } - update, err := newLightClientFinalityUpdateFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), st, block, attestedState, attestedBlock, finalizedBlock) + update, err := lightclient.NewLightClientFinalityUpdateFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), st, block, attestedState, attestedBlock, finalizedBlock) if err != nil { httputil.HandleError(w, "Could not get light client finality update: "+err.Error(), http.StatusInternalServerError) return } - response := &structs.LightClientFinalityUpdateResponse{ - Version: version.String(attestedState.Version()), - Data: update, + if httputil.RespondWithSsz(req) { + ssz, err := update.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "Could not marshal finality update to SSZ: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, ssz, "light_client_finality_update.ssz") + } else { + updateStruct, err := structs.LightClientFinalityUpdateFromConsensus(update) + if err != nil { + httputil.HandleError(w, "Could not convert light client finality update to API struct: "+err.Error(), http.StatusInternalServerError) + return + } + response := &structs.LightClientFinalityUpdateResponse{ + Version: version.String(attestedState.Version()), + Data: updateStruct, + } + httputil.WriteJson(w, response) } - - httputil.WriteJson(w, response) } // GetLightClientOptimisticUpdate - implements https://github.com/ethereum/beacon-APIs/blob/263f4ed6c263c967f13279c7a9f5629b51c5fc55/apis/beacon/light_client/optimistic_update.yaml @@ -232,18 +246,31 @@ func (s *Server) GetLightClientOptimisticUpdate(w http.ResponseWriter, req *http return } - update, err := newLightClientOptimisticUpdateFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), st, block, attestedState, attestedBlock) + update, err := lightclient.NewLightClientOptimisticUpdateFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), st, block, attestedState, attestedBlock) if err != nil { httputil.HandleError(w, "Could not get light client optimistic update: "+err.Error(), http.StatusInternalServerError) return } - response := &structs.LightClientOptimisticUpdateResponse{ - Version: version.String(attestedState.Version()), - Data: update, + if httputil.RespondWithSsz(req) { + ssz, err := update.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "Could not marshal optimistic update to SSZ: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, ssz, "light_client_optimistic_update.ssz") + } else { + updateStruct, err := structs.LightClientOptimisticUpdateFromConsensus(update) + if err != nil { + httputil.HandleError(w, "Could not convert light client optimistic update to API struct: "+err.Error(), http.StatusInternalServerError) + return + } + response := &structs.LightClientOptimisticUpdateResponse{ + Version: version.String(attestedState.Version()), + Data: updateStruct, + } + httputil.WriteJson(w, response) } - - httputil.WriteJson(w, response) } // suitableBlock returns the latest block that satisfies all criteria required for creating a new update diff --git a/beacon-chain/rpc/eth/light-client/handlers_test.go b/beacon-chain/rpc/eth/light-client/handlers_test.go index 422dfcba9d40..6e744e531625 100644 --- a/beacon-chain/rpc/eth/light-client/handlers_test.go +++ b/beacon-chain/rpc/eth/light-client/handlers_test.go @@ -1105,116 +1105,226 @@ func TestLightClientHandler_GetLightClientFinalityUpdate(t *testing.T) { EnableLightClient: true, }) defer resetFn() - helpers.ClearCache() - ctx := context.Background() config := params.BeaconConfig() - slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) - attestedState, err := util.NewBeaconStateAltair() - require.NoError(t, err) - err = attestedState.SetSlot(slot.Sub(1)) - require.NoError(t, err) + t.Run("altair", func(t *testing.T) { + ctx := context.Background() + slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) - require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ - Epoch: config.AltairForkEpoch - 10, - Root: make([]byte, 32), - })) + attestedState, err := util.NewBeaconStateAltair() + require.NoError(t, err) + err = attestedState.SetSlot(slot.Sub(1)) + require.NoError(t, err) - parent := util.NewBeaconBlockAltair() - parent.Block.Slot = slot.Sub(1) + require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ + Epoch: config.AltairForkEpoch - 10, + Root: make([]byte, 32), + })) - signedParent, err := blocks.NewSignedBeaconBlock(parent) - require.NoError(t, err) + parent := util.NewBeaconBlockAltair() + parent.Block.Slot = slot.Sub(1) - parentHeader, err := signedParent.Header() - require.NoError(t, err) - attestedHeader := parentHeader.Header + signedParent, err := blocks.NewSignedBeaconBlock(parent) + require.NoError(t, err) - err = attestedState.SetLatestBlockHeader(attestedHeader) - require.NoError(t, err) - attestedStateRoot, err := attestedState.HashTreeRoot(ctx) - require.NoError(t, err) + parentHeader, err := signedParent.Header() + require.NoError(t, err) + attestedHeader := parentHeader.Header - // 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(t, err) + err = attestedState.SetLatestBlockHeader(attestedHeader) + require.NoError(t, err) + attestedStateRoot, err := attestedState.HashTreeRoot(ctx) + require.NoError(t, err) - st, err := util.NewBeaconStateAltair() - require.NoError(t, err) - err = st.SetSlot(slot) - require.NoError(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(t, err) - parentRoot, err := signedParent.Block().HashTreeRoot() - require.NoError(t, err) + st, err := util.NewBeaconStateAltair() + require.NoError(t, err) + err = st.SetSlot(slot) + require.NoError(t, err) - block := util.NewBeaconBlockAltair() - block.Block.Slot = slot - block.Block.ParentRoot = parentRoot[:] + parentRoot, err := signedParent.Block().HashTreeRoot() + require.NoError(t, err) - for i := uint64(0); i < config.SyncCommitteeSize; i++ { - block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) - } + block := util.NewBeaconBlockAltair() + block.Block.Slot = slot + block.Block.ParentRoot = parentRoot[:] - signedBlock, err := blocks.NewSignedBeaconBlock(block) - require.NoError(t, err) + for i := uint64(0); i < config.SyncCommitteeSize; i++ { + block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) + } - h, err := signedBlock.Header() - require.NoError(t, err) + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) - err = st.SetLatestBlockHeader(h.Header) - require.NoError(t, err) - stateRoot, err := st.HashTreeRoot(ctx) - require.NoError(t, err) + h, err := signedBlock.Header() + require.NoError(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(t, err) + err = st.SetLatestBlockHeader(h.Header) + require.NoError(t, err) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err) - root, err := block.Block.HashTreeRoot() - require.NoError(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(t, err) - mockBlocker := &testutil.MockBlocker{ - RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ - parentRoot: signedParent, - root: signedBlock, - }, - SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ - slot.Sub(1): signedParent, - slot: signedBlock, - }, - } - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ - root: true, - }} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot.Sub(1): attestedState, - slot: st, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com", nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + root, err := block.Block.HashTreeRoot() + require.NoError(t, err) + + mockBlocker := &testutil.MockBlocker{ + RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ + parentRoot: signedParent, + root: signedBlock, + }, + SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ + slot.Sub(1): signedParent, + slot: signedBlock, + }, + } + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ + root: true, + }} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot.Sub(1): attestedState, + slot: st, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} - s.GetLightClientFinalityUpdate(writer, request) + s.GetLightClientFinalityUpdate(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - var resp *structs.LightClientUpdateResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) - require.NoError(t, err) - require.Equal(t, "altair", resp.Version) - require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) - require.NotNil(t, resp.Data) + require.Equal(t, http.StatusOK, writer.Code) + var resp *structs.LightClientUpdateResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeader + err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) + require.NoError(t, err) + require.Equal(t, "altair", resp.Version) + require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) + require.NotNil(t, resp.Data) + }) + + t.Run("altair SSZ", func(t *testing.T) { + ctx := context.Background() + slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) + + attestedState, err := util.NewBeaconStateAltair() + require.NoError(t, err) + err = attestedState.SetSlot(slot.Sub(1)) + require.NoError(t, err) + + require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ + Epoch: config.AltairForkEpoch - 10, + Root: make([]byte, 32), + })) + + parent := util.NewBeaconBlockAltair() + parent.Block.Slot = slot.Sub(1) + + signedParent, err := blocks.NewSignedBeaconBlock(parent) + require.NoError(t, err) + + parentHeader, err := signedParent.Header() + require.NoError(t, err) + attestedHeader := parentHeader.Header + + err = attestedState.SetLatestBlockHeader(attestedHeader) + require.NoError(t, err) + attestedStateRoot, err := attestedState.HashTreeRoot(ctx) + require.NoError(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(t, err) + + st, err := util.NewBeaconStateAltair() + require.NoError(t, err) + err = st.SetSlot(slot) + require.NoError(t, err) + + parentRoot, err := signedParent.Block().HashTreeRoot() + require.NoError(t, err) + + block := util.NewBeaconBlockAltair() + block.Block.Slot = slot + block.Block.ParentRoot = parentRoot[:] + + for i := uint64(0); i < config.SyncCommitteeSize; i++ { + block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) + } + + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + h, err := signedBlock.Header() + require.NoError(t, err) + + err = st.SetLatestBlockHeader(h.Header) + require.NoError(t, err) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(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(t, err) + + root, err := block.Block.HashTreeRoot() + require.NoError(t, err) + + mockBlocker := &testutil.MockBlocker{ + RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ + parentRoot: signedParent, + root: signedBlock, + }, + SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ + slot.Sub(1): signedParent, + slot: signedBlock, + }, + } + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ + root: true, + }} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot.Sub(1): attestedState, + slot: st, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com", nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetLightClientFinalityUpdate(writer, request) + + require.Equal(t, http.StatusOK, writer.Code) + + var resp pb.LightClientFinalityUpdateAltair + err = resp.UnmarshalSSZ(writer.Body.Bytes()) + require.NoError(t, err) + require.Equal(t, attestedHeader.Slot, resp.AttestedHeader.Beacon.Slot) + require.DeepEqual(t, attestedHeader.BodyRoot, resp.AttestedHeader.Beacon.BodyRoot) + }) } func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { @@ -1335,11 +1445,11 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { require.NotNil(t, resp.Data) }) - t.Run("capella", func(t *testing.T) { + t.Run("altair SSZ", func(t *testing.T) { ctx := context.Background() - slot := primitives.Slot(config.CapellaForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) + slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) - attestedState, err := util.NewBeaconStateCapella() + attestedState, err := util.NewBeaconStateAltair() require.NoError(t, err) err = attestedState.SetSlot(slot.Sub(1)) require.NoError(t, err) @@ -1349,7 +1459,7 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { Root: make([]byte, 32), })) - parent := util.NewBeaconBlockCapella() + parent := util.NewBeaconBlockAltair() parent.Block.Slot = slot.Sub(1) signedParent, err := blocks.NewSignedBeaconBlock(parent) @@ -1369,7 +1479,7 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { signedParent, err = blocks.NewSignedBeaconBlock(parent) require.NoError(t, err) - st, err := util.NewBeaconStateCapella() + st, err := util.NewBeaconStateAltair() require.NoError(t, err) err = st.SetSlot(slot) require.NoError(t, err) @@ -1377,7 +1487,7 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { parentRoot, err := signedParent.Block().HashTreeRoot() require.NoError(t, err) - block := util.NewBeaconBlockCapella() + block := util.NewBeaconBlockAltair() block.Block.Slot = slot block.Block.ParentRoot = parentRoot[:] @@ -1428,28 +1538,26 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { ChainInfoFetcher: mockChainInfoFetcher, } request := httptest.NewRequest("GET", "http://foo.com", nil) + request.Header.Add("Accept", "application/octet-stream") writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} s.GetLightClientOptimisticUpdate(writer, request) require.Equal(t, http.StatusOK, writer.Code) - var resp *structs.LightClientUpdateResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeaderCapella - err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) + + var resp pb.LightClientOptimisticUpdateAltair + err = resp.UnmarshalSSZ(writer.Body.Bytes()) require.NoError(t, err) - require.Equal(t, "capella", resp.Version) - require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) - require.NotNil(t, resp.Data) + require.Equal(t, resp.AttestedHeader.Beacon.Slot, attestedHeader.Slot) + require.DeepEqual(t, resp.AttestedHeader.Beacon.BodyRoot, attestedHeader.BodyRoot) }) - t.Run("deneb", func(t *testing.T) { + t.Run("capella", func(t *testing.T) { ctx := context.Background() - slot := primitives.Slot(config.DenebForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) + slot := primitives.Slot(config.CapellaForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) - attestedState, err := util.NewBeaconStateDeneb() + attestedState, err := util.NewBeaconStateCapella() require.NoError(t, err) err = attestedState.SetSlot(slot.Sub(1)) require.NoError(t, err) @@ -1459,7 +1567,7 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { Root: make([]byte, 32), })) - parent := util.NewBeaconBlockDeneb() + parent := util.NewBeaconBlockCapella() parent.Block.Slot = slot.Sub(1) signedParent, err := blocks.NewSignedBeaconBlock(parent) @@ -1479,7 +1587,7 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { signedParent, err = blocks.NewSignedBeaconBlock(parent) require.NoError(t, err) - st, err := util.NewBeaconStateDeneb() + st, err := util.NewBeaconStateCapella() require.NoError(t, err) err = st.SetSlot(slot) require.NoError(t, err) @@ -1487,7 +1595,7 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { parentRoot, err := signedParent.Block().HashTreeRoot() require.NoError(t, err) - block := util.NewBeaconBlockDeneb() + block := util.NewBeaconBlockCapella() block.Block.Slot = slot block.Block.ParentRoot = parentRoot[:] @@ -1547,13 +1655,339 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) { var resp *structs.LightClientUpdateResponse err = json.Unmarshal(writer.Body.Bytes(), &resp) require.NoError(t, err) - var respHeader structs.LightClientHeaderDeneb + var respHeader structs.LightClientHeaderCapella err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) require.NoError(t, err) - require.Equal(t, "deneb", resp.Version) + require.Equal(t, "capella", resp.Version) require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) require.NotNil(t, resp.Data) }) + + t.Run("capella SSZ", func(t *testing.T) { + ctx := context.Background() + slot := primitives.Slot(config.CapellaForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) + + attestedState, err := util.NewBeaconStateCapella() + require.NoError(t, err) + err = attestedState.SetSlot(slot.Sub(1)) + require.NoError(t, err) + + require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ + Epoch: config.AltairForkEpoch - 10, + Root: make([]byte, 32), + })) + + parent := util.NewBeaconBlockCapella() + parent.Block.Slot = slot.Sub(1) + + signedParent, err := blocks.NewSignedBeaconBlock(parent) + require.NoError(t, err) + + parentHeader, err := signedParent.Header() + require.NoError(t, err) + attestedHeader := parentHeader.Header + + err = attestedState.SetLatestBlockHeader(attestedHeader) + require.NoError(t, err) + attestedStateRoot, err := attestedState.HashTreeRoot(ctx) + require.NoError(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(t, err) + + st, err := util.NewBeaconStateCapella() + require.NoError(t, err) + err = st.SetSlot(slot) + require.NoError(t, err) + + parentRoot, err := signedParent.Block().HashTreeRoot() + require.NoError(t, err) + + block := util.NewBeaconBlockCapella() + block.Block.Slot = slot + block.Block.ParentRoot = parentRoot[:] + + for i := uint64(0); i < config.SyncCommitteeSize; i++ { + block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) + } + + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + h, err := signedBlock.Header() + require.NoError(t, err) + + err = st.SetLatestBlockHeader(h.Header) + require.NoError(t, err) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(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(t, err) + + root, err := block.Block.HashTreeRoot() + require.NoError(t, err) + + mockBlocker := &testutil.MockBlocker{ + RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ + parentRoot: signedParent, + root: signedBlock, + }, + SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ + slot.Sub(1): signedParent, + slot: signedBlock, + }, + } + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ + root: true, + }} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot.Sub(1): attestedState, + slot: st, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com", nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetLightClientOptimisticUpdate(writer, request) + + require.Equal(t, http.StatusOK, writer.Code) + + var resp pb.LightClientOptimisticUpdateCapella + err = resp.UnmarshalSSZ(writer.Body.Bytes()) + require.NoError(t, err) + require.Equal(t, resp.AttestedHeader.Beacon.Slot, attestedHeader.Slot) + require.DeepEqual(t, resp.AttestedHeader.Beacon.BodyRoot, attestedHeader.BodyRoot) + }) + + t.Run("deneb", func(t *testing.T) { + ctx := context.Background() + slot := primitives.Slot(config.DenebForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) + + attestedState, err := util.NewBeaconStateDeneb() + require.NoError(t, err) + err = attestedState.SetSlot(slot.Sub(1)) + require.NoError(t, err) + + require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ + Epoch: config.AltairForkEpoch - 10, + Root: make([]byte, 32), + })) + + parent := util.NewBeaconBlockDeneb() + parent.Block.Slot = slot.Sub(1) + + signedParent, err := blocks.NewSignedBeaconBlock(parent) + require.NoError(t, err) + + parentHeader, err := signedParent.Header() + require.NoError(t, err) + attestedHeader := parentHeader.Header + + err = attestedState.SetLatestBlockHeader(attestedHeader) + require.NoError(t, err) + attestedStateRoot, err := attestedState.HashTreeRoot(ctx) + require.NoError(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(t, err) + + st, err := util.NewBeaconStateDeneb() + require.NoError(t, err) + err = st.SetSlot(slot) + require.NoError(t, err) + + parentRoot, err := signedParent.Block().HashTreeRoot() + require.NoError(t, err) + + block := util.NewBeaconBlockDeneb() + block.Block.Slot = slot + block.Block.ParentRoot = parentRoot[:] + + for i := uint64(0); i < config.SyncCommitteeSize; i++ { + block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) + } + + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + h, err := signedBlock.Header() + require.NoError(t, err) + + err = st.SetLatestBlockHeader(h.Header) + require.NoError(t, err) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(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(t, err) + + root, err := block.Block.HashTreeRoot() + require.NoError(t, err) + + mockBlocker := &testutil.MockBlocker{ + RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ + parentRoot: signedParent, + root: signedBlock, + }, + SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ + slot.Sub(1): signedParent, + slot: signedBlock, + }, + } + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ + root: true, + }} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot.Sub(1): attestedState, + slot: st, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetLightClientOptimisticUpdate(writer, request) + + require.Equal(t, http.StatusOK, writer.Code) + var resp *structs.LightClientUpdateResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeaderDeneb + err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) + require.NoError(t, err) + require.Equal(t, "deneb", resp.Version) + require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) + require.NotNil(t, resp.Data) + }) + + t.Run("deneb SSZ", func(t *testing.T) { + ctx := context.Background() + slot := primitives.Slot(config.DenebForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) + + attestedState, err := util.NewBeaconStateDeneb() + require.NoError(t, err) + err = attestedState.SetSlot(slot.Sub(1)) + require.NoError(t, err) + + require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ + Epoch: config.AltairForkEpoch - 10, + Root: make([]byte, 32), + })) + + parent := util.NewBeaconBlockDeneb() + parent.Block.Slot = slot.Sub(1) + + signedParent, err := blocks.NewSignedBeaconBlock(parent) + require.NoError(t, err) + + parentHeader, err := signedParent.Header() + require.NoError(t, err) + attestedHeader := parentHeader.Header + + err = attestedState.SetLatestBlockHeader(attestedHeader) + require.NoError(t, err) + attestedStateRoot, err := attestedState.HashTreeRoot(ctx) + require.NoError(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(t, err) + + st, err := util.NewBeaconStateDeneb() + require.NoError(t, err) + err = st.SetSlot(slot) + require.NoError(t, err) + + parentRoot, err := signedParent.Block().HashTreeRoot() + require.NoError(t, err) + + block := util.NewBeaconBlockDeneb() + block.Block.Slot = slot + block.Block.ParentRoot = parentRoot[:] + + for i := uint64(0); i < config.SyncCommitteeSize; i++ { + block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) + } + + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + h, err := signedBlock.Header() + require.NoError(t, err) + + err = st.SetLatestBlockHeader(h.Header) + require.NoError(t, err) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(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(t, err) + + root, err := block.Block.HashTreeRoot() + require.NoError(t, err) + + mockBlocker := &testutil.MockBlocker{ + RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ + parentRoot: signedParent, + root: signedBlock, + }, + SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ + slot.Sub(1): signedParent, + slot: signedBlock, + }, + } + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ + root: true, + }} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot.Sub(1): attestedState, + slot: st, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com", nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetLightClientOptimisticUpdate(writer, request) + + require.Equal(t, http.StatusOK, writer.Code) + + var resp pb.LightClientOptimisticUpdateDeneb + err = resp.UnmarshalSSZ(writer.Body.Bytes()) + require.NoError(t, err) + require.Equal(t, resp.AttestedHeader.Beacon.Slot, attestedHeader.Slot) + require.DeepEqual(t, resp.AttestedHeader.Beacon.BodyRoot, attestedHeader.BodyRoot) + }) } func TestLightClientHandler_GetLightClientEventBlock(t *testing.T) { diff --git a/beacon-chain/rpc/eth/light-client/helpers.go b/beacon-chain/rpc/eth/light-client/helpers.go deleted file mode 100644 index 779bbd447dbd..000000000000 --- a/beacon-chain/rpc/eth/light-client/helpers.go +++ /dev/null @@ -1,45 +0,0 @@ -package lightclient - -import ( - "context" - - "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" - - "github.com/prysmaticlabs/prysm/v5/api/server/structs" - lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" - "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" -) - -func newLightClientFinalityUpdateFromBeaconState( - ctx context.Context, - currentSlot primitives.Slot, - state state.BeaconState, - block interfaces.ReadOnlySignedBeaconBlock, - attestedState state.BeaconState, - attestedBlock interfaces.ReadOnlySignedBeaconBlock, - finalizedBlock interfaces.ReadOnlySignedBeaconBlock, -) (*structs.LightClientFinalityUpdate, error) { - result, err := lightclient.NewLightClientFinalityUpdateFromBeaconState(ctx, currentSlot, state, block, attestedState, attestedBlock, finalizedBlock) - if err != nil { - return nil, err - } - - return structs.LightClientFinalityUpdateFromConsensus(result) -} - -func newLightClientOptimisticUpdateFromBeaconState( - ctx context.Context, - currentSlot primitives.Slot, - state state.BeaconState, - block interfaces.ReadOnlySignedBeaconBlock, - attestedState state.BeaconState, - attestedBlock interfaces.ReadOnlySignedBeaconBlock, -) (*structs.LightClientOptimisticUpdate, error) { - result, err := lightclient.NewLightClientOptimisticUpdateFromBeaconState(ctx, currentSlot, state, block, attestedState, attestedBlock) - if err != nil { - return nil, err - } - - return structs.LightClientOptimisticUpdateFromConsensus(result) -} diff --git a/changelog/bastin_lightclient-finality-optimistic-ssz.md b/changelog/bastin_lightclient-finality-optimistic-ssz.md new file mode 100644 index 000000000000..c53abe20ecc3 --- /dev/null +++ b/changelog/bastin_lightclient-finality-optimistic-ssz.md @@ -0,0 +1,3 @@ +### Added + +- Add SSZ support to light client finality and optimistic APIs. [[PR]](https://github.com/prysmaticlabs/prysm/pull/14836) \ No newline at end of file From bf62afb27c784fb6a4ef252410847ba39e244ea7 Mon Sep 17 00:00:00 2001 From: Nishant Das Date: Thu, 27 Feb 2025 16:50:24 +0800 Subject: [PATCH 2/5] Fix Gossip Validation of Electra Attester Slashings (#14985) * Fix Attester Slashing Validation In Electra * Changelog --- beacon-chain/p2p/types/object_mapping.go | 28 +++++ beacon-chain/p2p/types/object_mapping_test.go | 7 ++ beacon-chain/sync/decode_pubsub.go | 2 + beacon-chain/sync/decode_pubsub_test.go | 115 ++++++++++-------- ...nisdas_fix_attester_slashing_validation.md | 3 + 5 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 changelog/nisdas_fix_attester_slashing_validation.md diff --git a/beacon-chain/p2p/types/object_mapping.go b/beacon-chain/p2p/types/object_mapping.go index 584dfe1e64bf..ca875f3a0132 100644 --- a/beacon-chain/p2p/types/object_mapping.go +++ b/beacon-chain/p2p/types/object_mapping.go @@ -33,6 +33,9 @@ var ( // AggregateAttestationMap maps the fork-version to the underlying data type for that // particular fork period. AggregateAttestationMap map[[4]byte]func() (ethpb.SignedAggregateAttAndProof, error) + // AttesterSlashingMap maps the fork-version to the underlying data type for that particular + // fork period. + AttesterSlashingMap map[[4]byte]func() (ethpb.AttSlashing, error) ) // InitializeDataMaps initializes all the relevant object maps. This function is called to @@ -151,4 +154,29 @@ func InitializeDataMaps() { return ðpb.SignedAggregateAttestationAndProofElectra{}, nil }, } + + // Reset our aggregate attestation map. + AttesterSlashingMap = map[[4]byte]func() (ethpb.AttSlashing, error){ + bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashing{}, nil + }, + bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashing{}, nil + }, + bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashing{}, nil + }, + bytesutil.ToBytes4(params.BeaconConfig().CapellaForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashing{}, nil + }, + bytesutil.ToBytes4(params.BeaconConfig().DenebForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashing{}, nil + }, + bytesutil.ToBytes4(params.BeaconConfig().ElectraForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashingElectra{}, nil + }, + bytesutil.ToBytes4(params.BeaconConfig().FuluForkVersion): func() (ethpb.AttSlashing, error) { + return ðpb.AttesterSlashingElectra{}, nil + }, + } } diff --git a/beacon-chain/p2p/types/object_mapping_test.go b/beacon-chain/p2p/types/object_mapping_test.go index 42a186e61b1c..8120a9cd59bd 100644 --- a/beacon-chain/p2p/types/object_mapping_test.go +++ b/beacon-chain/p2p/types/object_mapping_test.go @@ -76,6 +76,13 @@ func TestInitializeDataMaps(t *testing.T) { require.NoError(t, err) assert.Equal(t, version.Phase0, agg.Version()) } + attSlashFunc, ok := AttesterSlashingMap[bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion)] + assert.Equal(t, tt.exists, ok) + if tt.exists { + attSlash, err := attSlashFunc() + require.NoError(t, err) + assert.Equal(t, version.Phase0, attSlash.Version()) + } }) } } diff --git a/beacon-chain/sync/decode_pubsub.go b/beacon-chain/sync/decode_pubsub.go index 1ec9d079448a..d55d546ed53e 100644 --- a/beacon-chain/sync/decode_pubsub.go +++ b/beacon-chain/sync/decode_pubsub.go @@ -87,6 +87,8 @@ func extractValidDataTypeFromTopic(topic string, digest []byte, clock *startup.C return extractDataTypeFromTypeMap(types.AttestationMap, digest, clock) case p2p.AggregateAndProofSubnetTopicFormat: return extractDataTypeFromTypeMap(types.AggregateAttestationMap, digest, clock) + case p2p.AttesterSlashingSubnetTopicFormat: + return extractDataTypeFromTypeMap(types.AttesterSlashingMap, digest, clock) } return nil, nil } diff --git a/beacon-chain/sync/decode_pubsub_test.go b/beacon-chain/sync/decode_pubsub_test.go index ba8e3007fcaa..6f940b47a17e 100644 --- a/beacon-chain/sync/decode_pubsub_test.go +++ b/beacon-chain/sync/decode_pubsub_test.go @@ -137,13 +137,14 @@ func TestExtractDataType(t *testing.T) { chain blockchain.ChainInfoFetcher } tests := []struct { - name string - args args - wantBlock interfaces.ReadOnlySignedBeaconBlock - wantMd metadata.Metadata - wantAtt ethpb.Att - wantAggregate ethpb.SignedAggregateAttAndProof - wantErr bool + name string + args args + wantBlock interfaces.ReadOnlySignedBeaconBlock + wantMd metadata.Metadata + wantAtt ethpb.Att + wantAggregate ethpb.SignedAggregateAttAndProof + wantAttSlashing ethpb.AttSlashing + wantErr bool }{ { name: "no digest", @@ -156,10 +157,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV0(ðpb.MetaDataV0{}), - wantAtt: ðpb.Attestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV0(ðpb.MetaDataV0{}), + wantAtt: ðpb.Attestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, + wantAttSlashing: ðpb.AttesterSlashing{}, + wantErr: false, }, { name: "invalid digest", @@ -167,11 +169,12 @@ func TestExtractDataType(t *testing.T) { digest: []byte{0x00, 0x01}, chain: &mock.ChainService{ValidatorsRoot: [32]byte{}}, }, - wantBlock: nil, - wantMd: nil, - wantAtt: nil, - wantAggregate: nil, - wantErr: true, + wantBlock: nil, + wantMd: nil, + wantAtt: nil, + wantAggregate: nil, + wantAttSlashing: nil, + wantErr: true, }, { name: "non existent digest", @@ -179,11 +182,12 @@ func TestExtractDataType(t *testing.T) { digest: []byte{0x00, 0x01, 0x02, 0x03}, chain: &mock.ChainService{ValidatorsRoot: [32]byte{}}, }, - wantBlock: nil, - wantMd: nil, - wantAtt: nil, - wantAggregate: nil, - wantErr: true, + wantBlock: nil, + wantMd: nil, + wantAtt: nil, + wantAggregate: nil, + wantAttSlashing: nil, + wantErr: true, }, { name: "genesis fork version", @@ -196,9 +200,10 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantAtt: ðpb.Attestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, - wantErr: false, + wantAtt: ðpb.Attestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, + wantAttSlashing: ðpb.AttesterSlashing{}, + wantErr: false, }, { name: "altair fork version", @@ -211,10 +216,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), - wantAtt: ðpb.Attestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), + wantAtt: ðpb.Attestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, + wantAttSlashing: ðpb.AttesterSlashing{}, + wantErr: false, }, { name: "bellatrix fork version", @@ -227,10 +233,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), - wantAtt: ðpb.Attestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), + wantAtt: ðpb.Attestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, + wantAttSlashing: ðpb.AttesterSlashing{}, + wantErr: false, }, { name: "capella fork version", @@ -243,10 +250,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), - wantAtt: ðpb.Attestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), + wantAtt: ðpb.Attestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, + wantAttSlashing: ðpb.AttesterSlashing{}, + wantErr: false, }, { name: "deneb fork version", @@ -259,10 +267,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), - wantAtt: ðpb.Attestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), + wantAtt: ðpb.Attestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProof{}, + wantAttSlashing: ðpb.AttesterSlashing{}, + wantErr: false, }, { name: "electra fork version", @@ -275,10 +284,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), - wantAtt: ðpb.SingleAttestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProofElectra{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), + wantAtt: ðpb.SingleAttestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProofElectra{}, + wantAttSlashing: ðpb.AttesterSlashingElectra{}, + wantErr: false, }, { name: "fulu fork version", @@ -291,10 +301,11 @@ func TestExtractDataType(t *testing.T) { require.NoError(t, err) return wsb }(), - wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), - wantAtt: ðpb.SingleAttestation{}, - wantAggregate: ðpb.SignedAggregateAttestationAndProofElectra{}, - wantErr: false, + wantMd: wrapper.WrappedMetadataV1(ðpb.MetaDataV1{}), + wantAtt: ðpb.SingleAttestation{}, + wantAggregate: ðpb.SignedAggregateAttestationAndProofElectra{}, + wantAttSlashing: ðpb.AttesterSlashingElectra{}, + wantErr: false, }, } for _, tt := range tests { @@ -323,6 +334,14 @@ func TestExtractDataType(t *testing.T) { if !reflect.DeepEqual(gotAggregate, tt.wantAggregate) { t.Errorf("aggregate: got = %v, want %v", gotAggregate, tt.wantAggregate) } + gotAttSlashing, err := extractDataTypeFromTypeMap(types.AttesterSlashingMap, tt.args.digest, tt.args.chain) + if (err != nil) != tt.wantErr { + t.Errorf("attester slashing: error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotAttSlashing, tt.wantAttSlashing) { + t.Errorf("attester slashin: got = %v, want %v", gotAttSlashing, tt.wantAttSlashing) + } }) } } diff --git a/changelog/nisdas_fix_attester_slashing_validation.md b/changelog/nisdas_fix_attester_slashing_validation.md new file mode 100644 index 000000000000..c515bba15699 --- /dev/null +++ b/changelog/nisdas_fix_attester_slashing_validation.md @@ -0,0 +1,3 @@ +### Fixed + +- Check for the correct attester slashing type during gossip validation. \ No newline at end of file From e56f489d064d114b5fb0ca68a8a547daed252002 Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:11:27 -0600 Subject: [PATCH 3/5] add log for committee index on electra attesation (#14993) * adding log in the case of debugging * gaz --- changelog/james-prysm_committee-index-log.md | 3 +++ proto/prysm/v1alpha1/BUILD.bazel | 3 +++ proto/prysm/v1alpha1/attestation.go | 2 ++ proto/prysm/v1alpha1/log.go | 6 ++++++ 4 files changed, 14 insertions(+) create mode 100644 changelog/james-prysm_committee-index-log.md create mode 100644 proto/prysm/v1alpha1/log.go diff --git a/changelog/james-prysm_committee-index-log.md b/changelog/james-prysm_committee-index-log.md new file mode 100644 index 000000000000..9624a8831652 --- /dev/null +++ b/changelog/james-prysm_committee-index-log.md @@ -0,0 +1,3 @@ +### Added + +- add log to committee index when committeebits are not the expected length of 1 \ No newline at end of file diff --git a/proto/prysm/v1alpha1/BUILD.bazel b/proto/prysm/v1alpha1/BUILD.bazel index 309f0e8d385e..231268880e07 100644 --- a/proto/prysm/v1alpha1/BUILD.bazel +++ b/proto/prysm/v1alpha1/BUILD.bazel @@ -333,6 +333,7 @@ go_library( srcs = [ "attestation.go", "beacon_block.go", + "log.go", "cloners.go", "eip_7521.go", "sync_committee_mainnet.go", @@ -373,6 +374,8 @@ go_library( "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", "@org_golang_google_protobuf//types/descriptorpb:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", ], ) diff --git a/proto/prysm/v1alpha1/attestation.go b/proto/prysm/v1alpha1/attestation.go index 493595d5a329..99f7698d00f4 100644 --- a/proto/prysm/v1alpha1/attestation.go +++ b/proto/prysm/v1alpha1/attestation.go @@ -291,6 +291,8 @@ func (a *AttestationElectra) GetCommitteeIndex() primitives.CommitteeIndex { indices := a.CommitteeBits.BitIndices() if len(indices) == 0 { return 0 + } else if len(indices) != 1 { + log.WithField("indices", a.CommitteeBits).Debugf("expected 1 committee bit indice got %d", len(indices)) } return primitives.CommitteeIndex(uint64(indices[0])) } diff --git a/proto/prysm/v1alpha1/log.go b/proto/prysm/v1alpha1/log.go new file mode 100644 index 000000000000..bce408e97f7d --- /dev/null +++ b/proto/prysm/v1alpha1/log.go @@ -0,0 +1,6 @@ +package eth + +import "github.com/sirupsen/logrus" + +var logger = logrus.StandardLogger() +var log = logger.WithField("prefix", "protobuf") From b6d1866deb7018e6d7b23c860cad46e1b123b62b Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 27 Feb 2025 17:38:29 -0300 Subject: [PATCH 4/5] Split out forkchoice setup from service start (#14997) --- beacon-chain/blockchain/BUILD.bazel | 1 + beacon-chain/blockchain/process_block_test.go | 2 +- beacon-chain/blockchain/service.go | 90 +++---------------- beacon-chain/blockchain/setup_forchoice.go | 84 +++++++++++++++++ changelog/potuz_forkchoice_startup.md | 3 + 5 files changed, 100 insertions(+), 80 deletions(-) create mode 100644 beacon-chain/blockchain/setup_forchoice.go create mode 100644 changelog/potuz_forkchoice_startup.md diff --git a/beacon-chain/blockchain/BUILD.bazel b/beacon-chain/blockchain/BUILD.bazel index 78ca6aaf7f7d..317a731ec4b0 100644 --- a/beacon-chain/blockchain/BUILD.bazel +++ b/beacon-chain/blockchain/BUILD.bazel @@ -26,6 +26,7 @@ go_library( "receive_blob.go", "receive_block.go", "service.go", + "setup_forchoice.go", "tracked_proposer.go", "weak_subjectivity_checks.go", ], diff --git a/beacon-chain/blockchain/process_block_test.go b/beacon-chain/blockchain/process_block_test.go index a41e5a178deb..b7f4a39e218c 100644 --- a/beacon-chain/blockchain/process_block_test.go +++ b/beacon-chain/blockchain/process_block_test.go @@ -2039,7 +2039,7 @@ func TestNoViableHead_Reboot(t *testing.T) { require.Equal(t, genesisRoot, bytesutil.ToBytes32(headRoot)) optimistic, err := service.IsOptimistic(ctx) require.NoError(t, err) - require.Equal(t, false, optimistic) + require.Equal(t, true, optimistic) // Check that the node's justified checkpoint does not agree with the // last valid state's justified checkpoint diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index dbd7685714bd..98cb0beb284b 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -3,7 +3,6 @@ package blockchain import ( - "bytes" "context" "fmt" "runtime" @@ -23,7 +22,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filesystem" "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" f "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice" - forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/blstoexec" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings" @@ -32,7 +30,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/stategen" - "github.com/prysmaticlabs/prysm/v5/config/features" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" @@ -269,69 +266,18 @@ func (s *Service) StartFromSavedState(saved state.BeaconState) error { return err } s.originBlockRoot = originRoot - - if err := s.initializeHeadFromDB(s.ctx); err != nil { - return errors.Wrap(err, "could not set up chain info") - } - spawnCountdownIfPreGenesis(s.ctx, s.genesisTime, s.cfg.BeaconDB) - - justified, err := s.cfg.BeaconDB.JustifiedCheckpoint(s.ctx) - if err != nil { - return errors.Wrap(err, "could not get justified checkpoint") - } - if justified == nil { - return errNilJustifiedCheckpoint - } - finalized, err := s.cfg.BeaconDB.FinalizedCheckpoint(s.ctx) - if err != nil { - return errors.Wrap(err, "could not get finalized checkpoint") - } - if finalized == nil { - return errNilFinalizedCheckpoint - } - - fRoot := s.ensureRootNotZeros(bytesutil.ToBytes32(finalized.Root)) - s.cfg.ForkChoiceStore.Lock() - defer s.cfg.ForkChoiceStore.Unlock() - if err := s.cfg.ForkChoiceStore.UpdateJustifiedCheckpoint(s.ctx, &forkchoicetypes.Checkpoint{Epoch: justified.Epoch, - Root: bytesutil.ToBytes32(justified.Root)}); err != nil { - return errors.Wrap(err, "could not update forkchoice's justified checkpoint") - } - if err := s.cfg.ForkChoiceStore.UpdateFinalizedCheckpoint(&forkchoicetypes.Checkpoint{Epoch: finalized.Epoch, - Root: bytesutil.ToBytes32(finalized.Root)}); err != nil { - return errors.Wrap(err, "could not update forkchoice's finalized checkpoint") - } - s.cfg.ForkChoiceStore.SetGenesisTime(uint64(s.genesisTime.Unix())) - - st, err := s.cfg.StateGen.StateByRoot(s.ctx, fRoot) - if err != nil { - return errors.Wrap(err, "could not get finalized checkpoint state") - } - finalizedBlock, err := s.cfg.BeaconDB.Block(s.ctx, fRoot) + st, err := s.cfg.StateGen.Resume(s.ctx, s.cfg.FinalizedStateAtStartUp) if err != nil { - return errors.Wrap(err, "could not get finalized checkpoint block") - } - roblock, err := blocks.NewROBlockWithRoot(finalizedBlock, fRoot) - if err != nil { - return err - } - if err := s.cfg.ForkChoiceStore.InsertNode(s.ctx, st, roblock); err != nil { - return errors.Wrap(err, "could not insert finalized block to forkchoice") + return errors.Wrap(err, "could not get finalized state from db") } - if !features.Get().EnableStartOptimistic { - lastValidatedCheckpoint, err := s.cfg.BeaconDB.LastValidatedCheckpoint(s.ctx) - if err != nil { - return errors.Wrap(err, "could not get last validated checkpoint") - } - if bytes.Equal(finalized.Root, lastValidatedCheckpoint.Root) { - if err := s.cfg.ForkChoiceStore.SetOptimisticToValid(s.ctx, fRoot); err != nil { - return errors.Wrap(err, "could not set finalized block as validated") - } - } + spawnCountdownIfPreGenesis(s.ctx, s.genesisTime, s.cfg.BeaconDB) + if err := s.setupForkchoice(st); err != nil { + return errors.Wrap(err, "could not set up forkchoice") } // not attempting to save initial sync blocks here, because there shouldn't be any until // after the statefeed.Initialized event is fired (below) - if err := s.wsVerifier.VerifyWeakSubjectivity(s.ctx, finalized.Epoch); err != nil { + cp := s.FinalizedCheckpt() + if err := s.wsVerifier.VerifyWeakSubjectivity(s.ctx, cp.Epoch); err != nil { // Exit run time if the node failed to verify weak subjectivity checkpoint. return errors.Wrap(err, "could not verify initial checkpoint provided for chain sync") } @@ -340,7 +286,6 @@ func (s *Service) StartFromSavedState(saved state.BeaconState) error { if err := s.clockSetter.SetClock(startup.NewClock(s.genesisTime, vr)); err != nil { return errors.Wrap(err, "failed to initialize blockchain service") } - return nil } @@ -373,23 +318,10 @@ func (s *Service) originRootFromSavedState(ctx context.Context) ([32]byte, error // initializeHeadFromDB uses the finalized checkpoint and head block found in the database to set the current head. // Note that this may block until stategen replays blocks between the finalized and head blocks // if the head sync flag was specified and the gap between the finalized and head blocks is at least 128 epochs long. -func (s *Service) initializeHeadFromDB(ctx context.Context) error { - finalized, err := s.cfg.BeaconDB.FinalizedCheckpoint(ctx) - if err != nil { - return errors.Wrap(err, "could not get finalized checkpoint from db") - } - if finalized == nil { - // This should never happen. At chain start, the finalized checkpoint - // would be the genesis state and block. - return errors.New("no finalized epoch in the database") - } - finalizedRoot := s.ensureRootNotZeros(bytesutil.ToBytes32(finalized.Root)) - var finalizedState state.BeaconState - - finalizedState, err = s.cfg.StateGen.Resume(ctx, s.cfg.FinalizedStateAtStartUp) - if err != nil { - return errors.Wrap(err, "could not get finalized state from db") - } +func (s *Service) initializeHeadFromDB(ctx context.Context, finalizedState state.BeaconState) error { + cp := s.FinalizedCheckpt() + fRoot := [32]byte(cp.Root) + finalizedRoot := s.ensureRootNotZeros(fRoot) if finalizedState == nil || finalizedState.IsNil() { return errors.New("finalized state can't be nil") diff --git a/beacon-chain/blockchain/setup_forchoice.go b/beacon-chain/blockchain/setup_forchoice.go new file mode 100644 index 000000000000..3ba8d1592f4b --- /dev/null +++ b/beacon-chain/blockchain/setup_forchoice.go @@ -0,0 +1,84 @@ +package blockchain + +import ( + "bytes" + + "github.com/pkg/errors" + forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/features" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" +) + +func (s *Service) setupForkchoice(st state.BeaconState) error { + if err := s.setupForkchoiceCheckpoints(); err != nil { + return errors.Wrap(err, "could not set up forkchoice checkpoints") + } + if err := s.setupForkchoiceRoot(st); err != nil { + return errors.Wrap(err, "could not set up forkchoice root") + } + if err := s.initializeHeadFromDB(s.ctx, st); err != nil { + return errors.Wrap(err, "could not initialize head from db") + } + return nil +} + +func (s *Service) setupForkchoiceRoot(st state.BeaconState) error { + cp := s.FinalizedCheckpt() + fRoot := s.ensureRootNotZeros([32]byte(cp.Root)) + finalizedBlock, err := s.cfg.BeaconDB.Block(s.ctx, fRoot) + if err != nil { + return errors.Wrap(err, "could not get finalized checkpoint block") + } + roblock, err := blocks.NewROBlockWithRoot(finalizedBlock, fRoot) + if err != nil { + return err + } + if err := s.cfg.ForkChoiceStore.InsertNode(s.ctx, st, roblock); err != nil { + return errors.Wrap(err, "could not insert finalized block to forkchoice") + } + if !features.Get().EnableStartOptimistic { + lastValidatedCheckpoint, err := s.cfg.BeaconDB.LastValidatedCheckpoint(s.ctx) + if err != nil { + return errors.Wrap(err, "could not get last validated checkpoint") + } + if bytes.Equal(fRoot[:], lastValidatedCheckpoint.Root) { + if err := s.cfg.ForkChoiceStore.SetOptimisticToValid(s.ctx, fRoot); err != nil { + return errors.Wrap(err, "could not set finalized block as validated") + } + } + } + return nil +} + +func (s *Service) setupForkchoiceCheckpoints() error { + justified, err := s.cfg.BeaconDB.JustifiedCheckpoint(s.ctx) + if err != nil { + return errors.Wrap(err, "could not get justified checkpoint") + } + if justified == nil { + return errNilJustifiedCheckpoint + } + finalized, err := s.cfg.BeaconDB.FinalizedCheckpoint(s.ctx) + if err != nil { + return errors.Wrap(err, "could not get finalized checkpoint") + } + if finalized == nil { + return errNilFinalizedCheckpoint + } + + fRoot := s.ensureRootNotZeros(bytesutil.ToBytes32(finalized.Root)) + s.cfg.ForkChoiceStore.Lock() + defer s.cfg.ForkChoiceStore.Unlock() + if err := s.cfg.ForkChoiceStore.UpdateJustifiedCheckpoint(s.ctx, &forkchoicetypes.Checkpoint{Epoch: justified.Epoch, + Root: bytesutil.ToBytes32(justified.Root)}); err != nil { + return errors.Wrap(err, "could not update forkchoice's justified checkpoint") + } + if err := s.cfg.ForkChoiceStore.UpdateFinalizedCheckpoint(&forkchoicetypes.Checkpoint{Epoch: finalized.Epoch, + Root: fRoot}); err != nil { + return errors.Wrap(err, "could not update forkchoice's finalized checkpoint") + } + s.cfg.ForkChoiceStore.SetGenesisTime(uint64(s.genesisTime.Unix())) + return nil +} diff --git a/changelog/potuz_forkchoice_startup.md b/changelog/potuz_forkchoice_startup.md new file mode 100644 index 000000000000..a217c38ee1b4 --- /dev/null +++ b/changelog/potuz_forkchoice_startup.md @@ -0,0 +1,3 @@ +### Ignored + +- Split out forkchoice startup from the main service startup. From 9e96de033bc2a3b080184527c6e8bb117cb91ea0 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 28 Feb 2025 10:39:42 -0300 Subject: [PATCH 5/5] Add feature flag to start from any beacon block in db The new feature flag called --sync-from takes a string that can take values: - `head` or - a 0x-prefixed hex encoded beacon block root. The beacon block root or the head block root has to be known in db and has to be a descendant of the current justified checkpoint. --- beacon-chain/blockchain/service.go | 40 +++++----- beacon-chain/blockchain/setup_forchoice.go | 88 +++++++++++++++++++++- beacon-chain/db/iface/interface.go | 1 + beacon-chain/db/kv/blocks.go | 15 ++++ changelog/potuz_sync_from_head.md | 3 + config/features/config.go | 5 ++ config/features/flags.go | 7 ++ encoding/bytesutil/hex.go | 8 +- 8 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 changelog/potuz_sync_from_head.md diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index 98cb0beb284b..c248b0a3ce92 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -315,33 +315,37 @@ func (s *Service) originRootFromSavedState(ctx context.Context) ([32]byte, error return genesisBlkRoot, nil } -// initializeHeadFromDB uses the finalized checkpoint and head block found in the database to set the current head. +// initializeHeadFromDB uses the finalized checkpoint and head block root from forkchoice to set the current head. // Note that this may block until stategen replays blocks between the finalized and head blocks // if the head sync flag was specified and the gap between the finalized and head blocks is at least 128 epochs long. -func (s *Service) initializeHeadFromDB(ctx context.Context, finalizedState state.BeaconState) error { +func (s *Service) initializeHead(ctx context.Context, st state.BeaconState) error { cp := s.FinalizedCheckpt() - fRoot := [32]byte(cp.Root) - finalizedRoot := s.ensureRootNotZeros(fRoot) - - if finalizedState == nil || finalizedState.IsNil() { + fRoot := s.ensureRootNotZeros([32]byte(cp.Root)) + if st == nil || st.IsNil() { return errors.New("finalized state can't be nil") } - finalizedBlock, err := s.getBlock(ctx, finalizedRoot) + root, err := s.cfg.ForkChoiceStore.Head(s.ctx) if err != nil { - return errors.Wrap(err, "could not get finalized block") + return errors.Wrap(err, "could not get head from fork choice") } - if err := s.setHead(&head{ - finalizedRoot, - finalizedBlock, - finalizedState, - finalizedBlock.Block().Slot(), - false, - }); err != nil { - return errors.Wrap(err, "could not set head") + blk, err := s.cfg.BeaconDB.Block(ctx, root) + if err != nil { + return errors.Wrap(err, "could not get head block") } - - return nil + if root != fRoot { + st, err = s.cfg.StateGen.StateByRoot(ctx, root) + if err != nil { + return errors.Wrap(err, "could not get head state") + } + } + return errors.Wrap(s.setHead(&head{ + root, + blk, + st, + blk.Block().Slot(), + false, + }), "could not set head") } func (s *Service) startFromExecutionChain() error { diff --git a/beacon-chain/blockchain/setup_forchoice.go b/beacon-chain/blockchain/setup_forchoice.go index 3ba8d1592f4b..6aadaff49c45 100644 --- a/beacon-chain/blockchain/setup_forchoice.go +++ b/beacon-chain/blockchain/setup_forchoice.go @@ -2,28 +2,112 @@ package blockchain import ( "bytes" + "context" + "fmt" "github.com/pkg/errors" forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/features" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/time/slots" ) func (s *Service) setupForkchoice(st state.BeaconState) error { if err := s.setupForkchoiceCheckpoints(); err != nil { return errors.Wrap(err, "could not set up forkchoice checkpoints") } - if err := s.setupForkchoiceRoot(st); err != nil { + if err := s.setupForkchoiceTree(st); err != nil { return errors.Wrap(err, "could not set up forkchoice root") } - if err := s.initializeHeadFromDB(s.ctx, st); err != nil { + if err := s.initializeHead(s.ctx, st); err != nil { return errors.Wrap(err, "could not initialize head from db") } return nil } +func (s *Service) startupHeadRoot() [32]byte { + headStr := features.Get().ForceHead + cp := s.FinalizedCheckpt() + fRoot := s.ensureRootNotZeros([32]byte(cp.Root)) + if headStr == "" { + return fRoot + } + if headStr == "head" { + root, err := s.cfg.BeaconDB.HeadBlockRoot() + if err != nil { + log.WithError(err).Error("could not get head block root, starting with finalized block as head") + return fRoot + } + return root + } + root, err := bytesutil.DecodeHexWithLength(headStr, 32) + if err != nil { + log.WithError(err).Error("could not parse head root, starting with finalized block as head") + return fRoot + } + return [32]byte(root) +} + +func (s *Service) setupForkchoiceTree(st state.BeaconState) error { + headRoot := s.startupHeadRoot() + cp := s.FinalizedCheckpt() + fRoot := s.ensureRootNotZeros([32]byte(cp.Root)) + if headRoot == fRoot { + return s.setupForkchoiceRoot(st) + } + blk, err := s.cfg.BeaconDB.Block(s.ctx, headRoot) + if err != nil { + log.WithError(err).Error("could not get head block, starting with finalized block as head") + return s.setupForkchoiceRoot(st) + } + if slots.ToEpoch(blk.Block().Slot()) < cp.Epoch { + log.WithField("headRoot", fmt.Sprintf("%#x", headRoot)).Error("head block is older than finalized block, starting with finalized block as head") + return s.setupForkchoiceRoot(st) + } + chain, err := s.buildForkchoiceChain(s.ctx, blk) + if err != nil { + log.WithError(err).Error("could not build forkchoice chain, starting with finalized block as head") + return s.setupForkchoiceRoot(st) + } + s.cfg.ForkChoiceStore.Lock() + return s.cfg.ForkChoiceStore.InsertChain(s.ctx, chain) +} + +func (s *Service) buildForkchoiceChain(ctx context.Context, head interfaces.ReadOnlySignedBeaconBlock) ([]*forkchoicetypes.BlockAndCheckpoints, error) { + chain := []*forkchoicetypes.BlockAndCheckpoints{} + cp := s.FinalizedCheckpt() + fRoot := s.ensureRootNotZeros([32]byte(cp.Root)) + jp := s.CurrentJustifiedCheckpt() + root, err := head.Block().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get head block root") + } + for { + roblock, err := blocks.NewROBlockWithRoot(head, root) + if err != nil { + return nil, err + } + // This chain sets the justified checkpoint for every block, including some that are older than jp. + // This should be however safe for forkchoice at startup. + chain = append(chain, &forkchoicetypes.BlockAndCheckpoints{Block: roblock, JustifiedCheckpoint: jp, FinalizedCheckpoint: cp}) + root := head.Block().ParentRoot() + if root == fRoot { + break + } + head, err = s.cfg.BeaconDB.Block(s.ctx, root) + if err != nil { + return nil, errors.Wrap(err, "could not get block") + } + if slots.ToEpoch(head.Block().Slot()) < cp.Epoch { + return nil, errors.New("head block is not a descendant of the finalized checkpoint") + } + } + return chain, nil +} + func (s *Service) setupForkchoiceRoot(st state.BeaconState) error { cp := s.FinalizedCheckpt() fRoot := s.ensureRootNotZeros([32]byte(cp.Root)) diff --git a/beacon-chain/db/iface/interface.go b/beacon-chain/db/iface/interface.go index 81ab5fef8d5b..76e657c1a057 100644 --- a/beacon-chain/db/iface/interface.go +++ b/beacon-chain/db/iface/interface.go @@ -110,6 +110,7 @@ type HeadAccessDatabase interface { // Block related methods. HeadBlock(ctx context.Context) (interfaces.ReadOnlySignedBeaconBlock, error) + HeadBlockRoot() ([32]byte, error) SaveHeadBlockRoot(ctx context.Context, blockRoot [32]byte) error // Genesis operations. diff --git a/beacon-chain/db/kv/blocks.go b/beacon-chain/db/kv/blocks.go index 4ce3144fc91b..92e5c533ff09 100644 --- a/beacon-chain/db/kv/blocks.go +++ b/beacon-chain/db/kv/blocks.go @@ -70,6 +70,21 @@ func (s *Store) OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error) return root, err } +// HeadBlockRoot returns the latest canonical block root in the Ethereum Beacon Chain. +func (s *Store) HeadBlockRoot() ([32]byte, error) { + var root [32]byte + err := s.db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket(blocksBucket) + headRoot := bkt.Get(headBlockRootKey) + if headRoot == nil { + return errors.New("no head block root found") + } + copy(root[:], headRoot) + return nil + }) + return root, err +} + // HeadBlock returns the latest canonical block in the Ethereum Beacon Chain. func (s *Store) HeadBlock(ctx context.Context) (interfaces.ReadOnlySignedBeaconBlock, error) { ctx, span := trace.StartSpan(ctx, "BeaconDB.HeadBlock") diff --git a/changelog/potuz_sync_from_head.md b/changelog/potuz_sync_from_head.md new file mode 100644 index 000000000000..72ae69a402af --- /dev/null +++ b/changelog/potuz_sync_from_head.md @@ -0,0 +1,3 @@ +### Added + +- Added a feature flag to sync from an arbitrary beacon block root at startup. diff --git a/config/features/config.go b/config/features/config.go index 1ceb28edf7fc..037c0f044afb 100644 --- a/config/features/config.go +++ b/config/features/config.go @@ -87,6 +87,9 @@ type Flags struct { // AggregateIntervals specifies the time durations at which we aggregate attestations preparing for forkchoice. AggregateIntervals [3]time.Duration + + // Feature related flags (alignment forced in the end) + ForceHead string // ForceHead forces the head block to be a specific block root, the last head block, or the last finalized block. } var featureConfig *Flags @@ -274,6 +277,8 @@ func ConfigureBeaconChain(ctx *cli.Context) error { cfg.EnableExperimentalAttestationPool = true } + cfg.ForceHead = forceHeadFlag.Value + cfg.AggregateIntervals = [3]time.Duration{aggregateFirstInterval.Value, aggregateSecondInterval.Value, aggregateThirdInterval.Value} Init(cfg) return nil diff --git a/config/features/flags.go b/config/features/flags.go index d7a60a923f05..93b99e533a87 100644 --- a/config/features/flags.go +++ b/config/features/flags.go @@ -178,6 +178,12 @@ var ( Name: "enable-experimental-attestation-pool", Usage: "Enables an experimental attestation pool design.", } + // forceHeadFlag is a flag to force the head of the beacon chain to a specific block. + forceHeadFlag = &cli.StringFlag{ + Name: "sync-from", + Usage: "Forces the head of the beacon chain to a specific block root. Values can be 'head' or a block root." + + " The block root has to be known to the beacon node and correspond to a block newer than the current finalized checkpoint.", + } ) // devModeFlags holds list of flags that are set when development mode is on. @@ -235,6 +241,7 @@ var BeaconChainFlags = combinedFlags([]cli.Flag{ DisableCommitteeAwarePacking, EnableDiscoveryReboot, enableExperimentalAttestationPool, + forceHeadFlag, }, deprecatedBeaconFlags, deprecatedFlags, upcomingDeprecation) func combinedFlags(flags ...[]cli.Flag) []cli.Flag { diff --git a/encoding/bytesutil/hex.go b/encoding/bytesutil/hex.go index 3c9939e16798..1ba3a1e1f9bf 100644 --- a/encoding/bytesutil/hex.go +++ b/encoding/bytesutil/hex.go @@ -22,14 +22,10 @@ func IsHex(b []byte) bool { // DecodeHexWithLength takes a string and a length in bytes, // and validates whether the string is a hex and has the correct length. func DecodeHexWithLength(s string, length int) ([]byte, error) { - bytes, err := hexutil.Decode(s) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("%s is not a valid hex", s)) - } - if len(bytes) != length { + if len(s) != 2*length+2 { return nil, fmt.Errorf("%s is not length %d bytes", s, length) } - return bytes, nil + return hexutil.Decode(s) } // DecodeHexWithMaxLength takes a string and a length in bytes,