Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions sidecar/rpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ type rpcError struct {
Data string `json:"data"`
}

// envelope is the JSON-RPC response wrapper returned by all CometBFT
// HTTP RPC endpoints (e.g. /status, /block, /block_results).
// envelope is the JSON-RPC response wrapper returned by standard CometBFT
// HTTP RPC endpoints. Note: seid's CometBFT fork returns flat JSON without
// this wrapper — Client.Get handles both formats.
type envelope struct {
Result json.RawMessage `json:"result"`
Error *rpcError `json:"error,omitempty"`
JSONRPC string `json:"jsonrpc"`
Result json.RawMessage `json:"result"`
Error *rpcError `json:"error,omitempty"`
}

// Client performs HTTP GET requests against a CometBFT RPC endpoint
Expand Down Expand Up @@ -61,8 +63,15 @@ func (c *Client) SetTimeout(d time.Duration) { c.timeout = d }
// Endpoint returns the configured RPC base URL.
func (c *Client) Endpoint() string { return c.endpoint }

// Get performs an HTTP GET to endpoint+path, unwraps the JSON-RPC
// envelope, and returns the inner "result" as raw JSON.
// Get performs an HTTP GET to endpoint+path and returns the inner result
// as raw JSON. It handles both response formats:
// - JSON-RPC envelope (standard CometBFT): {"jsonrpc":"2.0","result":{...}}
// → returns the unwrapped "result" value
// - Flat JSON (seid): {"node_info":{...},"sync_info":{...}}
// → returns the body as-is
//
// This dual-format support is necessary because seid's CometBFT fork
// returns flat responses while standard CometBFT uses JSON-RPC envelopes.
func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error) {
body, err := c.doGet(ctx, path)
if err != nil {
Expand All @@ -71,17 +80,24 @@ func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error)

var env envelope
if err := json.Unmarshal(body, &env); err != nil {
return nil, fmt.Errorf("decoding JSON-RPC envelope from %s: %w", path, err)
return nil, fmt.Errorf("decoding JSON response from %s: %w", path, err)
}
if env.Error != nil {
return nil, fmt.Errorf("JSON-RPC error from %s: %s (code %d, data: %s)",
path, env.Error.Message, env.Error.Code, env.Error.Data)
}
if len(env.Result) == 0 {
return nil, fmt.Errorf("empty result in JSON-RPC response from %s", path)

// Discriminate by the presence of "jsonrpc":"2.0" — only real JSON-RPC
// envelopes carry this field. Seid's flat responses never will.
if env.JSONRPC == "2.0" {
if env.Error != nil {
return nil, fmt.Errorf("JSON-RPC error from %s: %s (code %d, data: %s)",
path, env.Error.Message, env.Error.Code, env.Error.Data)
}
if len(env.Result) == 0 {
return nil, fmt.Errorf("empty result in JSON-RPC response from %s", path)
}
return env.Result, nil
}

return env.Result, nil
// Flat JSON (seid format) — return the body as-is.
return json.RawMessage(body), nil
}

// GetRaw performs an HTTP GET and returns the entire response body
Expand Down
46 changes: 39 additions & 7 deletions sidecar/rpc/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,51 @@ func TestClient_Get_UnwrapsEnvelope(t *testing.T) {
}
}

func TestClient_Get_EmptyResult(t *testing.T) {
func TestClient_Get_FlatJSON_SeidFormat(t *testing.T) {
// seid returns flat JSON without the JSON-RPC envelope.
flat := `{"node_info":{"id":"abc123"},"sync_info":{"latest_block_height":"42","catching_up":false}}`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":-1}`))
_, _ = w.Write([]byte(flat))
}))
defer srv.Close()

c := NewClient(srv.URL, nil)
_, err := c.Get(context.Background(), "/status")
if err == nil {
t.Fatal("expected error for empty result")
raw, err := c.Get(context.Background(), "/status")
if err != nil {
t.Fatalf("Get: %v", err)
}

got := string(raw)
if !strings.Contains(got, "abc123") {
t.Errorf("expected flat result containing node ID, got %s", got)
}
if !strings.Contains(got, "latest_block_height") {
t.Errorf("expected flat result containing latest_block_height, got %s", got)
}
}

func TestClient_Get_FlatJSON_WithResultKey(t *testing.T) {
// A flat response that happens to contain a "result" data key must NOT
// be mistaken for a JSON-RPC envelope.
flat := `{"result":{"code":0,"log":"ok"},"hash":"ABC123"}`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(flat))
}))
defer srv.Close()

c := NewClient(srv.URL, nil)
raw, err := c.Get(context.Background(), "/tx")
if err != nil {
t.Fatalf("Get: %v", err)
}

got := string(raw)
// Should return the full body, not just the inner "result" value.
if !strings.Contains(got, "hash") {
t.Errorf("expected full flat body with hash field, got %s", got)
}
if !strings.Contains(err.Error(), "empty result") {
t.Errorf("unexpected error: %v", err)
if !strings.Contains(got, "ABC123") {
t.Errorf("expected full flat body with hash value, got %s", got)
}
}

Expand Down
7 changes: 6 additions & 1 deletion sidecar/tasks/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,24 @@ func (s *EC2TagsSource) Discover(ctx context.Context) ([]string, error) {
return nil, fmt.Errorf("ec2 DescribeInstances: %w", err)
}

var instanceCount int
var peers []string
for _, reservation := range output.Reservations {
for _, instance := range reservation.Instances {
instanceCount++
peer, err := buildPeerAddress(ctx, querier, instance)
if err != nil {
peerLog.Info("skipping unreachable EC2 instance",
"ip", instanceIP(instance), "error", err)
continue
}
peers = append(peers, peer)
}
}

if len(peers) == 0 {
return nil, fmt.Errorf("no reachable peers found via ec2Tags in region %s", s.Region)
return nil, fmt.Errorf("no reachable peers found via ec2Tags in region %s (%d instances returned, 0 reachable)",
s.Region, instanceCount)
}

return peers, nil
Expand Down
Loading