HTTP proxy server for Eth2 APIs (#8904)

* Implement API HTTP proxy server

* cleanup + more comments

* gateway will no longer be dependent on beaconv1

* handle error during ErrorJson type assertion

* simplify handling of endpoint data

* fix mux v1 route

* use URL encoding for all requests

* comment fieldProcessor

* fix failing test

* change proxy port to not interfere with e2e

* gzl

* simplify conditional expression

* Move appending custom error header to grpcutils package

* add api-middleware-port flag

* fix documentation for processField

* modify e2e port

* change field processing error message

* better error message for field processing

* simplify base64ToHexProcessor

* fix json structs
This commit is contained in:
Radosław Kapka
2021-05-21 18:59:42 +02:00
committed by GitHub
parent 7dc3d7fc60
commit 2d0f8e012e
24 changed files with 1052 additions and 23 deletions

View File

@@ -642,10 +642,12 @@ func (b *BeaconNode) registerGRPCGateway() error {
return nil
}
gatewayPort := b.cliCtx.Int(flags.GRPCGatewayPort.Name)
apiMiddlewarePort := b.cliCtx.Int(flags.ApiMiddlewarePort.Name)
gatewayHost := b.cliCtx.String(flags.GRPCGatewayHost.Name)
rpcHost := b.cliCtx.String(flags.RPCHost.Name)
selfAddress := fmt.Sprintf("%s:%d", rpcHost, b.cliCtx.Int(flags.RPCPort.Name))
gatewayAddress := fmt.Sprintf("%s:%d", gatewayHost, gatewayPort)
apiMiddlewareAddress := fmt.Sprintf("%s:%d", gatewayHost, apiMiddlewarePort)
allowedOrigins := strings.Split(b.cliCtx.String(flags.GPRCGatewayCorsDomain.Name), ",")
enableDebugRPCEndpoints := b.cliCtx.Bool(flags.EnableDebugRPCEndpoints.Name)
selfCert := b.cliCtx.String(flags.CertFlag.Name)
@@ -655,6 +657,7 @@ func (b *BeaconNode) registerGRPCGateway() error {
selfAddress,
selfCert,
gatewayAddress,
apiMiddlewareAddress,
nil, /*optional mux*/
allowedOrigins,
enableDebugRPCEndpoints,

View File

@@ -32,6 +32,7 @@ go_library(
"//proto/migration:go_default_library",
"//shared/bytesutil:go_default_library",
"//shared/featureconfig:go_default_library",
"//shared/grpcutils:go_default_library",
"//shared/params:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_pkg_errors//:go_default_library",
@@ -80,10 +81,12 @@ go_test(
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library",
"@com_github_prysmaticlabs_eth2_types//:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
],
)

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
types "github.com/prysmaticlabs/eth2-types"
@@ -294,7 +295,7 @@ func (bs *Server) ListBlockAttestations(ctx context.Context, req *ethpb.BlockReq
func (bs *Server) blockFromBlockID(ctx context.Context, blockId []byte) (*ethpb_alpha.SignedBeaconBlock, error) {
var err error
var blk *ethpb_alpha.SignedBeaconBlock
switch string(blockId) {
switch strings.ToLower(string(blockId)) {
case "head":
blk, err = bs.ChainInfoFetcher.HeadBlock(ctx)
if err != nil {

View File

@@ -8,12 +8,24 @@ import (
"github.com/prysmaticlabs/prysm/beacon-chain/core/blocks"
"github.com/prysmaticlabs/prysm/proto/migration"
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/shared/grpcutils"
"go.opencensus.io/trace"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// attestationsVerificationFailure represents failures when verifying submitted attestations.
type attestationsVerificationFailure struct {
Failures []*singleAttestationVerificationFailure `json:"failures"`
}
// singleAttestationVerificationFailure represents an issue when verifying a single submitted attestation.
type singleAttestationVerificationFailure struct {
Index int `json:"index"`
Message string `json:"message"`
}
// ListPoolAttestations retrieves attestations known by the node but
// not necessarily incorporated into any block. Allows filtering by committee index or slot.
func (bs *Server) ListPoolAttestations(ctx context.Context, req *ethpb.AttestationsPoolRequest) (*ethpb.AttestationsPoolResponse, error) {
@@ -62,16 +74,26 @@ func (bs *Server) SubmitAttestations(ctx context.Context, req *ethpb.SubmitAttes
}
var validAttestations []*ethpb_alpha.Attestation
for _, sourceAtt := range req.Data {
var attFailures []*singleAttestationVerificationFailure
for i, sourceAtt := range req.Data {
att := migration.V1AttToV1Alpha1(sourceAtt)
err = blocks.VerifyAttestationNoVerifySignature(ctx, headState, att)
if err != nil {
attFailures = append(attFailures, &singleAttestationVerificationFailure{
Index: i,
Message: err.Error(),
})
continue
}
err = blocks.VerifyAttestationSignature(ctx, headState, att)
if err == nil {
validAttestations = append(validAttestations, att)
if err != nil {
attFailures = append(attFailures, &singleAttestationVerificationFailure{
Index: i,
Message: err.Error(),
})
continue
}
validAttestations = append(validAttestations, att)
}
err = bs.AttestationsPool.SaveAggregatedAttestations(validAttestations)
@@ -89,6 +111,16 @@ func (bs *Server) SubmitAttestations(ctx context.Context, req *ethpb.SubmitAttes
codes.Internal,
"Could not publish one or more attestations. Some attestations could be published successfully.")
}
if len(attFailures) > 0 {
failuresContainer := &attestationsVerificationFailure{Failures: attFailures}
err = grpcutils.AppendCustomErrorHeader(ctx, failuresContainer)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not prepare attestation failure information: %v", err)
}
return nil, status.Errorf(codes.Internal, "One or more attestations failed validation")
}
return &emptypb.Empty{}, nil
}

View File

@@ -5,6 +5,7 @@ import (
"reflect"
"testing"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
eth2types "github.com/prysmaticlabs/eth2-types"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1"
eth "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
@@ -23,6 +24,7 @@ import (
"github.com/prysmaticlabs/prysm/shared/testutil"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -801,7 +803,8 @@ func TestServer_SubmitAttestations_Ok(t *testing.T) {
}
func TestServer_SubmitAttestations_ValidAttestationSubmitted(t *testing.T) {
ctx := context.Background()
ctx := grpc.NewContextWithServerTransportStream(context.Background(), &runtime.ServerTransportStream{})
params.SetupTestConfigCleanup(t)
c := params.BeaconConfig()
// Required for correct committee size calculation.
@@ -905,7 +908,7 @@ func TestServer_SubmitAttestations_ValidAttestationSubmitted(t *testing.T) {
_, err = s.SubmitAttestations(ctx, &ethpb.SubmitAttestationsRequest{
Data: []*ethpb.Attestation{attValid, attInvalidTarget, attInvalidSignature},
})
require.NoError(t, err)
require.ErrorContains(t, "One or more attestations failed validation", err)
savedAtts := s.AttestationsPool.AggregatedAttestations()
require.Equal(t, 1, len(savedAtts))
expectedAtt, err := attValid.HashTreeRoot()

View File

@@ -18,6 +18,7 @@ import (
var (
beaconRPC = flag.String("beacon-rpc", "localhost:4000", "Beacon chain gRPC endpoint")
port = flag.Int("port", 8000, "Port to serve on")
apiMiddlewarePort = flag.Int("port", 8001, "Port to serve API middleware on")
host = flag.String("host", "127.0.0.1", "Host to serve on")
debug = flag.Bool("debug", false, "Enable debug logging")
allowedOrigins = flag.String("corsdomain", "localhost:4242", "A comma separated list of CORS domains to allow")
@@ -41,6 +42,7 @@ func main() {
*beaconRPC,
"", // remoteCert
fmt.Sprintf("%s:%d", *host, *port),
fmt.Sprintf("%s:%d", *host, *apiMiddlewarePort),
mux,
strings.Split(*allowedOrigins, ","),
*enableDebugRPCEndpoints,

View File

@@ -64,12 +64,18 @@ var (
Usage: "The host on which the gateway server runs on",
Value: "127.0.0.1",
}
// GRPCGatewayPort enables a gRPC gateway to be exposed for Prysm.
// GRPCGatewayPort specifies a gRPC gateway port for Prysm.
GRPCGatewayPort = &cli.IntFlag{
Name: "grpc-gateway-port",
Usage: "Enable gRPC gateway for JSON requests",
Usage: "The port on which the gateway server runs on",
Value: 3500,
}
// ApiMiddlewarePort specifies the port for an HTTP proxy server which acts as a middleware between Eth2 API clients and Prysm's gRPC gateway.
ApiMiddlewarePort = &cli.IntFlag{
Name: "api-middleware-port",
Usage: "The port on which the API middleware runs on",
Value: 3501,
}
// GPRCGatewayCorsDomain serves preflight requests when serving gRPC JSON gateway.
GPRCGatewayCorsDomain = &cli.StringFlag{
Name: "grpc-gateway-corsdomain",

View File

@@ -39,6 +39,7 @@ var appFlags = []cli.Flag{
flags.DisableGRPCGateway,
flags.GRPCGatewayHost,
flags.GRPCGatewayPort,
flags.ApiMiddlewarePort,
flags.GPRCGatewayCorsDomain,
flags.MinSyncPeers,
flags.ContractDeploymentBlock,

View File

@@ -102,6 +102,7 @@ var appHelpFlagGroups = []flagGroup{
flags.DisableGRPCGateway,
flags.GRPCGatewayHost,
flags.GRPCGatewayPort,
flags.ApiMiddlewarePort,
flags.GPRCGatewayCorsDomain,
flags.HTTPWeb3ProviderFlag,
flags.FallbackWeb3ProviderFlag,

View File

@@ -110,6 +110,7 @@ func (node *BeaconNode) Start(ctx context.Context) error {
fmt.Sprintf("--p2p-tcp-port=%d", e2e.TestParams.BeaconNodeRPCPort+index+20),
fmt.Sprintf("--monitoring-port=%d", e2e.TestParams.BeaconNodeMetricsPort+index),
fmt.Sprintf("--grpc-gateway-port=%d", e2e.TestParams.BeaconNodeRPCPort+index+40),
fmt.Sprintf("--api-middleware-port=%d", e2e.TestParams.BeaconNodeRPCPort+index+30),
fmt.Sprintf("--contract-deployment-block=%d", 0),
fmt.Sprintf("--rpc-max-page-size=%d", params.BeaconConfig().MinGenesisActiveValidatorCount),
fmt.Sprintf("--bootstrap-node=%s", enr),

2
go.mod
View File

@@ -39,6 +39,7 @@ require (
github.com/google/gofuzz v1.2.0
github.com/google/gopacket v1.1.19 // indirect
github.com/google/uuid v1.2.0
github.com/gorilla/mux v1.7.3
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1
@@ -100,6 +101,7 @@ require (
github.com/trailofbits/go-mutexasserts v0.0.0-20200708152505-19999e7d3cef
github.com/tyler-smith/go-bip39 v1.1.0
github.com/urfave/cli/v2 v2.3.0
github.com/wealdtech/go-bytesutil v1.1.1
github.com/wealdtech/go-eth2-util v1.6.3
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.3
github.com/wercker/journalhook v0.0.0-20180428041537-5d0a5ae867b3

1
go.sum
View File

@@ -432,6 +432,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

View File

@@ -6,10 +6,7 @@ go_library(
srcs = ["bytes.go"],
importpath = "github.com/prysmaticlabs/prysm/shared/bytesutil",
visibility = ["//visibility:public"],
deps = [
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_prysmaticlabs_eth2_types//:go_default_library",
],
deps = ["@com_github_prysmaticlabs_eth2_types//:go_default_library"],
)
go_test(
@@ -20,6 +17,5 @@ go_test(
":go_default_library",
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
],
)

View File

@@ -7,7 +7,6 @@ import (
"math/bits"
"regexp"
"github.com/ethereum/go-ethereum/common/hexutil"
types "github.com/prysmaticlabs/eth2-types"
)
@@ -334,10 +333,10 @@ func BytesToSlotBigEndian(b []byte) types.Slot {
return types.Slot(BytesToUint64BigEndian(b))
}
// IsBytes32Hex checks whether the byte array is a 32-byte long hex number.
// IsBytes32Hex checks whether the byte array is a 32-byte long hex number, optionally prefixed with '0x'.
func IsBytes32Hex(b []byte) (bool, error) {
if b == nil {
return false, nil
}
return regexp.Match("^0x[0-9a-fA-F]{64}$", []byte(hexutil.Encode(b)))
return regexp.Match("^(0x)?[0-9a-fA-F]{64}$", b)
}

View File

@@ -3,7 +3,6 @@ package bytesutil_test
import (
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
@@ -378,8 +377,8 @@ func TestUint64ToBytes_RoundTrip(t *testing.T) {
}
func TestIsBytes32Hex(t *testing.T) {
pass, err := hexutil.Decode("0x1234567890abcDEF1234567890abcDEF1234567890abcDEF1234567890abcDEF")
require.NoError(t, err)
pass := []byte("0x1234567890abcDEF1234567890abcDEF1234567890abcDEF1234567890abcDEF")
noPrefix := []byte("1234567890abcDEF1234567890abcDEF1234567890abcDEF1234567890abcDEF")
tests := []struct {
a []byte
@@ -391,6 +390,7 @@ func TestIsBytes32Hex(t *testing.T) {
{[]byte("XYZ4567890abcDEF1234567890abcDEF1234567890abcDEF1234567890abcDEF"), false},
{[]byte("foo1234567890abcDEF1234567890abcDEF1234567890abcDEF1234567890abcDEFbar"), false},
{pass, true},
{noPrefix, true},
}
for _, tt := range tests {
isHex, err := bytesutil.IsBytes32Hex(tt.a)

View File

@@ -5,8 +5,10 @@ load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"api_middleware.go",
"gateway.go",
"log.go",
"middleware_structs.go",
],
importpath = "github.com/prysmaticlabs/prysm/shared/gateway",
visibility = [
@@ -17,12 +19,18 @@ go_library(
"//proto/beacon/rpc/v1:go_default_library",
"//proto/validator/accounts/v2:go_default_library",
"//shared:go_default_library",
"//shared/bytesutil:go_default_library",
"//shared/grpcutils:go_default_library",
"//validator/web:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_gorilla_mux//:go_default_library",
"@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_rs_cors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_wealdtech_go_bytesutil//:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//connectivity:go_default_library",
"@org_golang_google_grpc//credentials:go_default_library",

View File

@@ -0,0 +1,461 @@
package gateway
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/gorilla/mux"
"github.com/pkg/errors"
butil "github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/grpcutils"
"github.com/wealdtech/go-bytesutil"
)
// ApiProxyMiddleware is a proxy between an Eth2 API HTTP client and gRPC-gateway.
// The purpose of the proxy is to handle HTTP requests and gRPC responses in such a way that:
// - Eth2 API requests can be handled by gRPC-gateway correctly
// - gRPC responses can be returned as spec-compliant Eth2 API responses
type ApiProxyMiddleware struct {
GatewayAddress string
ProxyAddress string
router *mux.Router
}
type endpointData struct {
postRequest interface{}
getResponse interface{}
err ErrorJson
}
// fieldProcessor applies the processing function f to a value when the tag is present on the field.
type fieldProcessor struct {
tag string
f func(value reflect.Value) error
}
// Run starts the proxy, registering all proxy endpoints on ApiProxyMiddleware.ProxyAddress.
func (m *ApiProxyMiddleware) Run() error {
m.router = mux.NewRouter()
m.handleApiEndpoint("/eth/v1/beacon/genesis")
m.handleApiEndpoint("/eth/v1/beacon/states/{state_id}/root")
m.handleApiEndpoint("/eth/v1/beacon/states/{state_id}/fork")
m.handleApiEndpoint("/eth/v1/beacon/states/{state_id}/finality_checkpoints")
m.handleApiEndpoint("/eth/v1/beacon/headers/{block_id}")
m.handleApiEndpoint("/eth/v1/beacon/blocks")
m.handleApiEndpoint("/eth/v1/beacon/blocks/{block_id}")
m.handleApiEndpoint("/eth/v1/beacon/blocks/{block_id}/root")
m.handleApiEndpoint("/eth/v1/beacon/blocks/{block_id}/attestations")
m.handleApiEndpoint("/eth/v1/beacon/pool/attestations")
m.handleApiEndpoint("/eth/v1/beacon/pool/attester_slashings")
m.handleApiEndpoint("/eth/v1/beacon/pool/proposer_slashings")
m.handleApiEndpoint("/eth/v1/beacon/pool/voluntary_exits")
m.handleApiEndpoint("/eth/v1/node/identity")
m.handleApiEndpoint("/eth/v1/node/peers")
m.handleApiEndpoint("/eth/v1/node/peers/{peer_id}")
m.handleApiEndpoint("/eth/v1/node/peer_count")
m.handleApiEndpoint("/eth/v1/node/version")
m.handleApiEndpoint("/eth/v1/node/health")
m.handleApiEndpoint("/eth/v1/debug/beacon/states/{state_id}")
m.handleApiEndpoint("/eth/v1/debug/beacon/heads")
m.handleApiEndpoint("/eth/v1/config/fork_schedule")
m.handleApiEndpoint("/eth/v1/config/deposit_contract")
m.handleApiEndpoint("/eth/v1/config/spec")
return http.ListenAndServe(m.ProxyAddress, m.router)
}
func (m *ApiProxyMiddleware) handleApiEndpoint(endpoint string) {
m.router.HandleFunc(endpoint, func(writer http.ResponseWriter, request *http.Request) {
data, err := getEndpointData(endpoint)
if err != nil {
e := fmt.Errorf("could not prepare endpoint data: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
if request.Method == "POST" {
if err := wrapAttestationsArray(&data, request); err != nil {
e := fmt.Errorf("could not decode request body: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
// Deserialize the body.
if err := json.NewDecoder(request.Body).Decode(data.postRequest); err != nil {
e := fmt.Errorf("could not decode request body: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
prepareGraffiti(&data)
// Apply processing functions to fields with specific tags.
if err := processField(data.postRequest, []fieldProcessor{
{
tag: "hex",
f: hexToBase64Processor,
},
}); err != nil {
e := fmt.Errorf("could not process request data: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
// Serialize the struct, which now includes a base64-encoded value, into JSON.
j, err := json.Marshal(data.postRequest)
if err != nil {
e := fmt.Errorf("could not marshal request: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
// Set the body to the new JSON.
request.Body = ioutil.NopCloser(bytes.NewReader(j))
request.Header.Set("Content-Length", strconv.Itoa(len(j)))
request.ContentLength = int64(len(j))
}
// Prepare request values for proxying.
request.URL.Scheme = "http"
request.URL.Host = m.GatewayAddress
request.RequestURI = ""
handleUrlParameters(endpoint, request, writer)
// Proxy the request to grpc-gateway.
grpcResp, err := http.DefaultClient.Do(request)
if err != nil {
e := fmt.Errorf("could not proxy request: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
if grpcResp == nil {
writeError(writer, &DefaultErrorJson{Message: "nil response from gRPC-gateway", Code: http.StatusInternalServerError}, nil)
return
}
// Deserialize the output of grpc-gateway into the error struct.
body, err := ioutil.ReadAll(grpcResp.Body)
if err != nil {
e := fmt.Errorf("could not read response body: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
if err := json.Unmarshal(body, data.err); err != nil {
e := fmt.Errorf("could not unmarshal error: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
var j []byte
if data.err.Msg() != "" {
// Something went wrong, but the request completed, meaning we can write headers and the error message.
for h, vs := range grpcResp.Header {
for _, v := range vs {
writer.Header().Set(h, v)
}
}
// Set code to HTTP code because unmarshalled body contained gRPC code.
data.err.SetCode(grpcResp.StatusCode)
writeError(writer, data.err, grpcResp.Header)
return
// Don't do anything if the response is only a status code.
} else if request.Method == "GET" && data.getResponse != nil {
// Deserialize the output of grpc-gateway.
if err := json.Unmarshal(body, &data.getResponse); err != nil {
e := fmt.Errorf("could not unmarshal response: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
// Apply processing functions to fields with specific tags.
if err := processField(data.getResponse, []fieldProcessor{
{
tag: "hex",
f: base64ToHexProcessor,
},
{
tag: "enum",
f: enumToLowercaseProcessor,
},
{
tag: "time",
f: timeToUnixProcessor,
},
}); err != nil {
e := fmt.Errorf("could not process response data: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
// Serialize the return value into JSON.
j, err = json.Marshal(data.getResponse)
if err != nil {
e := fmt.Errorf("could not marshal response: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
}
// Write the response (headers + body) and PROFIT!
for h, vs := range grpcResp.Header {
for _, v := range vs {
writer.Header().Set(h, v)
}
}
if request.Method == "GET" {
writer.Header().Set("Content-Length", strconv.Itoa(len(j)))
writer.WriteHeader(grpcResp.StatusCode)
if _, err := io.Copy(writer, ioutil.NopCloser(bytes.NewReader(j))); err != nil {
e := fmt.Errorf("could not write response message: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
} else if request.Method == "POST" {
writer.WriteHeader(grpcResp.StatusCode)
}
// Final cleanup.
if err := grpcResp.Body.Close(); err != nil {
e := fmt.Errorf("could not close response body: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
})
}
// Posted graffiti needs to have length of 32 bytes, but client is allowed to send data of any length.
func prepareGraffiti(data *endpointData) {
if block, ok := data.postRequest.(*BeaconBlockContainerJson); ok {
b := bytesutil.ToBytes32([]byte(block.Message.Body.Graffiti))
block.Message.Body.Graffiti = hexutil.Encode(b[:])
}
}
// https://ethereum.github.io/eth2.0-APIs/#/Beacon/submitPoolAttestations expects posting a top-level array.
// We make it more proto-friendly by wrapping it in a struct with a 'data' field.
func wrapAttestationsArray(data *endpointData, req *http.Request) error {
if _, ok := data.postRequest.(*SubmitAttestationRequestJson); ok {
atts := make([]*AttestationJson, 0)
if err := json.NewDecoder(req.Body).Decode(&atts); err != nil {
return fmt.Errorf("could not decode attestations array: %w", err)
}
j := &SubmitAttestationRequestJson{Data: atts}
b, err := json.Marshal(j)
if err != nil {
return fmt.Errorf("could not marshal wrapped attestations array: %w", err)
}
req.Body = ioutil.NopCloser(bytes.NewReader(b))
}
return nil
}
// handleUrlParameters processes URL parameters, allowing parameterized URLs to be safely and correctly proxied to grpc-gateway.
func handleUrlParameters(endpoint string, request *http.Request, writer http.ResponseWriter) {
segments := strings.Split(endpoint, "/")
for i, s := range segments {
// We only care about segments which are parameterized.
if len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}' {
bRouteVar := []byte(mux.Vars(request)[s[1:len(s)-1]])
var routeVar string
isHex, err := butil.IsBytes32Hex(bRouteVar)
if err != nil {
e := fmt.Errorf("could not process URL parameter: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
if isHex {
bRouteVar, err = bytesutil.FromHexString(string(bRouteVar))
if err != nil {
e := fmt.Errorf("could not process URL parameter: %w", err)
writeError(writer, &DefaultErrorJson{Message: e.Error(), Code: http.StatusInternalServerError}, nil)
return
}
}
// Converting hex to base64 may result in a value which malforms the URL.
// We use URLEncoding to safely escape such values.
routeVar = base64.URLEncoding.EncodeToString(bRouteVar)
// Merge segments back into the full URL.
splitPath := strings.Split(request.URL.Path, "/")
splitPath[i] = routeVar
request.URL.Path = strings.Join(splitPath, "/")
}
}
}
func writeError(writer http.ResponseWriter, e ErrorJson, responseHeader http.Header) {
// Include custom error in the error JSON.
if responseHeader != nil {
customError, ok := responseHeader["Grpc-Metadata-"+grpcutils.CustomErrorMetadataKey]
if ok {
// Assume header has only one value and read the 0 index.
if err := json.Unmarshal([]byte(customError[0]), e); err != nil {
log.WithError(err).Error("Could not unmarshal custom error message")
return
}
}
}
j, err := json.Marshal(e)
if err != nil {
log.WithError(err).Error("Could not marshal error message")
return
}
writer.Header().Set("Content-Length", strconv.Itoa(len(j)))
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(e.StatusCode())
if _, err := io.Copy(writer, ioutil.NopCloser(bytes.NewReader(j))); err != nil {
log.WithError(err).Error("Could not write error message")
}
}
// processField calls each processor function on any field that has the matching tag set.
// It is a recursive function.
func processField(s interface{}, processors []fieldProcessor) error {
t := reflect.TypeOf(s).Elem()
v := reflect.Indirect(reflect.ValueOf(s))
for i := 0; i < t.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.Slice:
sliceElem := t.Field(i).Type.Elem()
kind := sliceElem.Kind()
// Recursively process slices to struct pointers.
if kind == reflect.Ptr && sliceElem.Elem().Kind() == reflect.Struct {
for j := 0; j < v.Field(i).Len(); j++ {
if err := processField(v.Field(i).Index(j).Interface(), processors); err != nil {
return fmt.Errorf("could not process field '%s': %w", t.Field(i).Name, err)
}
}
}
// Process each string in string slices.
if kind == reflect.String {
for _, proc := range processors {
_, hasTag := t.Field(i).Tag.Lookup(proc.tag)
if hasTag {
for j := 0; j < v.Field(i).Len(); j++ {
if err := proc.f(v.Field(i).Index(j)); err != nil {
return fmt.Errorf("could not process field '%s': %w", t.Field(i).Name, err)
}
}
}
}
}
// Recursively process struct pointers.
case reflect.Ptr:
if v.Field(i).Elem().Kind() == reflect.Struct {
if err := processField(v.Field(i).Interface(), processors); err != nil {
return fmt.Errorf("could not process field '%s': %w", t.Field(i).Name, err)
}
}
default:
field := t.Field(i)
for _, proc := range processors {
if _, hasTag := field.Tag.Lookup(proc.tag); hasTag {
if err := proc.f(v.Field(i)); err != nil {
return fmt.Errorf("could not process field '%s': %w", t.Field(i).Name, err)
}
}
}
}
}
return nil
}
func hexToBase64Processor(v reflect.Value) error {
b, err := bytesutil.FromHexString(v.String())
if err != nil {
return err
}
v.SetString(base64.StdEncoding.EncodeToString(b))
return nil
}
func base64ToHexProcessor(v reflect.Value) error {
b, err := base64.StdEncoding.DecodeString(v.String())
if err != nil {
return err
}
v.SetString(hexutil.Encode(b))
return nil
}
func enumToLowercaseProcessor(v reflect.Value) error {
v.SetString(strings.ToLower(v.String()))
return nil
}
func timeToUnixProcessor(v reflect.Value) error {
t, err := time.Parse(time.RFC3339, v.String())
if err != nil {
return err
}
v.SetString(strconv.FormatUint(uint64(t.Unix()), 10))
return nil
}
// getEndpointData constructs and returns a struct containing necessary information to process a request based on the provided endpoint.
// The returned struct is meant to be used during a single request.
func getEndpointData(endpoint string) (endpointData, error) {
switch endpoint {
case "/eth/v1/beacon/genesis":
return endpointData{getResponse: &GenesisResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/states/{state_id}/root":
return endpointData{getResponse: &StateRootResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/states/{state_id}/fork":
return endpointData{getResponse: &StateForkResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/states/{state_id}/finality_checkpoints":
return endpointData{getResponse: &StateFinalityCheckpointResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/headers/{block_id}":
return endpointData{getResponse: &BlockHeaderResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/blocks":
return endpointData{postRequest: &BeaconBlockContainerJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/blocks/{block_id}":
return endpointData{getResponse: &BlockResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/blocks/{block_id}/root":
return endpointData{getResponse: &BlockRootResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/blocks/{block_id}/attestations":
return endpointData{getResponse: &BlockAttestationsResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/pool/attestations":
return endpointData{postRequest: &SubmitAttestationRequestJson{}, getResponse: &BlockAttestationsResponseJson{}, err: &SubmitAttestationsErrorJson{}}, nil
case "/eth/v1/beacon/pool/attester_slashings":
return endpointData{postRequest: &AttesterSlashingJson{}, getResponse: &AttesterSlashingsPoolResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/pool/proposer_slashings":
return endpointData{postRequest: &ProposerSlashingJson{}, getResponse: &ProposerSlashingsPoolResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/beacon/pool/voluntary_exits":
return endpointData{postRequest: &SignedVoluntaryExitJson{}, getResponse: &VoluntaryExitsPoolResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/node/identity":
return endpointData{getResponse: &IdentityResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/node/peers":
return endpointData{getResponse: &PeersResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/node/peers/{peer_id}":
return endpointData{getResponse: &PeerResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/node/peer_count":
return endpointData{getResponse: &PeerCountResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/node/version":
return endpointData{getResponse: &VersionResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/node/health":
return endpointData{err: &DefaultErrorJson{}}, nil
case "/eth/v1/debug/beacon/states/{state_id}":
return endpointData{getResponse: &BeaconStateResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/debug/beacon/heads":
return endpointData{getResponse: &ForkChoiceHeadsResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/config/fork_schedule":
return endpointData{getResponse: &ForkScheduleResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/config/deposit_contract":
return endpointData{getResponse: &DepositContractResponseJson{}, err: &DefaultErrorJson{}}, nil
case "/eth/v1/config/spec":
return endpointData{getResponse: &SpecResponseJson{}, err: &DefaultErrorJson{}}, nil
default:
return endpointData{}, errors.New("invalid endpoint")
}
}

View File

@@ -13,6 +13,7 @@ import (
gwruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/pkg/errors"
ethpbv1 "github.com/prysmaticlabs/ethereumapis/eth/v1"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
pbrpc "github.com/prysmaticlabs/prysm/proto/beacon/rpc/v1"
pb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
@@ -48,6 +49,7 @@ type Gateway struct {
cancel context.CancelFunc
remoteCert string
gatewayAddr string
apiMiddlewareAddr string
ctx context.Context
startFailure error
remoteAddr string
@@ -79,6 +81,7 @@ func NewBeacon(
remoteAddress,
remoteCert,
gatewayAddress string,
apiMiddlewareAddress string,
mux *http.ServeMux,
allowedOrigins []string,
enableDebugRPCEndpoints bool,
@@ -93,6 +96,7 @@ func NewBeacon(
remoteAddr: remoteAddress,
remoteCert: remoteCert,
gatewayAddr: gatewayAddress,
apiMiddlewareAddr: apiMiddlewareAddress,
ctx: ctx,
mux: mux,
allowedOrigins: allowedOrigins,
@@ -132,24 +136,52 @@ func (g *Gateway) Start() {
}),
)
if g.callerId == Beacon {
gwmuxV1 := gwruntime.NewServeMux(
gwruntime.WithMarshalerOption(gwruntime.MIMEWildcard, &gwruntime.HTTPBodyMarshaler{
Marshaler: &gwruntime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
},
}),
)
handlers := []func(context.Context, *gwruntime.ServeMux, *grpc.ClientConn) error{
ethpb.RegisterNodeHandler,
ethpb.RegisterBeaconChainHandler,
ethpb.RegisterBeaconNodeValidatorHandler,
pbrpc.RegisterHealthHandler,
}
handlersV1 := []func(context.Context, *gwruntime.ServeMux, *grpc.ClientConn) error{
ethpbv1.RegisterBeaconNodeHandler,
ethpbv1.RegisterBeaconChainHandler,
ethpbv1.RegisterBeaconValidatorHandler,
}
if g.enableDebugRPCEndpoints {
handlers = append(handlers, pbrpc.RegisterDebugHandler)
handlersV1 = append(handlersV1, ethpbv1.RegisterBeaconDebugHandler)
}
for _, f := range handlers {
if err := f(ctx, gwmux, g.conn); err != nil {
log.WithError(err).Error("Failed to start gateway")
log.WithError(err).Error("Failed to start v1alpha1 gateway")
g.startFailure = err
return
}
}
for _, f := range handlersV1 {
if err := f(ctx, gwmuxV1, g.conn); err != nil {
log.WithError(err).Error("Failed to start v1 gateway")
g.startFailure = err
return
}
}
g.mux.Handle("/", gwmux)
g.mux.Handle("/eth/v1alpha1/", gwmux)
g.mux.Handle("/eth/v1/", gwmuxV1)
g.server = &http.Server{
Addr: g.gatewayAddr,
Handler: g.corsMiddleware(g.mux),
@@ -187,7 +219,20 @@ func (g *Gateway) Start() {
go func() {
log.WithField("address", g.gatewayAddr).Info("Starting gRPC gateway")
if err := g.server.ListenAndServe(); err != http.ErrServerClosed {
log.WithError(err).Error("Failed to listen and serve")
log.WithError(err).Error("Failed to start gRPC gateway")
g.startFailure = err
return
}
}()
go func() {
proxy := &ApiProxyMiddleware{
GatewayAddress: g.gatewayAddr,
ProxyAddress: g.apiMiddlewareAddr,
}
log.WithField("API middleware address", g.apiMiddlewareAddr).Info("Starting API middleware")
if err := proxy.Run(); err != http.ErrServerClosed {
log.WithError(err).Error("Failed to start API middleware")
g.startFailure = err
return
}

View File

@@ -21,10 +21,12 @@ func TestBeaconGateway_StartStop(t *testing.T) {
ctx := cli.NewContext(&app, set, nil)
gatewayPort := ctx.Int(flags.GRPCGatewayPort.Name)
apiMiddlewarePort := ctx.Int(flags.ApiMiddlewarePort.Name)
gatewayHost := ctx.String(flags.GRPCGatewayHost.Name)
rpcHost := ctx.String(flags.RPCHost.Name)
selfAddress := fmt.Sprintf("%s:%d", rpcHost, ctx.Int(flags.RPCPort.Name))
gatewayAddress := fmt.Sprintf("%s:%d", gatewayHost, gatewayPort)
apiMiddlewareAddress := fmt.Sprintf("%s:%d", gatewayHost, apiMiddlewarePort)
allowedOrigins := strings.Split(ctx.String(flags.GPRCGatewayCorsDomain.Name), ",")
enableDebugRPCEndpoints := ctx.Bool(flags.EnableDebugRPCEndpoints.Name)
selfCert := ctx.String(flags.CertFlag.Name)
@@ -34,6 +36,7 @@ func TestBeaconGateway_StartStop(t *testing.T) {
selfAddress,
selfCert,
gatewayAddress,
apiMiddlewareAddress,
nil, /*optional mux*/
allowedOrigins,
enableDebugRPCEndpoints,
@@ -43,6 +46,7 @@ func TestBeaconGateway_StartStop(t *testing.T) {
beaconGateway.Start()
go func() {
require.LogsContain(t, hook, "Starting gRPC gateway")
require.LogsContain(t, hook, "Starting API middleware")
}()
err := beaconGateway.Stop()

View File

@@ -0,0 +1,413 @@
package gateway
// GenesisResponseJson is used in /beacon/genesis API endpoint.
type GenesisResponseJson struct {
Data *GenesisResponse_GenesisJson `json:"data"`
}
// GenesisResponse_GenesisJson is used in /beacon/genesis API endpoint.
type GenesisResponse_GenesisJson struct {
GenesisTime string `json:"genesis_time" time:"true"`
GenesisValidatorsRoot string `json:"genesis_validators_root" hex:"true"`
GenesisForkVersion string `json:"genesis_fork_version" hex:"true"`
}
// StateRootResponseJson is used in /beacon/states/{state_id}/root API endpoint.
type StateRootResponseJson struct {
Data *StateRootResponse_StateRootJson `json:"data"`
}
// StateRootResponse_StateRootJson is used in /beacon/states/{state_id}/root API endpoint.
type StateRootResponse_StateRootJson struct {
StateRoot string `json:"state_root" hex:"true"`
}
// StateForkResponseJson is used in /beacon/states/{state_id}/fork API endpoint.
type StateForkResponseJson struct {
Data *ForkJson `json:"data"`
}
// StateFinalityCheckpointResponseJson is used in /beacon/states/{state_id}/finality_checkpoints API endpoint.
type StateFinalityCheckpointResponseJson struct {
Data *StateFinalityCheckpointResponse_StateFinalityCheckpointJson `json:"data"`
}
// StateFinalityCheckpointResponse_StateFinalityCheckpointJson is used in /beacon/states/{state_id}/finality_checkpoints API endpoint.
type StateFinalityCheckpointResponse_StateFinalityCheckpointJson struct {
PreviousJustified *CheckpointJson `json:"previous_justified"`
CurrentJustified *CheckpointJson `json:"current_justified"`
Finalized *CheckpointJson `json:"finalized"`
}
// BlockHeaderResponseJson is used in /beacon/headers/{block_id} API endpoint.
type BlockHeaderResponseJson struct {
Data *BlockHeaderContainerJson `json:"data"`
}
// BlockResponseJson is used in /beacon/blocks/{block_id} API endpoint.
type BlockResponseJson struct {
Data *BeaconBlockContainerJson `json:"data"`
}
// BlockRootResponseJson is used in /beacon/blocks/{block_id}/root API endpoint.
type BlockRootResponseJson struct {
Data *BlockRootContainerJson `json:"data"`
}
// BlockAttestationsResponseJson is used in /beacon/blocks/{block_id}/attestations GET API endpoint.
type BlockAttestationsResponseJson struct {
Data []*AttestationJson `json:"data"`
}
// SubmitAttestationRequestJson is used in /beacon/blocks/{block_id}/attestations POST API endpoint.
type SubmitAttestationRequestJson struct {
Data []*AttestationJson `json:"data"`
}
// AttesterSlashingsPoolResponseJson is used in /beacon/pool/attester_slashings API endpoint.
type AttesterSlashingsPoolResponseJson struct {
Data []*AttesterSlashingJson `json:"data"`
}
// ProposerSlashingsPoolResponseJson is used in /beacon/pool/proposer_slashings API endpoint.
type ProposerSlashingsPoolResponseJson struct {
Data []*ProposerSlashingJson `json:"data"`
}
// VoluntaryExitsPoolResponseJson is used in /beacon/pool/voluntary_exits API endpoint.
type VoluntaryExitsPoolResponseJson struct {
Data []*SignedVoluntaryExitJson `json:"data"`
}
// IdentityResponseJson is used in /node/identity API endpoint.
type IdentityResponseJson struct {
Data *IdentityJson `json:"data"`
}
// PeersResponseJson is used in /node/peers API endpoint.
type PeersResponseJson struct {
Data []*PeerJson `json:"data"`
}
// PeerResponseJson is used in /node/peers/{peer_id} API endpoint.
type PeerResponseJson struct {
Data *PeerJson `json:"data"`
}
// PeerCountResponseJson is used in /node/peer_count API endpoint.
type PeerCountResponseJson struct {
Data PeerCountResponse_PeerCountJson `json:"data"`
}
// PeerCountResponse_PeerCountJson is used in /node/peer_count API endpoint.
type PeerCountResponse_PeerCountJson struct {
Disconnected string `json:"disconnected"`
Connecting string `json:"connecting"`
Connected string `json:"connected"`
Disconnecting string `json:"disconnecting"`
}
// VersionResponseJson is used in /node/version API endpoint.
type VersionResponseJson struct {
Data *VersionJson `json:"data"`
}
// BeaconStateResponseJson is used in /debug/beacon/states/{state_id} API endpoint.
type BeaconStateResponseJson struct {
Data *BeaconStateJson `json:"data"`
}
// ForkChoiceHeadsResponseJson is used in /debug/beacon/heads API endpoint.
type ForkChoiceHeadsResponseJson struct {
Data []*ForkChoiceHeadJson `json:"data"`
}
// ForkScheduleResponseJson is used in /config/fork_schedule API endpoint.
type ForkScheduleResponseJson struct {
Data []*ForkJson `json:"data"`
}
// DepositContractResponseJson is used in /config/deposit_contract API endpoint.
type DepositContractResponseJson struct {
Data *DepositContractJson `json:"data"`
}
// SpecResponseJson is used in /config/spec API endpoint.
type SpecResponseJson struct {
Data interface{} `json:"data"`
}
//----------------
// Reusable types.
//----------------
// CheckpointJson is a JSON representation of a checkpoint.
type CheckpointJson struct {
Epoch string `json:"epoch"`
Root string `json:"root" hex:"true"`
}
// BlockRootContainerJson is a JSON representation of a block root container.
type BlockRootContainerJson struct {
Root string `json:"root" hex:"true"`
}
// BeaconBlockContainerJson is a JSON representation of a beacon block container.
type BeaconBlockContainerJson struct {
Message *BeaconBlockJson `json:"message"`
Signature string `json:"signature" hex:"true"`
}
// BeaconBlockJson is a JSON representation of a beacon block.
type BeaconBlockJson struct {
Slot string `json:"slot"`
ProposerIndex string `json:"proposer_index"`
ParentRoot string `json:"parent_root" hex:"true"`
StateRoot string `json:"state_root" hex:"true"`
Body *BeaconBlockBodyJson `json:"body"`
}
// BeaconBlockBodyJson is a JSON representation of a beacon block body.
type BeaconBlockBodyJson struct {
RandaoReveal string `json:"randao_reveal" hex:"true"`
Eth1Data *Eth1DataJson `json:"eth1_data"`
Graffiti string `json:"graffiti" hex:"true"`
ProposerSlashings []*ProposerSlashingJson `json:"proposer_slashings"`
AttesterSlashings []*AttesterSlashingJson `json:"attester_slashings"`
Attestations []*AttestationJson `json:"attestations"`
Deposits []*DepositJson `json:"deposits"`
VoluntaryExits []*SignedVoluntaryExitJson `json:"voluntary_exits"`
}
// BlockHeaderContainerJson is a JSON representation of a block header container.
type BlockHeaderContainerJson struct {
Root string `json:"root" hex:"true"`
Canonical bool `json:"canonical"`
Header *BeaconBlockHeaderContainerJson `json:"header"`
}
// BeaconBlockHeaderContainerJson is a JSON representation of a beacon block header container.
type BeaconBlockHeaderContainerJson struct {
Message *BeaconBlockHeaderJson `json:"message"`
Signature string `json:"signature" hex:"true"`
}
// SignedBeaconBlockHeaderJson is a JSON representation of a signed beacon block header.
type SignedBeaconBlockHeaderJson struct {
Header *BeaconBlockHeaderJson `json:"header"`
Signature string `json:"signature" hex:"true"`
}
// BeaconBlockHeaderJson is a JSON representation of a beacon block header.
type BeaconBlockHeaderJson struct {
Slot string `json:"slot"`
ProposerIndex string `json:"proposer_index"`
ParentRoot string `json:"parent_root" hex:"true"`
StateRoot string `json:"state_root" hex:"true"`
BodyRoot string `json:"body_root" hex:"true"`
}
// Eth1DataJson is a JSON representation of eth1data.
type Eth1DataJson struct {
DepositRoot string `json:"deposit_root" hex:"true"`
DepositCount string `json:"deposit_count"`
BlockHash string `json:"block_hash" hex:"true"`
}
// ProposerSlashingJson is a JSON representation of a proposer slashing.
type ProposerSlashingJson struct {
Header_1 *SignedBeaconBlockHeaderJson `json:"header_1"`
Header_2 *SignedBeaconBlockHeaderJson `json:"header_2"`
}
// AttesterSlashingJson is a JSON representation of an attester slashing.
type AttesterSlashingJson struct {
Attestation_1 *IndexedAttestationJson `json:"attestation_1"`
Attestation_2 *IndexedAttestationJson `json:"attestation_2"`
}
// IndexedAttestationJson is a JSON representation of an indexed attestation.
type IndexedAttestationJson struct {
AttestingIndices []string `json:"attesting_indices"`
Data *AttestationDataJson `json:"data"`
Signature string `json:"signature" hex:"true"`
}
// AttestationJson is a JSON representation of an attestation.
type AttestationJson struct {
AggregationBits string `json:"aggregation_bits" hex:"true"`
Data *AttestationDataJson `json:"data"`
Signature string `json:"signature" hex:"true"`
}
// AttestationDataJson is a JSON representation of attestation data.
type AttestationDataJson struct {
Slot string `json:"slot"`
CommitteeIndex string `json:"committee_index"`
BeaconBlockRoot string `json:"beacon_block_root" hex:"true"`
Source *CheckpointJson `json:"source"`
Target *CheckpointJson `json:"target"`
}
// DepositJson is a JSON representation of a deposit.
type DepositJson struct {
Proof []string `json:"proof" hex:"true"`
Data *Deposit_DataJson `json:"data"`
}
// Deposit_DataJson is a JSON representation of deposit data.
type Deposit_DataJson struct {
PublicKey string `json:"public_key" hex:"true"`
WithdrawalCredentials string `json:"withdrawal_credentials" hex:"true"`
Amount string `json:"amount"`
Signature string `json:"signature" hex:"true"`
}
// SignedVoluntaryExitJson is a JSON representation of a signed voluntary exit.
type SignedVoluntaryExitJson struct {
Exit *VoluntaryExitJson `json:"exit"`
Signature string `json:"signature" hex:"true"`
}
// VoluntaryExitJson is a JSON representation of a voluntary exit.
type VoluntaryExitJson struct {
Epoch string `json:"epoch"`
ValidatorIndex string `json:"validator_index"`
}
// IdentityJson is a JSON representation of a peer's identity.
type IdentityJson struct {
PeerId string `json:"peer_id"`
Enr string `json:"enr"`
P2PAddresses []string `json:"p2p_addresses"`
DiscoveryAddresses []string `json:"discovery_addresses"`
Metadata *MetadataJson `json:"metadata"`
}
// MetadataJson is a JSON representation of p2p metadata.
type MetadataJson struct {
SeqNumber string `json:"seq_number"`
Attnets string `json:"attnets" hex:"true"`
}
// PeerJson is a JSON representation of a peer.
type PeerJson struct {
PeerId string `json:"peer_id"`
Enr string `json:"enr"`
Address string `json:"address"`
State string `json:"state" enum:"true"`
Direction string `json:"direction" enum:"true"`
}
// VersionJson is a JSON representation of the system's version.
type VersionJson struct {
Version string `json:"version"`
}
// BeaconStateJson is a JSON representation of the beacon state.
type BeaconStateJson struct {
GenesisTime string `json:"genesis_time"`
GenesisValidatorsRoot string `json:"genesis_validators_root" hex:"true"`
Slot string `json:"slot"`
Fork *ForkJson `json:"fork"`
LatestBlockHeader *BeaconBlockHeaderJson `json:"latest_block_header"`
BlockRoots []string `json:"block_roots" hex:"true"`
StateRoots []string `json:"state_roots" hex:"true"`
HistoricalRoots []string `json:"historical_roots" hex:"true"`
Eth1Data *Eth1DataJson `json:"eth1_data"`
Eth1DataVotes []*Eth1DataJson `json:"eth1_data_votes"`
Eth1DepositIndex string `json:"eth1_deposit_index"`
Validators []*ValidatorJson `json:"validators"`
Balances []string `json:"balances"`
RandaoMixes []string `json:"randao_mixes" hex:"true"`
Slashings []string `json:"slashings"`
PreviousEpochAttestations []*PendingAttestationJson `json:"previous_epoch_attestations"`
CurrentEpochAttestations []*PendingAttestationJson `json:"current_epoch_attestations"`
JustificationBits string `json:"justification_bits" hex:"true"`
PreviousJustifiedCheckpoint *CheckpointJson `json:"previous_justified_checkpoint"`
CurrentJustifiedCheckpoint *CheckpointJson `json:"current_justified_checkpoint"`
FinalizedCheckpoint *CheckpointJson `json:"finalized_checkpoint"`
}
// ForkJson is a JSON representation of a fork.
type ForkJson struct {
PreviousVersion string `json:"previous_version" hex:"true"`
CurrentVersion string `json:"current_version" hex:"true"`
Epoch string `json:"epoch"`
}
// ValidatorJson is a JSON representation of a validator.
type ValidatorJson struct {
PublicKey string `json:"public_key" hex:"true"`
WithdrawalCredentials string `json:"withdrawal_credentials" hex:"true"`
EffectiveBalance string `json:"effective_balance"`
Slashed bool `json:"slashed"`
ActivationEligibilityEpoch string `json:"activation_eligibility_epoch"`
ActivationEpoch string `json:"activation_epoch"`
ExitEpoch string `json:"exit_epoch"`
WithdrawableEpoch string `json:"withdrawable_epoch"`
}
// PendingAttestationJson is a JSON representation of a pending attestation.
type PendingAttestationJson struct {
AggregationBits string `json:"aggregation_bits" hex:"true"`
Data *AttestationDataJson `json:"data"`
InclusionDelay string `json:"inclusion_delay"`
ProposerIndex string `json:"proposer_index"`
}
// ForkChoiceHeadJson is a JSON representation of a fork choice head.
type ForkChoiceHeadJson struct {
Root string `json:"root" hex:"true"`
Slot string `json:"slot"`
}
// DepositContractJson is a JSON representation of the deposit contract.
type DepositContractJson struct {
ChainId string `json:"chain_id"`
Address string `json:"address"`
}
// ---------------
// Error handling.
// ---------------
// ErrorJson describes common functionality of all JSON error representations.
type ErrorJson interface {
StatusCode() int
SetCode(code int)
Msg() string
}
// DefaultErrorJson is a JSON representation of a simple error value, containing only a message and an error code.
type DefaultErrorJson struct {
Message string `json:"message"`
Code int `json:"code"`
}
// SubmitAttestationsErrorJson is a JSON representation of the error returned when submitting attestations.
type SubmitAttestationsErrorJson struct {
DefaultErrorJson
Failures []*SingleAttestationVerificationFailureJson `json:"failures"`
}
// SingleAttestationVerificationFailureJson is a JSON representation of a failure when verifying a single submitted attestation.
type SingleAttestationVerificationFailureJson struct {
Index int `json:"index"`
Message string `json:"message"`
}
// StatusCode returns the error's underlying error code.
func (e *DefaultErrorJson) StatusCode() int {
return e.Code
}
// Msg returns the error's underlying message.
func (e *DefaultErrorJson) Msg() string {
return e.Message
}
// SetCode sets the error's underlying error code.
func (e *DefaultErrorJson) SetCode(code int) {
e.Code = code
}

View File

@@ -3,7 +3,10 @@ load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["grpcutils.go"],
srcs = [
"grpcutils.go",
"parameters.go",
],
importpath = "github.com/prysmaticlabs/prysm/shared/grpcutils",
visibility = ["//visibility:public"],
deps = [
@@ -20,7 +23,9 @@ go_test(
deps = [
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//metadata:go_default_library",
],
)

View File

@@ -2,6 +2,8 @@ package grpcutils
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
@@ -79,3 +81,16 @@ func AppendHeaders(parent context.Context, headers []string) context.Context {
}
return parent
}
// AppendCustomErrorHeader sets a CustomErrorMetadataKey gRPC header on the passed in context,
// using the passed in error data as the header's value. The data is serialized as JSON.
func AppendCustomErrorHeader(ctx context.Context, errorData interface{}) error {
j, err := json.Marshal(errorData)
if err != nil {
return fmt.Errorf("could not marshal error data into JSON: %w", err)
}
if err := grpc.SetHeader(ctx, metadata.Pairs(CustomErrorMetadataKey, string(j))); err != nil {
return fmt.Errorf("could not set custom error header: %w", err)
}
return nil
}

View File

@@ -2,14 +2,22 @@ package grpcutils
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
logTest "github.com/sirupsen/logrus/hooks/test"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
type customErrorData struct {
Message string `json:"message"`
}
func TestAppendHeaders(t *testing.T) {
t.Run("One header", func(t *testing.T) {
ctx := AppendHeaders(context.Background(), []string{"first=value1"})
@@ -54,3 +62,17 @@ func TestAppendHeaders(t *testing.T) {
assert.Equal(t, "value=1", md.Get("first")[0])
})
}
func TestAppendCustomErrorHeader(t *testing.T) {
stream := &runtime.ServerTransportStream{}
ctx := grpc.NewContextWithServerTransportStream(context.Background(), stream)
data := &customErrorData{Message: "foo"}
require.NoError(t, AppendCustomErrorHeader(ctx, data))
// The stream used in test setup sets the metadata key in lowercase.
value, ok := stream.Header()[strings.ToLower(CustomErrorMetadataKey)]
require.Equal(t, true, ok, "Failed to retrieve custom error metadata value")
expected, err := json.Marshal(data)
require.NoError(t, err)
assert.Equal(t, string(expected), value[0])
}

View File

@@ -0,0 +1,5 @@
package grpcutils
// CustomErrorMetadataKey is the name of the metadata key storing additional error information.
// Metadata value is expected to be a byte-encoded JSON object.
const CustomErrorMetadataKey = "Custom-Error"