mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
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:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ðpb.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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -39,6 +39,7 @@ var appFlags = []cli.Flag{
|
||||
flags.DisableGRPCGateway,
|
||||
flags.GRPCGatewayHost,
|
||||
flags.GRPCGatewayPort,
|
||||
flags.ApiMiddlewarePort,
|
||||
flags.GPRCGatewayCorsDomain,
|
||||
flags.MinSyncPeers,
|
||||
flags.ContractDeploymentBlock,
|
||||
|
||||
@@ -102,6 +102,7 @@ var appHelpFlagGroups = []flagGroup{
|
||||
flags.DisableGRPCGateway,
|
||||
flags.GRPCGatewayHost,
|
||||
flags.GRPCGatewayPort,
|
||||
flags.ApiMiddlewarePort,
|
||||
flags.GPRCGatewayCorsDomain,
|
||||
flags.HTTPWeb3ProviderFlag,
|
||||
flags.FallbackWeb3ProviderFlag,
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
1
go.sum
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
461
shared/gateway/api_middleware.go
Normal file
461
shared/gateway/api_middleware.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
413
shared/gateway/middleware_structs.go
Normal file
413
shared/gateway/middleware_structs.go
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
}
|
||||
|
||||
5
shared/grpcutils/parameters.go
Normal file
5
shared/grpcutils/parameters.go
Normal 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"
|
||||
Reference in New Issue
Block a user