Skip to content

Commit 5a897df

Browse files
syjn99rkapka
andauthored
SSZ-QL: Add endpoints (BeaconState/BeaconBlock) (#15888)
* Move ssz_query objects into testing folder (ensuring test objects only used in test environment) * Add containers for response * Export sszInfo * Add QueryBeaconState/Block * Add comments and few refactor * Fix merge conflict issues * Return 500 when calculate offset fails * Add test for QueryBeaconState * Add test for QueryBeaconBlock * Changelog :) * Rename `QuerySSZRequest` to `SSZQueryRequest` * Fix middleware hooks for RPC to accept JSON from client and return SSZ * Convert to `SSZObject` directly from proto * Move marshalling/calculating hash tree root part after `CalculateOffsetAndLength` * Make nogo happy * Add informing comment for using proto unsafe conversion --------- Co-authored-by: Radosław Kapka <[email protected]>
1 parent 9019088 commit 5a897df

26 files changed

+1642
-390
lines changed

api/server/structs/endpoints_beacon.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,8 @@ type GetBlobsResponse struct {
296296
Finalized bool `json:"finalized"`
297297
Data []string `json:"data"` //blobs
298298
}
299+
300+
type SSZQueryRequest struct {
301+
Query string `json:"query"`
302+
IncludeProof bool `json:"include_proof,omitempty"`
303+
}

beacon-chain/rpc/endpoints.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func (s *Service) endpoints(
9797
endpoints = append(endpoints, s.beaconEndpoints(ch, stater, blocker, validatorServer, coreService)...)
9898
endpoints = append(endpoints, s.configEndpoints()...)
9999
endpoints = append(endpoints, s.eventsEndpoints()...)
100-
endpoints = append(endpoints, s.prysmBeaconEndpoints(ch, stater, coreService)...)
100+
endpoints = append(endpoints, s.prysmBeaconEndpoints(ch, stater, blocker, coreService)...)
101101
endpoints = append(endpoints, s.prysmNodeEndpoints()...)
102102
endpoints = append(endpoints, s.prysmValidatorEndpoints(stater, coreService)...)
103103

@@ -1184,6 +1184,7 @@ func (s *Service) eventsEndpoints() []endpoint {
11841184
func (s *Service) prysmBeaconEndpoints(
11851185
ch *stategen.CanonicalHistory,
11861186
stater lookup.Stater,
1187+
blocker lookup.Blocker,
11871188
coreService *core.Service,
11881189
) []endpoint {
11891190
server := &beaconprysm.Server{
@@ -1194,6 +1195,7 @@ func (s *Service) prysmBeaconEndpoints(
11941195
CanonicalHistory: ch,
11951196
BeaconDB: s.cfg.BeaconDB,
11961197
Stater: stater,
1198+
Blocker: blocker,
11971199
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
11981200
FinalizationFetcher: s.cfg.FinalizationFetcher,
11991201
CoreService: coreService,
@@ -1266,6 +1268,28 @@ func (s *Service) prysmBeaconEndpoints(
12661268
handler: server.PublishBlobs,
12671269
methods: []string{http.MethodPost},
12681270
},
1271+
{
1272+
template: "/prysm/v1/beacon/states/{state_id}/query",
1273+
name: namespace + ".QueryBeaconState",
1274+
middleware: []middleware.Middleware{
1275+
middleware.ContentTypeHandler([]string{api.JsonMediaType}),
1276+
middleware.AcceptHeaderHandler([]string{api.OctetStreamMediaType}),
1277+
middleware.AcceptEncodingHeaderHandler(),
1278+
},
1279+
handler: server.QueryBeaconState,
1280+
methods: []string{http.MethodPost},
1281+
},
1282+
{
1283+
template: "/prysm/v1/beacon/blocks/{block_id}/query",
1284+
name: namespace + ".QueryBeaconBlock",
1285+
middleware: []middleware.Middleware{
1286+
middleware.ContentTypeHandler([]string{api.JsonMediaType}),
1287+
middleware.AcceptHeaderHandler([]string{api.OctetStreamMediaType}),
1288+
middleware.AcceptEncodingHeaderHandler(),
1289+
},
1290+
handler: server.QueryBeaconBlock,
1291+
methods: []string{http.MethodPost},
1292+
},
12691293
}
12701294
}
12711295

beacon-chain/rpc/endpoints_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ func Test_endpoints(t *testing.T) {
127127
"/prysm/v1/beacon/states/{state_id}/validator_count": {http.MethodGet},
128128
"/prysm/v1/beacon/chain_head": {http.MethodGet},
129129
"/prysm/v1/beacon/blobs": {http.MethodPost},
130+
"/prysm/v1/beacon/states/{state_id}/query": {http.MethodPost},
131+
"/prysm/v1/beacon/blocks/{block_id}/query": {http.MethodPost},
130132
}
131133

132134
prysmNodeRoutes := map[string][]string{

beacon-chain/rpc/prysm/beacon/BUILD.bazel

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ go_library(
55
srcs = [
66
"handlers.go",
77
"server.go",
8+
"ssz_query.go",
89
"validator_count.go",
910
],
1011
importpath = "github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/prysm/beacon",
1112
visibility = ["//visibility:public"],
1213
deps = [
14+
"//api:go_default_library",
1315
"//api/server/structs:go_default_library",
1416
"//beacon-chain/blockchain:go_default_library",
1517
"//beacon-chain/core/helpers:go_default_library",
@@ -27,10 +29,13 @@ go_library(
2729
"//consensus-types/primitives:go_default_library",
2830
"//consensus-types/validator:go_default_library",
2931
"//encoding/bytesutil:go_default_library",
32+
"//encoding/ssz/query:go_default_library",
3033
"//monitoring/tracing/trace:go_default_library",
3134
"//network/httputil:go_default_library",
3235
"//proto/eth/v1:go_default_library",
3336
"//proto/prysm/v1alpha1:go_default_library",
37+
"//proto/ssz_query:go_default_library",
38+
"//runtime/version:go_default_library",
3439
"//time/slots:go_default_library",
3540
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
3641
"@com_github_pkg_errors//:go_default_library",
@@ -41,10 +46,12 @@ go_test(
4146
name = "go_default_test",
4247
srcs = [
4348
"handlers_test.go",
49+
"ssz_query_test.go",
4450
"validator_count_test.go",
4551
],
4652
embed = [":go_default_library"],
4753
deps = [
54+
"//api:go_default_library",
4855
"//api/server/structs:go_default_library",
4956
"//beacon-chain/blockchain/testing:go_default_library",
5057
"//beacon-chain/core/helpers:go_default_library",
@@ -63,10 +70,13 @@ go_test(
6370
"//config/fieldparams:go_default_library",
6471
"//config/params:go_default_library",
6572
"//consensus-types/blocks:go_default_library",
73+
"//consensus-types/interfaces:go_default_library",
6674
"//consensus-types/primitives:go_default_library",
6775
"//encoding/bytesutil:go_default_library",
6876
"//network/httputil:go_default_library",
6977
"//proto/prysm/v1alpha1:go_default_library",
78+
"//proto/ssz_query:go_default_library",
79+
"//runtime/version:go_default_library",
7080
"//testing/assert:go_default_library",
7181
"//testing/require:go_default_library",
7282
"//testing/util:go_default_library",

beacon-chain/rpc/prysm/beacon/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Server struct {
1818
CanonicalHistory *stategen.CanonicalHistory
1919
BeaconDB beacondb.ReadOnlyDatabase
2020
Stater lookup.Stater
21+
Blocker lookup.Blocker
2122
ChainInfoFetcher blockchain.ChainInfoFetcher
2223
FinalizationFetcher blockchain.FinalizationFetcher
2324
CoreService *core.Service
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package beacon
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"io"
7+
"net/http"
8+
9+
"github.com/OffchainLabs/prysm/v6/api"
10+
"github.com/OffchainLabs/prysm/v6/api/server/structs"
11+
"github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/eth/shared"
12+
"github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/lookup"
13+
"github.com/OffchainLabs/prysm/v6/encoding/ssz/query"
14+
"github.com/OffchainLabs/prysm/v6/monitoring/tracing/trace"
15+
"github.com/OffchainLabs/prysm/v6/network/httputil"
16+
sszquerypb "github.com/OffchainLabs/prysm/v6/proto/ssz_query"
17+
"github.com/OffchainLabs/prysm/v6/runtime/version"
18+
)
19+
20+
// QueryBeaconState handles SSZ Query request for BeaconState.
21+
// Returns as bytes serialized SSZQueryResponse.
22+
func (s *Server) QueryBeaconState(w http.ResponseWriter, r *http.Request) {
23+
ctx, span := trace.StartSpan(r.Context(), "beacon.QueryBeaconState")
24+
defer span.End()
25+
26+
stateID := r.PathValue("state_id")
27+
if stateID == "" {
28+
httputil.HandleError(w, "state_id is required in URL params", http.StatusBadRequest)
29+
return
30+
}
31+
32+
// Validate path before lookup: it might be expensive.
33+
var req structs.SSZQueryRequest
34+
err := json.NewDecoder(r.Body).Decode(&req)
35+
switch {
36+
case errors.Is(err, io.EOF):
37+
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
38+
return
39+
case err != nil:
40+
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
41+
return
42+
}
43+
44+
if len(req.Query) == 0 {
45+
httputil.HandleError(w, "Empty query submitted", http.StatusBadRequest)
46+
return
47+
}
48+
49+
path, err := query.ParsePath(req.Query)
50+
if err != nil {
51+
httputil.HandleError(w, "Could not parse path '"+req.Query+"': "+err.Error(), http.StatusBadRequest)
52+
return
53+
}
54+
55+
stateRoot, err := s.Stater.StateRoot(ctx, []byte(stateID))
56+
if err != nil {
57+
var rootNotFoundErr *lookup.StateRootNotFoundError
58+
if errors.As(err, &rootNotFoundErr) {
59+
httputil.HandleError(w, "State root not found: "+rootNotFoundErr.Error(), http.StatusNotFound)
60+
return
61+
}
62+
httputil.HandleError(w, "Could not get state root: "+err.Error(), http.StatusInternalServerError)
63+
return
64+
}
65+
66+
st, err := s.Stater.State(ctx, []byte(stateID))
67+
if err != nil {
68+
shared.WriteStateFetchError(w, err)
69+
return
70+
}
71+
72+
// NOTE: Using unsafe conversion to proto is acceptable here,
73+
// as we play with a copy of the state returned by Stater.
74+
sszObject, ok := st.ToProtoUnsafe().(query.SSZObject)
75+
if !ok {
76+
httputil.HandleError(w, "Unsupported state version for querying: "+version.String(st.Version()), http.StatusBadRequest)
77+
return
78+
}
79+
80+
info, err := query.AnalyzeObject(sszObject)
81+
if err != nil {
82+
httputil.HandleError(w, "Could not analyze state object: "+err.Error(), http.StatusInternalServerError)
83+
return
84+
}
85+
86+
_, offset, length, err := query.CalculateOffsetAndLength(info, path)
87+
if err != nil {
88+
httputil.HandleError(w, "Could not calculate offset and length for path '"+req.Query+"': "+err.Error(), http.StatusInternalServerError)
89+
return
90+
}
91+
92+
encodedState, err := st.MarshalSSZ()
93+
if err != nil {
94+
httputil.HandleError(w, "Could not marshal state to SSZ: "+err.Error(), http.StatusInternalServerError)
95+
return
96+
}
97+
98+
response := &sszquerypb.SSZQueryResponse{
99+
Root: stateRoot,
100+
Result: encodedState[offset : offset+length],
101+
}
102+
103+
responseSsz, err := response.MarshalSSZ()
104+
if err != nil {
105+
httputil.HandleError(w, "Could not marshal response to SSZ: "+err.Error(), http.StatusInternalServerError)
106+
return
107+
}
108+
109+
w.Header().Set(api.VersionHeader, version.String(st.Version()))
110+
httputil.WriteSsz(w, responseSsz)
111+
}
112+
113+
// QueryBeaconState handles SSZ Query request for BeaconState.
114+
// Returns as bytes serialized SSZQueryResponse.
115+
func (s *Server) QueryBeaconBlock(w http.ResponseWriter, r *http.Request) {
116+
ctx, span := trace.StartSpan(r.Context(), "beacon.QueryBeaconBlock")
117+
defer span.End()
118+
119+
blockId := r.PathValue("block_id")
120+
if blockId == "" {
121+
httputil.HandleError(w, "block_id is required in URL params", http.StatusBadRequest)
122+
return
123+
}
124+
125+
// Validate path before lookup: it might be expensive.
126+
var req structs.SSZQueryRequest
127+
err := json.NewDecoder(r.Body).Decode(&req)
128+
switch {
129+
case errors.Is(err, io.EOF):
130+
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
131+
return
132+
case err != nil:
133+
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
134+
return
135+
}
136+
137+
if len(req.Query) == 0 {
138+
httputil.HandleError(w, "Empty query submitted", http.StatusBadRequest)
139+
return
140+
}
141+
142+
path, err := query.ParsePath(req.Query)
143+
if err != nil {
144+
httputil.HandleError(w, "Could not parse path '"+req.Query+"': "+err.Error(), http.StatusBadRequest)
145+
return
146+
}
147+
148+
signedBlock, err := s.Blocker.Block(ctx, []byte(blockId))
149+
if !shared.WriteBlockFetchError(w, signedBlock, err) {
150+
return
151+
}
152+
153+
protoBlock, err := signedBlock.Block().Proto()
154+
if err != nil {
155+
httputil.HandleError(w, "Could not convert block to proto: "+err.Error(), http.StatusInternalServerError)
156+
return
157+
}
158+
159+
block, ok := protoBlock.(query.SSZObject)
160+
if !ok {
161+
httputil.HandleError(w, "Unsupported block version for querying: "+version.String(signedBlock.Version()), http.StatusBadRequest)
162+
return
163+
}
164+
165+
info, err := query.AnalyzeObject(block)
166+
if err != nil {
167+
httputil.HandleError(w, "Could not analyze block object: "+err.Error(), http.StatusInternalServerError)
168+
return
169+
}
170+
171+
_, offset, length, err := query.CalculateOffsetAndLength(info, path)
172+
if err != nil {
173+
httputil.HandleError(w, "Could not calculate offset and length for path '"+req.Query+"': "+err.Error(), http.StatusInternalServerError)
174+
return
175+
}
176+
177+
encodedBlock, err := signedBlock.Block().MarshalSSZ()
178+
if err != nil {
179+
httputil.HandleError(w, "Could not marshal block to SSZ: "+err.Error(), http.StatusInternalServerError)
180+
return
181+
}
182+
183+
blockRoot, err := block.HashTreeRoot()
184+
if err != nil {
185+
httputil.HandleError(w, "Could not compute block root: "+err.Error(), http.StatusInternalServerError)
186+
return
187+
}
188+
189+
response := &sszquerypb.SSZQueryResponse{
190+
Root: blockRoot[:],
191+
Result: encodedBlock[offset : offset+length],
192+
}
193+
194+
responseSsz, err := response.MarshalSSZ()
195+
if err != nil {
196+
httputil.HandleError(w, "Could not marshal response to SSZ: "+err.Error(), http.StatusInternalServerError)
197+
return
198+
}
199+
200+
w.Header().Set(api.VersionHeader, version.String(signedBlock.Version()))
201+
httputil.WriteSsz(w, responseSsz)
202+
}

0 commit comments

Comments
 (0)