Skip to content

Commit

Permalink
Add get pending deposits endpoint (#14941)
Browse files Browse the repository at this point in the history
* Add GetPendingDeposits endpoint

* add comment

* add changelog

* gaz

* Radek' review

* move JSON object params

* gaz

* Radek' nits xD

* James' review
  • Loading branch information
saolyn authored Feb 18, 2025
1 parent 961d8e1 commit 55efccb
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 0 deletions.
7 changes: 7 additions & 0 deletions api/server/structs/endpoints_beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,10 @@ type ChainHead struct {
PreviousJustifiedBlockRoot string `json:"previous_justified_block_root"`
OptimisticStatus bool `json:"optimistic_status"`
}

type GetPendingDepositsResponse struct {
Version string `json:"version"`
ExecutionOptimistic bool `json:"execution_optimistic"`
Finalized bool `json:"finalized"`
Data []*PendingDeposit `json:"data"`
}
9 changes: 9 additions & 0 deletions beacon-chain/rpc/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,15 @@ func (s *Service) beaconEndpoints(
handler: server.GetDepositSnapshot,
methods: []string{http.MethodGet},
},
{
template: "/eth/v1/beacon/states/{state_id}/pending_deposits",
name: namespace + ".GetPendingDeposits",
middleware: []middleware.Middleware{
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
},
handler: server.GetPendingDeposits,
methods: []string{http.MethodGet},
},
}
}

Expand Down
1 change: 1 addition & 0 deletions beacon-chain/rpc/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func Test_endpoints(t *testing.T) {
"/eth/v1/beacon/states/{state_id}/committees": {http.MethodGet},
"/eth/v1/beacon/states/{state_id}/sync_committees": {http.MethodGet},
"/eth/v1/beacon/states/{state_id}/randao": {http.MethodGet},
"/eth/v1/beacon/states/{state_id}/pending_deposits": {http.MethodGet},
"/eth/v1/beacon/headers": {http.MethodGet},
"/eth/v1/beacon/headers/{block_id}": {http.MethodGet},
"/eth/v1/beacon/blinded_blocks": {http.MethodPost},
Expand Down
69 changes: 69 additions & 0 deletions beacon-chain/rpc/eth/beacon/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1608,3 +1608,72 @@ func (s *Server) broadcastSeenBlockSidecars(
}
return nil
}

// GetPendingDeposits returns pending deposits for state with given 'stateId'.
// Should return 400 if the state retrieved is prior to Electra.
// Supports both JSON and SSZ responses based on Accept header.
func (s *Server) GetPendingDeposits(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "beacon.GetPendingDeposits")
defer span.End()

stateId := r.PathValue("state_id")
if stateId == "" {
httputil.HandleError(w, "state_id is required in URL params", http.StatusBadRequest)
return
}
st, err := s.Stater.State(ctx, []byte(stateId))
if err != nil {
shared.WriteStateFetchError(w, err)
return
}
if st.Version() < version.Electra {
httputil.HandleError(w, "state_id is prior to electra", http.StatusBadRequest)
return
}
pd, err := st.PendingDeposits()
if err != nil {
httputil.HandleError(w, "Could not get pending deposits: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set(api.VersionHeader, version.String(st.Version()))
if httputil.RespondWithSsz(r) {
sszData, err := serializePendingDeposits(pd)
if err != nil {
httputil.HandleError(w, "Failed to serialize pending deposits: "+err.Error(), http.StatusInternalServerError)
return
}
httputil.WriteSsz(w, sszData, "pending_deposits.ssz")
} else {
isOptimistic, err := helpers.IsOptimistic(ctx, []byte(stateId), s.OptimisticModeFetcher, s.Stater, s.ChainInfoFetcher, s.BeaconDB)
if err != nil {
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
return
}
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
if err != nil {
httputil.HandleError(w, "Could not calculate root of latest block header: "+err.Error(), http.StatusInternalServerError)
return
}
isFinalized := s.FinalizationFetcher.IsFinalized(ctx, blockRoot)
resp := structs.GetPendingDepositsResponse{
Version: version.String(st.Version()),
ExecutionOptimistic: isOptimistic,
Finalized: isFinalized,
Data: structs.PendingDepositsFromConsensus(pd),
}
httputil.WriteJson(w, resp)
}
}

// serializePendingDeposits serializes a slice of PendingDeposit objects into a single byte array.
func serializePendingDeposits(pd []*eth.PendingDeposit) ([]byte, error) {
var result []byte
for _, d := range pd {
b, err := d.MarshalSSZ()
if err != nil {
return nil, err
}
result = append(result, b...)
}
return result, nil
}
193 changes: 193 additions & 0 deletions beacon-chain/rpc/eth/beacon/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4754,3 +4754,196 @@ func Test_validateBlobSidecars(t *testing.T) {
require.NoError(t, err)
require.ErrorContains(t, "could not verify blob proof: can't verify opening proof", s.validateBlobSidecars(b, [][]byte{blob[:]}, [][]byte{proof[:]}))
}

func TestGetPendingDeposits(t *testing.T) {
st, _ := util.DeterministicGenesisStateElectra(t, 10)

validators := st.Validators()
dummySig := make([]byte, 96)
for j := 0; j < 96; j++ {
dummySig[j] = byte(j)
}
deps := make([]*eth.PendingDeposit, 10)
for i := 0; i < len(deps); i += 1 {
deps[i] = &eth.PendingDeposit{
PublicKey: validators[i].PublicKey,
WithdrawalCredentials: validators[i].WithdrawalCredentials,
Amount: 100,
Slot: 0,
Signature: dummySig,
}
}
require.NoError(t, st.SetPendingDeposits(deps))

chainService := &chainMock.ChainService{
Optimistic: false,
FinalizedRoots: map[[32]byte]bool{},
}
server := &Server{
Stater: &testutil.MockStater{
BeaconState: st,
},
OptimisticModeFetcher: chainService,
FinalizationFetcher: chainService,
}

t.Run("json response", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
req.SetPathValue("state_id", "head")
rec := httptest.NewRecorder()
rec.Body = new(bytes.Buffer)

server.GetPendingDeposits(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, "electra", rec.Header().Get(api.VersionHeader))

var resp structs.GetPendingDepositsResponse
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))

expectedVersion := version.String(st.Version())
require.Equal(t, expectedVersion, resp.Version)

require.Equal(t, false, resp.ExecutionOptimistic)
require.Equal(t, false, resp.Finalized)

expectedDeposits := structs.PendingDepositsFromConsensus(deps)
require.DeepEqual(t, expectedDeposits, resp.Data)
})
t.Run("ssz response", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
req.Header.Set("Accept", "application/octet-stream")
req.SetPathValue("state_id", "head")
rec := httptest.NewRecorder()
rec.Body = new(bytes.Buffer)

server.GetPendingDeposits(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, "electra", rec.Header().Get(api.VersionHeader))

responseBytes := rec.Body.Bytes()
var recoveredDeposits []*eth.PendingDeposit

// Verify total size matches expected number of deposits
depositSize := (&eth.PendingDeposit{}).SizeSSZ()
require.Equal(t, len(responseBytes), depositSize*len(deps))

for i := 0; i < len(deps); i++ {
start := i * depositSize
end := start + depositSize

var deposit eth.PendingDeposit
require.NoError(t, deposit.UnmarshalSSZ(responseBytes[start:end]))
recoveredDeposits = append(recoveredDeposits, &deposit)
}
require.DeepEqual(t, deps, recoveredDeposits)
})
t.Run("pre electra state", func(t *testing.T) {
preElectraSt, _ := util.DeterministicGenesisStateDeneb(t, 1)
preElectraServer := &Server{
Stater: &testutil.MockStater{
BeaconState: preElectraSt,
},
OptimisticModeFetcher: chainService,
FinalizationFetcher: chainService,
}

// Test JSON request
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
req.SetPathValue("state_id", "head")
rec := httptest.NewRecorder()
rec.Body = new(bytes.Buffer)

preElectraServer.GetPendingDeposits(rec, req)
require.Equal(t, http.StatusBadRequest, rec.Code)

var errResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp))
require.Equal(t, "state_id is prior to electra", errResp.Message)

// Test SSZ request
sszReq := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
sszReq.Header.Set("Accept", "application/octet-stream")
sszReq.SetPathValue("state_id", "head")
sszRec := httptest.NewRecorder()
sszRec.Body = new(bytes.Buffer)

preElectraServer.GetPendingDeposits(sszRec, sszReq)
require.Equal(t, http.StatusBadRequest, sszRec.Code)

var sszErrResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
require.NoError(t, json.Unmarshal(sszRec.Body.Bytes(), &sszErrResp))
require.Equal(t, "state_id is prior to electra", sszErrResp.Message)
})
t.Run("missing state_id parameter", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
// Intentionally not setting state_id
rec := httptest.NewRecorder()
rec.Body = new(bytes.Buffer)

server.GetPendingDeposits(rec, req)
require.Equal(t, http.StatusBadRequest, rec.Code)

var errResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp))
require.Equal(t, "state_id is required in URL params", errResp.Message)
})
t.Run("optimistic node", func(t *testing.T) {
optimisticChainService := &chainMock.ChainService{
Optimistic: true,
FinalizedRoots: map[[32]byte]bool{},
}
optimisticServer := &Server{
Stater: server.Stater,
OptimisticModeFetcher: optimisticChainService,
FinalizationFetcher: optimisticChainService,
}

req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
req.SetPathValue("state_id", "head")
rec := httptest.NewRecorder()
rec.Body = new(bytes.Buffer)

optimisticServer.GetPendingDeposits(rec, req)
require.Equal(t, http.StatusOK, rec.Code)

var resp structs.GetPendingDepositsResponse
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, true, resp.ExecutionOptimistic)
})

t.Run("finalized node", func(t *testing.T) {
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
require.NoError(t, err)

finalizedChainService := &chainMock.ChainService{
Optimistic: false,
FinalizedRoots: map[[32]byte]bool{blockRoot: true},
}
finalizedServer := &Server{
Stater: server.Stater,
OptimisticModeFetcher: finalizedChainService,
FinalizationFetcher: finalizedChainService,
}

req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_deposits", nil)
req.SetPathValue("state_id", "head")
rec := httptest.NewRecorder()
rec.Body = new(bytes.Buffer)

finalizedServer.GetPendingDeposits(rec, req)
require.Equal(t, http.StatusOK, rec.Code)

var resp structs.GetPendingDepositsResponse
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, true, resp.Finalized)
})
}
3 changes: 3 additions & 0 deletions changelog/saolyn_add-GetPendingDeposits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Add endpoint for getting pending deposits.

0 comments on commit 55efccb

Please sign in to comment.