Skip to content

Commit 3baaa73

Browse files
saolynjames-prysm
andauthored
Add get pending partial withdrawals (#14949)
* add pending partial withdrawals endpoint * changelog * missing new line * fix changelog * removing unneeded header * using generic instead of redundant functions --------- Co-authored-by: james-prysm <[email protected]>
1 parent 8ceb7e7 commit 3baaa73

File tree

6 files changed

+305
-38
lines changed

6 files changed

+305
-38
lines changed

api/server/structs/endpoints_beacon.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,10 @@ type GetPendingDepositsResponse struct {
257257
Finalized bool `json:"finalized"`
258258
Data []*PendingDeposit `json:"data"`
259259
}
260+
261+
type GetPendingPartialWithdrawalsResponse struct {
262+
Version string `json:"version"`
263+
ExecutionOptimistic bool `json:"execution_optimistic"`
264+
Finalized bool `json:"finalized"`
265+
Data []*PendingPartialWithdrawal `json:"data"`
266+
}

beacon-chain/rpc/endpoints.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,15 @@ func (s *Service) beaconEndpoints(
893893
handler: server.GetPendingDeposits,
894894
methods: []string{http.MethodGet},
895895
},
896+
{
897+
template: "/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals",
898+
name: namespace + ".GetPendingPartialWithdrawals",
899+
middleware: []middleware.Middleware{
900+
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
901+
},
902+
handler: server.GetPendingPartialWithdrawals,
903+
methods: []string{http.MethodGet},
904+
},
896905
}
897906
}
898907

beacon-chain/rpc/endpoints_test.go

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,40 @@ func Test_endpoints(t *testing.T) {
1717
}
1818

1919
beaconRoutes := map[string][]string{
20-
"/eth/v1/beacon/genesis": {http.MethodGet},
21-
"/eth/v1/beacon/states/{state_id}/root": {http.MethodGet},
22-
"/eth/v1/beacon/states/{state_id}/fork": {http.MethodGet},
23-
"/eth/v1/beacon/states/{state_id}/finality_checkpoints": {http.MethodGet},
24-
"/eth/v1/beacon/states/{state_id}/validators": {http.MethodGet, http.MethodPost},
25-
"/eth/v1/beacon/states/{state_id}/validators/{validator_id}": {http.MethodGet},
26-
"/eth/v1/beacon/states/{state_id}/validator_balances": {http.MethodGet, http.MethodPost},
27-
"/eth/v1/beacon/states/{state_id}/committees": {http.MethodGet},
28-
"/eth/v1/beacon/states/{state_id}/sync_committees": {http.MethodGet},
29-
"/eth/v1/beacon/states/{state_id}/randao": {http.MethodGet},
30-
"/eth/v1/beacon/states/{state_id}/pending_deposits": {http.MethodGet},
31-
"/eth/v1/beacon/headers": {http.MethodGet},
32-
"/eth/v1/beacon/headers/{block_id}": {http.MethodGet},
33-
"/eth/v1/beacon/blinded_blocks": {http.MethodPost},
34-
"/eth/v2/beacon/blinded_blocks": {http.MethodPost},
35-
"/eth/v1/beacon/blocks": {http.MethodPost},
36-
"/eth/v2/beacon/blocks": {http.MethodPost},
37-
"/eth/v2/beacon/blocks/{block_id}": {http.MethodGet},
38-
"/eth/v1/beacon/blocks/{block_id}/root": {http.MethodGet},
39-
"/eth/v1/beacon/blocks/{block_id}/attestations": {http.MethodGet},
40-
"/eth/v2/beacon/blocks/{block_id}/attestations": {http.MethodGet},
41-
"/eth/v1/beacon/blob_sidecars/{block_id}": {http.MethodGet},
42-
"/eth/v1/beacon/deposit_snapshot": {http.MethodGet},
43-
"/eth/v1/beacon/blinded_blocks/{block_id}": {http.MethodGet},
44-
"/eth/v1/beacon/pool/attestations": {http.MethodGet, http.MethodPost},
45-
"/eth/v2/beacon/pool/attestations": {http.MethodGet, http.MethodPost},
46-
"/eth/v1/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost},
47-
"/eth/v2/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost},
48-
"/eth/v1/beacon/pool/proposer_slashings": {http.MethodGet, http.MethodPost},
49-
"/eth/v1/beacon/pool/sync_committees": {http.MethodPost},
50-
"/eth/v1/beacon/pool/voluntary_exits": {http.MethodGet, http.MethodPost},
51-
"/eth/v1/beacon/pool/bls_to_execution_changes": {http.MethodGet, http.MethodPost},
52-
"/prysm/v1/beacon/individual_votes": {http.MethodPost},
20+
"/eth/v1/beacon/genesis": {http.MethodGet},
21+
"/eth/v1/beacon/states/{state_id}/root": {http.MethodGet},
22+
"/eth/v1/beacon/states/{state_id}/fork": {http.MethodGet},
23+
"/eth/v1/beacon/states/{state_id}/finality_checkpoints": {http.MethodGet},
24+
"/eth/v1/beacon/states/{state_id}/validators": {http.MethodGet, http.MethodPost},
25+
"/eth/v1/beacon/states/{state_id}/validators/{validator_id}": {http.MethodGet},
26+
"/eth/v1/beacon/states/{state_id}/validator_balances": {http.MethodGet, http.MethodPost},
27+
"/eth/v1/beacon/states/{state_id}/committees": {http.MethodGet},
28+
"/eth/v1/beacon/states/{state_id}/sync_committees": {http.MethodGet},
29+
"/eth/v1/beacon/states/{state_id}/randao": {http.MethodGet},
30+
"/eth/v1/beacon/states/{state_id}/pending_deposits": {http.MethodGet},
31+
"/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals": {http.MethodGet},
32+
"/eth/v1/beacon/headers": {http.MethodGet},
33+
"/eth/v1/beacon/headers/{block_id}": {http.MethodGet},
34+
"/eth/v1/beacon/blinded_blocks": {http.MethodPost},
35+
"/eth/v2/beacon/blinded_blocks": {http.MethodPost},
36+
"/eth/v1/beacon/blocks": {http.MethodPost},
37+
"/eth/v2/beacon/blocks": {http.MethodPost},
38+
"/eth/v2/beacon/blocks/{block_id}": {http.MethodGet},
39+
"/eth/v1/beacon/blocks/{block_id}/root": {http.MethodGet},
40+
"/eth/v1/beacon/blocks/{block_id}/attestations": {http.MethodGet},
41+
"/eth/v2/beacon/blocks/{block_id}/attestations": {http.MethodGet},
42+
"/eth/v1/beacon/blob_sidecars/{block_id}": {http.MethodGet},
43+
"/eth/v1/beacon/deposit_snapshot": {http.MethodGet},
44+
"/eth/v1/beacon/blinded_blocks/{block_id}": {http.MethodGet},
45+
"/eth/v1/beacon/pool/attestations": {http.MethodGet, http.MethodPost},
46+
"/eth/v2/beacon/pool/attestations": {http.MethodGet, http.MethodPost},
47+
"/eth/v1/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost},
48+
"/eth/v2/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost},
49+
"/eth/v1/beacon/pool/proposer_slashings": {http.MethodGet, http.MethodPost},
50+
"/eth/v1/beacon/pool/sync_committees": {http.MethodPost},
51+
"/eth/v1/beacon/pool/voluntary_exits": {http.MethodGet, http.MethodPost},
52+
"/eth/v1/beacon/pool/bls_to_execution_changes": {http.MethodGet, http.MethodPost},
53+
"/prysm/v1/beacon/individual_votes": {http.MethodPost},
5354
}
5455

5556
lightClientRoutes := map[string][]string{

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

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,7 @@ func (s *Server) GetPendingDeposits(w http.ResponseWriter, r *http.Request) {
16371637
}
16381638
w.Header().Set(api.VersionHeader, version.String(st.Version()))
16391639
if httputil.RespondWithSsz(r) {
1640-
sszData, err := serializePendingDeposits(pd)
1640+
sszData, err := serializeItems(pd)
16411641
if err != nil {
16421642
httputil.HandleError(w, "Failed to serialize pending deposits: "+err.Error(), http.StatusInternalServerError)
16431643
return
@@ -1665,11 +1665,68 @@ func (s *Server) GetPendingDeposits(w http.ResponseWriter, r *http.Request) {
16651665
}
16661666
}
16671667

1668-
// serializePendingDeposits serializes a slice of PendingDeposit objects into a single byte array.
1669-
func serializePendingDeposits(pd []*eth.PendingDeposit) ([]byte, error) {
1668+
// GetPendingPartialWithdrawals returns pending partial withdrawals for state with given 'stateId'.
1669+
// Should return 400 if the state retrieved is prior to Electra.
1670+
// Supports both JSON and SSZ responses based on Accept header.
1671+
func (s *Server) GetPendingPartialWithdrawals(w http.ResponseWriter, r *http.Request) {
1672+
ctx, span := trace.StartSpan(r.Context(), "beacon.GetPendingPartialWithdrawals")
1673+
defer span.End()
1674+
1675+
stateId := r.PathValue("state_id")
1676+
if stateId == "" {
1677+
httputil.HandleError(w, "state_id is required in URL params", http.StatusBadRequest)
1678+
return
1679+
}
1680+
st, err := s.Stater.State(ctx, []byte(stateId))
1681+
if err != nil {
1682+
shared.WriteStateFetchError(w, err)
1683+
return
1684+
}
1685+
if st.Version() < version.Electra {
1686+
httputil.HandleError(w, "state_id is prior to electra", http.StatusBadRequest)
1687+
return
1688+
}
1689+
ppw, err := st.PendingPartialWithdrawals()
1690+
if err != nil {
1691+
httputil.HandleError(w, "Could not get pending partial withdrawals: "+err.Error(), http.StatusInternalServerError)
1692+
return
1693+
}
1694+
w.Header().Set(api.VersionHeader, version.String(st.Version()))
1695+
if httputil.RespondWithSsz(r) {
1696+
sszData, err := serializeItems(ppw)
1697+
if err != nil {
1698+
httputil.HandleError(w, "Failed to serialize pending partial withdrawals: "+err.Error(), http.StatusInternalServerError)
1699+
return
1700+
}
1701+
httputil.WriteSsz(w, sszData, "pending_partial_withdrawals.ssz")
1702+
} else {
1703+
isOptimistic, err := helpers.IsOptimistic(ctx, []byte(stateId), s.OptimisticModeFetcher, s.Stater, s.ChainInfoFetcher, s.BeaconDB)
1704+
if err != nil {
1705+
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
1706+
return
1707+
}
1708+
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
1709+
if err != nil {
1710+
httputil.HandleError(w, "Could not calculate root of latest block header: "+err.Error(), http.StatusInternalServerError)
1711+
return
1712+
}
1713+
isFinalized := s.FinalizationFetcher.IsFinalized(ctx, blockRoot)
1714+
resp := structs.GetPendingPartialWithdrawalsResponse{
1715+
Version: version.String(st.Version()),
1716+
ExecutionOptimistic: isOptimistic,
1717+
Finalized: isFinalized,
1718+
Data: structs.PendingPartialWithdrawalsFromConsensus(ppw),
1719+
}
1720+
httputil.WriteJson(w, resp)
1721+
}
1722+
}
1723+
1724+
// SerializeItems serializes a slice of items, each of which implements the MarshalSSZ method,
1725+
// into a single byte array.
1726+
func serializeItems[T interface{ MarshalSSZ() ([]byte, error) }](items []T) ([]byte, error) {
16701727
var result []byte
1671-
for _, d := range pd {
1672-
b, err := d.MarshalSSZ()
1728+
for _, item := range items {
1729+
b, err := item.MarshalSSZ()
16731730
if err != nil {
16741731
return nil, err
16751732
}

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

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4947,3 +4947,193 @@ func TestGetPendingDeposits(t *testing.T) {
49474947
require.Equal(t, true, resp.Finalized)
49484948
})
49494949
}
4950+
4951+
func TestGetPendingPartialWithdrawals(t *testing.T) {
4952+
st, _ := util.DeterministicGenesisStateElectra(t, 10)
4953+
for i := 0; i < 10; i += 1 {
4954+
err := st.AppendPendingPartialWithdrawal(
4955+
&eth.PendingPartialWithdrawal{
4956+
Index: primitives.ValidatorIndex(i),
4957+
Amount: 100,
4958+
WithdrawableEpoch: primitives.Epoch(0),
4959+
})
4960+
require.NoError(t, err)
4961+
}
4962+
withdrawals, err := st.PendingPartialWithdrawals()
4963+
require.NoError(t, err)
4964+
4965+
chainService := &chainMock.ChainService{
4966+
Optimistic: false,
4967+
FinalizedRoots: map[[32]byte]bool{},
4968+
}
4969+
server := &Server{
4970+
Stater: &testutil.MockStater{
4971+
BeaconState: st,
4972+
},
4973+
OptimisticModeFetcher: chainService,
4974+
FinalizationFetcher: chainService,
4975+
}
4976+
4977+
t.Run("json response", func(t *testing.T) {
4978+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
4979+
req.SetPathValue("state_id", "head")
4980+
rec := httptest.NewRecorder()
4981+
rec.Body = new(bytes.Buffer)
4982+
4983+
server.GetPendingPartialWithdrawals(rec, req)
4984+
require.Equal(t, http.StatusOK, rec.Code)
4985+
require.Equal(t, "electra", rec.Header().Get(api.VersionHeader))
4986+
4987+
var resp structs.GetPendingPartialWithdrawalsResponse
4988+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
4989+
4990+
expectedVersion := version.String(st.Version())
4991+
require.Equal(t, expectedVersion, resp.Version)
4992+
4993+
require.Equal(t, false, resp.ExecutionOptimistic)
4994+
require.Equal(t, false, resp.Finalized)
4995+
4996+
expectedWithdrawals := structs.PendingPartialWithdrawalsFromConsensus(withdrawals)
4997+
require.DeepEqual(t, expectedWithdrawals, resp.Data)
4998+
})
4999+
5000+
t.Run("ssz response", func(t *testing.T) {
5001+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
5002+
req.Header.Set("Accept", "application/octet-stream")
5003+
req.SetPathValue("state_id", "head")
5004+
rec := httptest.NewRecorder()
5005+
rec.Body = new(bytes.Buffer)
5006+
5007+
server.GetPendingPartialWithdrawals(rec, req)
5008+
require.Equal(t, http.StatusOK, rec.Code)
5009+
require.Equal(t, "electra", rec.Header().Get(api.VersionHeader))
5010+
5011+
responseBytes := rec.Body.Bytes()
5012+
var recoveredWithdrawals []*eth.PendingPartialWithdrawal
5013+
5014+
withdrawalSize := (&eth.PendingPartialWithdrawal{}).SizeSSZ()
5015+
require.Equal(t, len(responseBytes), withdrawalSize*len(withdrawals))
5016+
5017+
for i := 0; i < len(withdrawals); i++ {
5018+
start := i * withdrawalSize
5019+
end := start + withdrawalSize
5020+
5021+
var withdrawal eth.PendingPartialWithdrawal
5022+
require.NoError(t, withdrawal.UnmarshalSSZ(responseBytes[start:end]))
5023+
recoveredWithdrawals = append(recoveredWithdrawals, &withdrawal)
5024+
}
5025+
require.DeepEqual(t, withdrawals, recoveredWithdrawals)
5026+
})
5027+
5028+
t.Run("pre electra state", func(t *testing.T) {
5029+
preElectraSt, _ := util.DeterministicGenesisStateDeneb(t, 1)
5030+
preElectraServer := &Server{
5031+
Stater: &testutil.MockStater{
5032+
BeaconState: preElectraSt,
5033+
},
5034+
OptimisticModeFetcher: chainService,
5035+
FinalizationFetcher: chainService,
5036+
}
5037+
5038+
// Test JSON request
5039+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
5040+
req.SetPathValue("state_id", "head")
5041+
rec := httptest.NewRecorder()
5042+
rec.Body = new(bytes.Buffer)
5043+
5044+
preElectraServer.GetPendingPartialWithdrawals(rec, req)
5045+
require.Equal(t, http.StatusBadRequest, rec.Code)
5046+
5047+
var errResp struct {
5048+
Code int `json:"code"`
5049+
Message string `json:"message"`
5050+
}
5051+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp))
5052+
require.Equal(t, "state_id is prior to electra", errResp.Message)
5053+
5054+
// Test SSZ request
5055+
sszReq := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
5056+
sszReq.Header.Set("Accept", "application/octet-stream")
5057+
sszReq.SetPathValue("state_id", "head")
5058+
sszRec := httptest.NewRecorder()
5059+
sszRec.Body = new(bytes.Buffer)
5060+
5061+
preElectraServer.GetPendingPartialWithdrawals(sszRec, sszReq)
5062+
require.Equal(t, http.StatusBadRequest, sszRec.Code)
5063+
5064+
var sszErrResp struct {
5065+
Code int `json:"code"`
5066+
Message string `json:"message"`
5067+
}
5068+
require.NoError(t, json.Unmarshal(sszRec.Body.Bytes(), &sszErrResp))
5069+
require.Equal(t, "state_id is prior to electra", sszErrResp.Message)
5070+
})
5071+
5072+
t.Run("missing state_id parameter", func(t *testing.T) {
5073+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
5074+
// Intentionally not setting state_id
5075+
rec := httptest.NewRecorder()
5076+
rec.Body = new(bytes.Buffer)
5077+
5078+
server.GetPendingPartialWithdrawals(rec, req)
5079+
require.Equal(t, http.StatusBadRequest, rec.Code)
5080+
5081+
var errResp struct {
5082+
Code int `json:"code"`
5083+
Message string `json:"message"`
5084+
}
5085+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp))
5086+
require.Equal(t, "state_id is required in URL params", errResp.Message)
5087+
})
5088+
5089+
t.Run("optimistic node", func(t *testing.T) {
5090+
optimisticChainService := &chainMock.ChainService{
5091+
Optimistic: true,
5092+
FinalizedRoots: map[[32]byte]bool{},
5093+
}
5094+
optimisticServer := &Server{
5095+
Stater: server.Stater,
5096+
OptimisticModeFetcher: optimisticChainService,
5097+
FinalizationFetcher: optimisticChainService,
5098+
}
5099+
5100+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
5101+
req.SetPathValue("state_id", "head")
5102+
rec := httptest.NewRecorder()
5103+
rec.Body = new(bytes.Buffer)
5104+
5105+
optimisticServer.GetPendingPartialWithdrawals(rec, req)
5106+
require.Equal(t, http.StatusOK, rec.Code)
5107+
5108+
var resp structs.GetPendingPartialWithdrawalsResponse
5109+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
5110+
require.Equal(t, true, resp.ExecutionOptimistic)
5111+
})
5112+
5113+
t.Run("finalized node", func(t *testing.T) {
5114+
blockRoot, err := st.LatestBlockHeader().HashTreeRoot()
5115+
require.NoError(t, err)
5116+
5117+
finalizedChainService := &chainMock.ChainService{
5118+
Optimistic: false,
5119+
FinalizedRoots: map[[32]byte]bool{blockRoot: true},
5120+
}
5121+
finalizedServer := &Server{
5122+
Stater: server.Stater,
5123+
OptimisticModeFetcher: finalizedChainService,
5124+
FinalizationFetcher: finalizedChainService,
5125+
}
5126+
5127+
req := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals", nil)
5128+
req.SetPathValue("state_id", "head")
5129+
rec := httptest.NewRecorder()
5130+
rec.Body = new(bytes.Buffer)
5131+
5132+
finalizedServer.GetPendingPartialWithdrawals(rec, req)
5133+
require.Equal(t, http.StatusOK, rec.Code)
5134+
5135+
var resp structs.GetPendingPartialWithdrawalsResponse
5136+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
5137+
require.Equal(t, true, resp.Finalized)
5138+
})
5139+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
- Add endpoint for getting pending partial withdrawals.

0 commit comments

Comments
 (0)