Skip to content

Commit 55efccb

Browse files
authored
Add get pending deposits endpoint (#14941)
* Add GetPendingDeposits endpoint * add comment * add changelog * gaz * Radek' review * move JSON object params * gaz * Radek' nits xD * James' review
1 parent 961d8e1 commit 55efccb

File tree

6 files changed

+282
-0
lines changed

6 files changed

+282
-0
lines changed

api/server/structs/endpoints_beacon.go

+7
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,10 @@ type ChainHead struct {
250250
PreviousJustifiedBlockRoot string `json:"previous_justified_block_root"`
251251
OptimisticStatus bool `json:"optimistic_status"`
252252
}
253+
254+
type GetPendingDepositsResponse struct {
255+
Version string `json:"version"`
256+
ExecutionOptimistic bool `json:"execution_optimistic"`
257+
Finalized bool `json:"finalized"`
258+
Data []*PendingDeposit `json:"data"`
259+
}

beacon-chain/rpc/endpoints.go

+9
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,15 @@ func (s *Service) beaconEndpoints(
884884
handler: server.GetDepositSnapshot,
885885
methods: []string{http.MethodGet},
886886
},
887+
{
888+
template: "/eth/v1/beacon/states/{state_id}/pending_deposits",
889+
name: namespace + ".GetPendingDeposits",
890+
middleware: []middleware.Middleware{
891+
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
892+
},
893+
handler: server.GetPendingDeposits,
894+
methods: []string{http.MethodGet},
895+
},
887896
}
888897
}
889898

beacon-chain/rpc/endpoints_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func Test_endpoints(t *testing.T) {
2727
"/eth/v1/beacon/states/{state_id}/committees": {http.MethodGet},
2828
"/eth/v1/beacon/states/{state_id}/sync_committees": {http.MethodGet},
2929
"/eth/v1/beacon/states/{state_id}/randao": {http.MethodGet},
30+
"/eth/v1/beacon/states/{state_id}/pending_deposits": {http.MethodGet},
3031
"/eth/v1/beacon/headers": {http.MethodGet},
3132
"/eth/v1/beacon/headers/{block_id}": {http.MethodGet},
3233
"/eth/v1/beacon/blinded_blocks": {http.MethodPost},

beacon-chain/rpc/eth/beacon/handlers.go

+69
Original file line numberDiff line numberDiff line change
@@ -1608,3 +1608,72 @@ func (s *Server) broadcastSeenBlockSidecars(
16081608
}
16091609
return nil
16101610
}
1611+
1612+
// GetPendingDeposits returns pending deposits for state with given 'stateId'.
1613+
// Should return 400 if the state retrieved is prior to Electra.
1614+
// Supports both JSON and SSZ responses based on Accept header.
1615+
func (s *Server) GetPendingDeposits(w http.ResponseWriter, r *http.Request) {
1616+
ctx, span := trace.StartSpan(r.Context(), "beacon.GetPendingDeposits")
1617+
defer span.End()
1618+
1619+
stateId := r.PathValue("state_id")
1620+
if stateId == "" {
1621+
httputil.HandleError(w, "state_id is required in URL params", http.StatusBadRequest)
1622+
return
1623+
}
1624+
st, err := s.Stater.State(ctx, []byte(stateId))
1625+
if err != nil {
1626+
shared.WriteStateFetchError(w, err)
1627+
return
1628+
}
1629+
if st.Version() < version.Electra {
1630+
httputil.HandleError(w, "state_id is prior to electra", http.StatusBadRequest)
1631+
return
1632+
}
1633+
pd, err := st.PendingDeposits()
1634+
if err != nil {
1635+
httputil.HandleError(w, "Could not get pending deposits: "+err.Error(), http.StatusInternalServerError)
1636+
return
1637+
}
1638+
w.Header().Set(api.VersionHeader, version.String(st.Version()))
1639+
if httputil.RespondWithSsz(r) {
1640+
sszData, err := serializePendingDeposits(pd)
1641+
if err != nil {
1642+
httputil.HandleError(w, "Failed to serialize pending deposits: "+err.Error(), http.StatusInternalServerError)
1643+
return
1644+
}
1645+
httputil.WriteSsz(w, sszData, "pending_deposits.ssz")
1646+
} else {
1647+
isOptimistic, err := helpers.IsOptimistic(ctx, []byte(stateId), s.OptimisticModeFetcher, s.Stater, s.ChainInfoFetcher, s.BeaconDB)
1648+
if err != nil {
1649+
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
1650+
return
1651+
}
1652+
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
1653+
if err != nil {
1654+
httputil.HandleError(w, "Could not calculate root of latest block header: "+err.Error(), http.StatusInternalServerError)
1655+
return
1656+
}
1657+
isFinalized := s.FinalizationFetcher.IsFinalized(ctx, blockRoot)
1658+
resp := structs.GetPendingDepositsResponse{
1659+
Version: version.String(st.Version()),
1660+
ExecutionOptimistic: isOptimistic,
1661+
Finalized: isFinalized,
1662+
Data: structs.PendingDepositsFromConsensus(pd),
1663+
}
1664+
httputil.WriteJson(w, resp)
1665+
}
1666+
}
1667+
1668+
// serializePendingDeposits serializes a slice of PendingDeposit objects into a single byte array.
1669+
func serializePendingDeposits(pd []*eth.PendingDeposit) ([]byte, error) {
1670+
var result []byte
1671+
for _, d := range pd {
1672+
b, err := d.MarshalSSZ()
1673+
if err != nil {
1674+
return nil, err
1675+
}
1676+
result = append(result, b...)
1677+
}
1678+
return result, nil
1679+
}

beacon-chain/rpc/eth/beacon/handlers_test.go

+193
Original file line numberDiff line numberDiff line change
@@ -4754,3 +4754,196 @@ func Test_validateBlobSidecars(t *testing.T) {
47544754
require.NoError(t, err)
47554755
require.ErrorContains(t, "could not verify blob proof: can't verify opening proof", s.validateBlobSidecars(b, [][]byte{blob[:]}, [][]byte{proof[:]}))
47564756
}
4757+
4758+
func TestGetPendingDeposits(t *testing.T) {
4759+
st, _ := util.DeterministicGenesisStateElectra(t, 10)
4760+
4761+
validators := st.Validators()
4762+
dummySig := make([]byte, 96)
4763+
for j := 0; j < 96; j++ {
4764+
dummySig[j] = byte(j)
4765+
}
4766+
deps := make([]*eth.PendingDeposit, 10)
4767+
for i := 0; i < len(deps); i += 1 {
4768+
deps[i] = &eth.PendingDeposit{
4769+
PublicKey: validators[i].PublicKey,
4770+
WithdrawalCredentials: validators[i].WithdrawalCredentials,
4771+
Amount: 100,
4772+
Slot: 0,
4773+
Signature: dummySig,
4774+
}
4775+
}
4776+
require.NoError(t, st.SetPendingDeposits(deps))
4777+
4778+
chainService := &chainMock.ChainService{
4779+
Optimistic: false,
4780+
FinalizedRoots: map[[32]byte]bool{},
4781+
}
4782+
server := &Server{
4783+
Stater: &testutil.MockStater{
4784+
BeaconState: st,
4785+
},
4786+
OptimisticModeFetcher: chainService,
4787+
FinalizationFetcher: chainService,
4788+
}
4789+
4790+
t.Run("json response", func(t *testing.T) {
4791+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4792+
req.SetPathValue("state_id", "head")
4793+
rec := httptest.NewRecorder()
4794+
rec.Body = new(bytes.Buffer)
4795+
4796+
server.GetPendingDeposits(rec, req)
4797+
require.Equal(t, http.StatusOK, rec.Code)
4798+
require.Equal(t, "electra", rec.Header().Get(api.VersionHeader))
4799+
4800+
var resp structs.GetPendingDepositsResponse
4801+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
4802+
4803+
expectedVersion := version.String(st.Version())
4804+
require.Equal(t, expectedVersion, resp.Version)
4805+
4806+
require.Equal(t, false, resp.ExecutionOptimistic)
4807+
require.Equal(t, false, resp.Finalized)
4808+
4809+
expectedDeposits := structs.PendingDepositsFromConsensus(deps)
4810+
require.DeepEqual(t, expectedDeposits, resp.Data)
4811+
})
4812+
t.Run("ssz response", func(t *testing.T) {
4813+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4814+
req.Header.Set("Accept", "application/octet-stream")
4815+
req.SetPathValue("state_id", "head")
4816+
rec := httptest.NewRecorder()
4817+
rec.Body = new(bytes.Buffer)
4818+
4819+
server.GetPendingDeposits(rec, req)
4820+
require.Equal(t, http.StatusOK, rec.Code)
4821+
require.Equal(t, "electra", rec.Header().Get(api.VersionHeader))
4822+
4823+
responseBytes := rec.Body.Bytes()
4824+
var recoveredDeposits []*eth.PendingDeposit
4825+
4826+
// Verify total size matches expected number of deposits
4827+
depositSize := (&eth.PendingDeposit{}).SizeSSZ()
4828+
require.Equal(t, len(responseBytes), depositSize*len(deps))
4829+
4830+
for i := 0; i < len(deps); i++ {
4831+
start := i * depositSize
4832+
end := start + depositSize
4833+
4834+
var deposit eth.PendingDeposit
4835+
require.NoError(t, deposit.UnmarshalSSZ(responseBytes[start:end]))
4836+
recoveredDeposits = append(recoveredDeposits, &deposit)
4837+
}
4838+
require.DeepEqual(t, deps, recoveredDeposits)
4839+
})
4840+
t.Run("pre electra state", func(t *testing.T) {
4841+
preElectraSt, _ := util.DeterministicGenesisStateDeneb(t, 1)
4842+
preElectraServer := &Server{
4843+
Stater: &testutil.MockStater{
4844+
BeaconState: preElectraSt,
4845+
},
4846+
OptimisticModeFetcher: chainService,
4847+
FinalizationFetcher: chainService,
4848+
}
4849+
4850+
// Test JSON request
4851+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4852+
req.SetPathValue("state_id", "head")
4853+
rec := httptest.NewRecorder()
4854+
rec.Body = new(bytes.Buffer)
4855+
4856+
preElectraServer.GetPendingDeposits(rec, req)
4857+
require.Equal(t, http.StatusBadRequest, rec.Code)
4858+
4859+
var errResp struct {
4860+
Code int `json:"code"`
4861+
Message string `json:"message"`
4862+
}
4863+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp))
4864+
require.Equal(t, "state_id is prior to electra", errResp.Message)
4865+
4866+
// Test SSZ request
4867+
sszReq := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4868+
sszReq.Header.Set("Accept", "application/octet-stream")
4869+
sszReq.SetPathValue("state_id", "head")
4870+
sszRec := httptest.NewRecorder()
4871+
sszRec.Body = new(bytes.Buffer)
4872+
4873+
preElectraServer.GetPendingDeposits(sszRec, sszReq)
4874+
require.Equal(t, http.StatusBadRequest, sszRec.Code)
4875+
4876+
var sszErrResp struct {
4877+
Code int `json:"code"`
4878+
Message string `json:"message"`
4879+
}
4880+
require.NoError(t, json.Unmarshal(sszRec.Body.Bytes(), &sszErrResp))
4881+
require.Equal(t, "state_id is prior to electra", sszErrResp.Message)
4882+
})
4883+
t.Run("missing state_id parameter", func(t *testing.T) {
4884+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4885+
// Intentionally not setting state_id
4886+
rec := httptest.NewRecorder()
4887+
rec.Body = new(bytes.Buffer)
4888+
4889+
server.GetPendingDeposits(rec, req)
4890+
require.Equal(t, http.StatusBadRequest, rec.Code)
4891+
4892+
var errResp struct {
4893+
Code int `json:"code"`
4894+
Message string `json:"message"`
4895+
}
4896+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp))
4897+
require.Equal(t, "state_id is required in URL params", errResp.Message)
4898+
})
4899+
t.Run("optimistic node", func(t *testing.T) {
4900+
optimisticChainService := &chainMock.ChainService{
4901+
Optimistic: true,
4902+
FinalizedRoots: map[[32]byte]bool{},
4903+
}
4904+
optimisticServer := &Server{
4905+
Stater: server.Stater,
4906+
OptimisticModeFetcher: optimisticChainService,
4907+
FinalizationFetcher: optimisticChainService,
4908+
}
4909+
4910+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4911+
req.SetPathValue("state_id", "head")
4912+
rec := httptest.NewRecorder()
4913+
rec.Body = new(bytes.Buffer)
4914+
4915+
optimisticServer.GetPendingDeposits(rec, req)
4916+
require.Equal(t, http.StatusOK, rec.Code)
4917+
4918+
var resp structs.GetPendingDepositsResponse
4919+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
4920+
require.Equal(t, true, resp.ExecutionOptimistic)
4921+
})
4922+
4923+
t.Run("finalized node", func(t *testing.T) {
4924+
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
4925+
require.NoError(t, err)
4926+
4927+
finalizedChainService := &chainMock.ChainService{
4928+
Optimistic: false,
4929+
FinalizedRoots: map[[32]byte]bool{blockRoot: true},
4930+
}
4931+
finalizedServer := &Server{
4932+
Stater: server.Stater,
4933+
OptimisticModeFetcher: finalizedChainService,
4934+
FinalizationFetcher: finalizedChainService,
4935+
}
4936+
4937+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
4938+
req.SetPathValue("state_id", "head")
4939+
rec := httptest.NewRecorder()
4940+
rec.Body = new(bytes.Buffer)
4941+
4942+
finalizedServer.GetPendingDeposits(rec, req)
4943+
require.Equal(t, http.StatusOK, rec.Code)
4944+
4945+
var resp structs.GetPendingDepositsResponse
4946+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
4947+
require.Equal(t, true, resp.Finalized)
4948+
})
4949+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
- Add endpoint for getting pending deposits.

0 commit comments

Comments
 (0)