node: refactor package node (#21105)

This PR significantly changes the APIs for instantiating Ethereum nodes in
a Go program. The new APIs are not backwards-compatible, but we feel that
this is made up for by the much simpler way of registering services on
node.Node. You can find more information and rationale in the design
document: https://gist.github.com/renaynay/5bec2de19fde66f4d04c535fd24f0775.

There is also a new feature in Node's Go API: it is now possible to
register arbitrary handlers on the user-facing HTTP server. In geth, this
facility is used to enable GraphQL.

There is a single minor change relevant for geth users in this PR: The
GraphQL API is no longer available separately from the JSON-RPC HTTP
server. If you want GraphQL, you need to enable it using the
./geth --http --graphql flag combination.

The --graphql.port and --graphql.addr flags are no longer available.

# Conflicts:
#	cmd/faucet/faucet.go
#	cmd/geth/chaincmd.go
#	cmd/geth/config.go
#	cmd/geth/consolecmd.go
#	cmd/geth/main.go
#	cmd/utils/flags.go
#	cmd/wnode/main.go
#	core/rawdb/freezer.go
#	eth/api_backend.go
#	eth/backend.go
#	ethclient/ethclient_test.go
#	ethstats/ethstats.go
#	graphql/service.go
#	internal/ethapi/backend.go
#	les/api_backend.go
#	les/api_test.go
#	les/checkpointoracle/oracle.go
#	les/client.go
#	les/commons.go
#	les/server.go
#	miner/stresstest/stress_clique.go
#	miner/stresstest/stress_ethash.go
#	mobile/geth.go
#	node/api.go
#	node/node.go
#	node/node_example_test.go
#	node/node_test.go
#	node/rpcstack.go
#	node/rpcstack_test.go
#	node/service.go
#	node/service_test.go
#	node/utils_test.go
#	p2p/simulations/examples/ping-pong.go
#	p2p/testing/peerpool.go
#	p2p/testing/protocolsession.go
#	p2p/testing/protocoltester.go
#	whisper/mailserver/server_test.go
#	whisper/whisperv6/api_test.go
#	whisper/whisperv6/filter_test.go
#	whisper/whisperv6/whisper.go
#	whisper/whisperv6/whisper_test.go
This commit is contained in:
rene 2020-08-03 19:40:46 +02:00 committed by Igor Mandrigin
parent 8d68d97cad
commit fe1bb62bad
48 changed files with 2298 additions and 2647 deletions

View File

@ -231,8 +231,9 @@ func initGenesis(ctx *cli.Context) error {
if err := json.NewDecoder(file).Decode(genesis); err != nil {
utils.Fatalf("invalid genesis file: %v", err)
}
// Open an initialise both full and light databases
stack, _ := makeFullNode(ctx)
stack, _ := makeConfigNode(ctx)
defer stack.Close()
for _, name := range []string{"chaindata", "lightchaindata"} {

View File

@ -28,6 +28,7 @@ import (
"github.com/ledgerwatch/turbo-geth/cmd/utils"
"github.com/ledgerwatch/turbo-geth/eth"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ledgerwatch/turbo-geth/node"
"github.com/ledgerwatch/turbo-geth/params"
@ -131,19 +132,20 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
return stack, cfg
}
func makeFullNode(ctx *cli.Context) (*node.Node, *eth.Ethereum) {
func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
stack, cfg := makeConfigNode(ctx)
service := utils.RegisterEthService(stack, &cfg.Eth)
backend := utils.RegisterEthService(stack, &cfg.Eth)
// Configure GraphQL if required
if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) {
utils.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.GraphQLCors, cfg.Node.GraphQLVirtualHosts, cfg.Node.HTTPTimeouts)
utils.RegisterGraphQLService(stack, backend, cfg.Node)
}
// Add the Ethereum Stats daemon if requested.
if cfg.Ethstats.URL != "" {
utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL)
}
return stack, service
return stack, backend
}
// dumpConfig is the dumpconfig command.

View File

@ -80,26 +80,13 @@ JavaScript API. See https://github.com/ledgerwatch/turbo-geth/wiki/JavaScript-Co
func localConsole(ctx *cli.Context) error {
// Create and start the node based on the CLI flags
prepare(ctx)
stack, ethService := makeFullNode(ctx)
stack, backend := makeFullNode(ctx)
err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
diskdb, err := ctx.OpenDatabaseWithFreezer("chaindata", "")
if err != nil {
return nil, err
}
return service.New(diskdb, ethService.TxPool()), nil
})
if err != nil {
panic(err)
}
node := stack
startNode(ctx, node)
defer node.Close()
startNode(ctx, stack, backend)
defer stack.Close()
// Attach to the newly started node and start the JavaScript console
client, err := node.Attach()
client, err := stack.Attach()
if err != nil {
utils.Fatalf("Failed to attach to the inproc geth: %v", err)
}
@ -206,12 +193,12 @@ func dialRPC(endpoint string) (*rpc.Client, error) {
// everything down.
func ephemeralConsole(ctx *cli.Context) error {
// Create and start the node based on the CLI flags
node, _ := makeFullNode(ctx)
startNode(ctx, node)
defer node.Close()
stack, backend := makeFullNode(ctx)
startNode(ctx, stack, backend)
defer stack.Close()
// Attach to the newly started node and start the JavaScript console
client, err := node.Attach()
client, err := stack.Attach()
if err != nil {
utils.Fatalf("Failed to attach to the inproc geth: %v", err)
}

View File

@ -120,8 +120,7 @@ func testDAOForkBlockNewChain(t *testing.T, test int, genesis string, expectBloc
} else {
// Force chain initialization
args := []string{"--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none", "--ipcdisable", "--datadir", datadir}
geth := runGeth(t, append(args, []string{"--exec", "2+2", "console"}...)...)
geth.WaitExit()
runGeth(t, append(args, []string{"--exec", "2+2", "console"}...)...).WaitExit()
}
// Retrieve the DAO config flag from the database
path := filepath.Join(datadir, "tg", "chaindata")

View File

@ -174,8 +174,6 @@ var (
utils.LegacyRPCCORSDomainFlag,
utils.LegacyRPCVirtualHostsFlag,
utils.GraphQLEnabledFlag,
utils.GraphQLListenAddrFlag,
utils.GraphQLPortFlag,
utils.GraphQLCORSDomainFlag,
utils.GraphQLVirtualHostsFlag,
utils.HTTPApiFlag,
@ -347,18 +345,20 @@ func geth(ctx *cli.Context) error {
if args := ctx.Args(); len(args) > 0 {
return fmt.Errorf("invalid command: %q", args[0])
}
prepare(ctx)
node, _ := makeFullNode(ctx)
defer node.Close()
startNode(ctx, node)
node.Wait()
stack, backend := makeFullNode(ctx)
defer stack.Close()
startNode(ctx, stack, backend)
stack.Wait()
return nil
}
// startNode boots up the system node and all registered protocols, after which
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
// miner.
func startNode(ctx *cli.Context, stack *node.Node) {
func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend) {
debug.Memsize.Add("node", stack)
// Start up the node itself
@ -378,16 +378,6 @@ func startNode(ctx *cli.Context, stack *node.Node) {
}
ethClient := ethclient.NewClient(rpcClient)
// Set contract backend for ethereum service if local node
// is serving LES requests.
if ctx.GlobalInt(utils.LegacyLightServFlag.Name) > 0 || ctx.GlobalInt(utils.LightServeFlag.Name) > 0 {
var ethService *eth.Ethereum
if err := stack.Service(&ethService); err != nil {
utils.Fatalf("Failed to retrieve ethereum service: %v", err)
}
ethService.SetContractBackend(ethClient)
}
go func() {
// Open any wallets already attached
for _, wallet := range stack.AccountManager().Wallets() {
@ -439,7 +429,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
if timestamp := time.Unix(int64(done.Latest.Time), 0); time.Since(timestamp) < 10*time.Minute {
log.Info("Synchronisation completed", "latestnum", done.Latest.Number, "latesthash", done.Latest.Hash(),
"age", common.PrettyAge(timestamp))
stack.Stop()
stack.Close()
}
}
}()
@ -447,24 +437,28 @@ func startNode(ctx *cli.Context, stack *node.Node) {
// Start auxiliary services if enabled
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
var ethereum *eth.Ethereum
if err := stack.Service(&ethereum); err != nil {
// Mining only makes sense if a full Ethereum node is running
if ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
utils.Fatalf("Light clients do not support mining")
}
ethBackend, ok := backend.(*eth.EthAPIBackend)
if !ok {
utils.Fatalf("Ethereum service not running: %v", err)
}
// Set the gas price to the limits from the CLI and start mining
gasprice := utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
if ctx.GlobalIsSet(utils.LegacyMinerGasPriceFlag.Name) && !ctx.GlobalIsSet(utils.MinerGasPriceFlag.Name) {
gasprice = utils.GlobalBig(ctx, utils.LegacyMinerGasPriceFlag.Name)
}
ethereum.TxPool().SetGasPrice(gasprice)
ethBackend.TxPool().SetGasPrice(gasprice)
// start mining
threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name)
if ctx.GlobalIsSet(utils.LegacyMinerThreadsFlag.Name) && !ctx.GlobalIsSet(utils.MinerThreadsFlag.Name) {
threads = ctx.GlobalInt(utils.LegacyMinerThreadsFlag.Name)
log.Warn("The flag --minerthreads is deprecated and will be removed in the future, please use --miner.threads")
}
if err := ethereum.StartMining(threads); err != nil {
if err := ethBackend.StartMining(threads); err != nil {
utils.Fatalf("Failed to start mining: %v", err)
}
}

View File

@ -148,8 +148,6 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.WSApiFlag,
utils.WSAllowedOriginsFlag,
utils.GraphQLEnabledFlag,
utils.GraphQLListenAddrFlag,
utils.GraphQLPortFlag,
utils.GraphQLCORSDomainFlag,
utils.GraphQLVirtualHostsFlag,
utils.RPCGlobalGasCap,
@ -235,6 +233,8 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.LegacyWSApiFlag,
utils.LegacyGpoBlocksFlag,
utils.LegacyGpoPercentileFlag,
utils.LegacyGraphQLListenAddrFlag,
utils.LegacyGraphQLPortFlag,
}, debug.DeprecatedFlags...),
},
{

View File

@ -289,7 +289,7 @@ func createNode(ctx *cli.Context) error {
config.PrivateKey = privKey
}
if services := ctx.String("services"); services != "" {
config.Services = strings.Split(services, ",")
config.Lifecycles = strings.Split(services, ",")
}
node, err := client.CreateNode(config)
if err != nil {

View File

@ -75,7 +75,7 @@ func StartNode(stack *node.Node) {
defer signal.Stop(sigc)
<-sigc
log.Info("Got interrupt, shutting down...")
go stack.Stop()
go stack.Close()
for i := 10; i > 0; i-- {
<-sigc
if i > 1 {

View File

@ -19,7 +19,6 @@ package utils
import (
"crypto/ecdsa"
"errors"
"fmt"
"io"
"io/ioutil"
@ -561,6 +560,20 @@ var (
Usage: "API's offered over the HTTP-RPC interface",
Value: "",
}
GraphQLEnabledFlag = cli.BoolFlag{
Name: "graphql",
Usage: "Enable GraphQL on the HTTP-RPC server. Note that GraphQL can only be started if an HTTP server is started as well.",
}
GraphQLCORSDomainFlag = cli.StringFlag{
Name: "graphql.corsdomain",
Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)",
Value: "",
}
GraphQLVirtualHostsFlag = cli.StringFlag{
Name: "graphql.vhosts",
Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.",
Value: strings.Join(node.DefaultConfig.GraphQLVirtualHosts, ","),
}
WSEnabledFlag = cli.BoolFlag{
Name: "ws",
Usage: "Enable the WS-RPC server",
@ -585,30 +598,6 @@ var (
Usage: "Origins from which to accept websockets requests",
Value: "",
}
GraphQLEnabledFlag = cli.BoolFlag{
Name: "graphql",
Usage: "Enable the GraphQL server",
}
GraphQLListenAddrFlag = cli.StringFlag{
Name: "graphql.addr",
Usage: "GraphQL server listening interface",
Value: node.DefaultGraphQLHost,
}
GraphQLPortFlag = cli.IntFlag{
Name: "graphql.port",
Usage: "GraphQL server listening port",
Value: node.DefaultGraphQLPort,
}
GraphQLCORSDomainFlag = cli.StringFlag{
Name: "graphql.corsdomain",
Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)",
Value: "",
}
GraphQLVirtualHostsFlag = cli.StringFlag{
Name: "graphql.vhosts",
Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.",
Value: strings.Join(node.DefaultConfig.GraphQLVirtualHosts, ","),
}
ExecFlag = cli.StringFlag{
Name: "exec",
Usage: "Execute JavaScript statement",
@ -984,13 +973,6 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
// setGraphQL creates the GraphQL listener interface string from the set
// command line flags, returning empty if the GraphQL endpoint is disabled.
func setGraphQL(ctx *cli.Context, cfg *node.Config) {
if ctx.GlobalBool(GraphQLEnabledFlag.Name) && cfg.GraphQLHost == "" {
cfg.GraphQLHost = localhost
if ctx.GlobalIsSet(GraphQLListenAddrFlag.Name) {
cfg.GraphQLHost = ctx.GlobalString(GraphQLListenAddrFlag.Name)
}
}
cfg.GraphQLPort = ctx.GlobalInt(GraphQLPortFlag.Name)
if ctx.GlobalIsSet(GraphQLCORSDomainFlag.Name) {
cfg.GraphQLCors = splitAndTrim(ctx.GlobalString(GraphQLCORSDomainFlag.Name))
}
@ -1742,12 +1724,24 @@ func setDNSDiscoveryDefaults(cfg *eth.Config, genesis common.Hash) {
}
// RegisterEthService adds an Ethereum client to the stack.
func RegisterEthService(stack *node.Node, cfg *eth.Config) *eth.Ethereum {
fullNode := new(eth.Ethereum)
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
fullNodeInst, err := eth.New(ctx, cfg)
*fullNode = *fullNodeInst //nolint:govet
return fullNode, err
func RegisterEthService(stack *node.Node, cfg *eth.Config) ethapi.Backend {
backend, err := les.New(stack, cfg)
if err != nil {
Fatalf("Failed to register the Ethereum service: %v", err)
}
return backend.ApiBackend
} else {
backend, err := eth.New(stack, cfg)
if err != nil {
Fatalf("Failed to register the Ethereum service: %v", err)
}
if cfg.LightServ > 0 {
_, err := les.NewLesServer(stack, backend, cfg)
if err != nil {
Fatalf("Failed to create the LES server: %v", err)
}
}
return backend.APIBackend
}); err != nil {
Fatalf("Failed to register the Ethereum service: %v", err)
}
@ -1756,31 +1750,15 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) *eth.Ethereum {
// RegisterEthStatsService configures the Ethereum Stats daemon and adds it to
// the given node.
func RegisterEthStatsService(stack *node.Node, url string) {
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
// Retrieve both eth and les services
var ethServ *eth.Ethereum
if err := ctx.Service(&ethServ); err != nil {
return nil, err
}
return ethstats.New(url, ethServ)
}); err != nil {
func RegisterEthStatsService(stack *node.Node, backend ethapi.Backend, url string) {
if err := ethstats.New(stack, backend, backend.Engine(), url); err != nil {
Fatalf("Failed to register the Ethereum Stats service: %v", err)
}
}
// RegisterGraphQLService is a utility function to construct a new service and register it against a node.
func RegisterGraphQLService(stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) {
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
// Try to construct the GraphQL service backed by a full node
var ethServ *eth.Ethereum
if err := ctx.Service(&ethServ); err == nil {
return graphql.New(ethServ.APIBackend, endpoint, cors, vhosts, timeouts)
}
// Well, this should not have happened, bail out
return nil, errors.New("no Ethereum service")
}); err != nil {
func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, cfg node.Config) {
if err := graphql.New(stack, backend, cfg.GraphQLCors, cfg.GraphQLVirtualHosts); err != nil {
Fatalf("Failed to register the GraphQL service: %v", err)
}
}

View File

@ -90,6 +90,8 @@ var (
Name: "testnet",
Usage: "Pre-configured test network (Deprecated: Please choose one of --goerli, --rinkeby, or --ropsten.)",
}
// (Deprecated May 2020, shown in aliased flags section)
LegacyRPCEnabledFlag = cli.BoolFlag{
Name: "rpc",
Usage: "Enable the HTTP-RPC server (deprecated, use --http)",
@ -159,6 +161,17 @@ var (
Usage: "Comma separated enode URLs for P2P v5 discovery bootstrap (light server, light nodes) (deprecated, use --bootnodes)",
Value: "",
}
// (Deprecated July 2020, shown in aliased flags section)
LegacyGraphQLListenAddrFlag = cli.StringFlag{
Name: "graphql.addr",
Usage: "GraphQL server listening interface (deprecated, graphql can only be enabled on the HTTP-RPC server endpoint, use --graphql)",
}
LegacyGraphQLPortFlag = cli.IntFlag{
Name: "graphql.port",
Usage: "GraphQL server listening port (deprecated, graphql can only be enabled on the HTTP-RPC server endpoint, use --graphql)",
Value: node.DefaultHTTPPort,
}
)
// showDeprecated displays deprecated flags that will be soon removed from the codebase.

View File

@ -114,7 +114,8 @@ func newTester(t *testing.T, confOverride func(*eth.Config)) *tester {
if confOverride != nil {
confOverride(ethConf)
}
if err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { return eth.New(ctx, ethConf) }); err != nil {
ethBackend, err := eth.New(stack, ethConf)
if err != nil {
t.Fatalf("failed to register Ethereum protocol: %v", err)
}
// Start the node and assemble the JavaScript console around it
@ -140,13 +141,10 @@ func newTester(t *testing.T, confOverride func(*eth.Config)) *tester {
t.Fatalf("failed to create JavaScript console: %v", err)
}
// Create the final tester and return
var ethereum *eth.Ethereum
stack.Service(&ethereum)
return &tester{
workspace: workspace,
stack: stack,
ethereum: ethereum,
ethereum: ethBackend,
console: console,
input: prompter,
output: printer,

View File

@ -24,6 +24,7 @@ import (
"github.com/ledgerwatch/turbo-geth/accounts"
"github.com/ledgerwatch/turbo-geth/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ledgerwatch/turbo-geth/core"
"github.com/ledgerwatch/turbo-geth/core/bloombits"
"github.com/ledgerwatch/turbo-geth/core/rawdb"
@ -318,6 +319,10 @@ func (b *EthAPIBackend) Stats() (pending int, queued int) {
func (b *EthAPIBackend) TxPoolContent() (map[common.Address]types.Transactions, map[common.Address]types.Transactions) {
return b.eth.TxPool().Content()
func (b *EthAPIBackend) TxPool() *core.TxPool {
}
return b.eth.TxPool()
}
func (b *EthAPIBackend) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription {
@ -370,3 +375,19 @@ func (b *EthAPIBackend) ServiceFilter(ctx context.Context, session *bloombits.Ma
go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests)
}
}
func (b *EthAPIBackend) Engine() consensus.Engine {
return b.eth.engine
}
func (b *EthAPIBackend) CurrentHeader() *types.Header {
return b.eth.blockchain.CurrentHeader()
}
func (b *EthAPIBackend) Miner() *miner.Miner {
return b.eth.Miner()
}
func (b *EthAPIBackend) StartMining(threads int) error {
return b.eth.StartMining(threads)
}

View File

@ -58,15 +58,6 @@ import (
"github.com/ledgerwatch/turbo-geth/rpc"
)
type LesServer interface {
Start(srvr *p2p.Server)
Stop()
APIs() []rpc.API
Protocols() []p2p.Protocol
SetBloomBitsIndexer(bbIndexer *core.ChainIndexer)
SetContractBackend(bind.ContractBackend)
}
// Ethereum implements the Ethereum full node service.
type Ethereum struct {
config *Config
@ -75,7 +66,6 @@ type Ethereum struct {
txPool *core.TxPool
blockchain *core.BlockChain
protocolManager *ProtocolManager
lesServer LesServer
dialCandidates enode.Iterator
// DB interfaces
@ -99,26 +89,16 @@ type Ethereum struct {
networkID uint64
netRPCService *ethapi.PublicNetAPI
lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)
p2pServer *p2p.Server
txPoolStarted bool
}
func (s *Ethereum) AddLesServer(ls LesServer) {
s.lesServer = ls
ls.SetBloomBitsIndexer(s.bloomIndexer)
}
// SetClient sets a rpc client which connecting to our local node.
func (s *Ethereum) SetContractBackend(backend bind.ContractBackend) {
// Pass the rpc client to les server if it is enabled.
if s.lesServer != nil {
s.lesServer.SetContractBackend(backend)
}
lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)
}
// New creates a new Ethereum object (including the
// initialisation of the common Ethereum object)
func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
func New(stack *node.Node, config *Config) (*Ethereum, error) {
// Ensure configuration values are compatible and sane
if config.SyncMode == downloader.LightSync {
return nil, errors.New("can't run eth.Ethereum in light sync mode, use les.LightEthereum")
@ -150,7 +130,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
}
chainDb = ethdb.MustOpen("simulator")
} else {
if chainDb, err = ctx.OpenDatabaseWithFreezer("chaindata", config.DatabaseFreezer); err != nil {
chainDb, err := stack.OpenDatabaseWithFreezer("chaindata", config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, "eth/db/chaindata/")
return nil, err
}
}
@ -171,15 +151,16 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
config: config,
chainDb: chainDb,
chainKV: chainDb.KV(),
eventMux: ctx.EventMux,
accountManager: ctx.AccountManager,
engine: CreateConsensusEngine(ctx, chainConfig, &config.Ethash, config.Miner.Notify, config.Miner.Noverify, chainDb),
eventMux: stack.EventMux(),
accountManager: stack.AccountManager(),
engine: CreateConsensusEngine(stack, chainConfig, &config.Ethash, config.Miner.Notify, config.Miner.Noverify, chainDb),
closeBloomHandler: make(chan struct{}),
networkID: config.NetworkID,
gasPrice: config.Miner.GasPrice,
etherbase: config.Miner.Etherbase,
bloomRequests: make(chan chan *bloombits.Retrieval),
bloomIndexer: NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms),
p2pServer: stack.Server(),
}
log.Info("Initialising Ethereum protocol", "versions", ProtocolVersions, "network", config.NetworkID)
@ -239,7 +220,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
}
if config.TxPool.Journal != "" {
config.TxPool.Journal = ctx.ResolvePath(config.TxPool.Journal)
config.TxPool.Journal = stack.ResolvePath(config.TxPool.Journal)
}
eth.txPool = core.NewTxPool(config.TxPool, chainConfig, chainDb, txCacher)
@ -255,13 +236,19 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
if eth.protocolManager, err = NewProtocolManager(chainConfig, checkpoint, config.SyncMode, config.NetworkID, eth.eventMux, eth.txPool, eth.engine, eth.blockchain, chainDb, config.Whitelist); err != nil {
return nil, err
}
eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock)
eth.protocolManager.SetDataDir(ctx.Config.DataDir)
if config.SyncMode != downloader.StagedSync {
if err = eth.StartTxPool(); err != nil {
return nil, err
}
eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), eth, nil}
gpoParams := config.GPO
if gpoParams.Default == nil {
gpoParams.Default = config.Miner.GasPrice
}
eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams)
if config.SyncMode != downloader.StagedSync {
eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock)
@ -277,11 +264,18 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams)
}
eth.dialCandidates, err = eth.setupDiscovery(&ctx.Config.P2P)
eth.dialCandidates, err = eth.setupDiscovery(&stack.Config().P2P)
if err != nil {
return nil, err
}
// Start the RPC service
eth.netRPCService = ethapi.NewPublicNetAPI(eth.p2pServer, eth.NetVersion())
// Register the backend on the node
stack.RegisterAPIs(eth.APIs())
stack.RegisterProtocols(eth.Protocols())
stack.RegisterLifecycle(eth)
return eth, nil
}
@ -327,7 +321,7 @@ func makeExtraData(extra []byte) []byte {
}
// CreateConsensusEngine creates the required type of consensus engine instance for an Ethereum service
func CreateConsensusEngine(ctx *node.ServiceContext, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database) consensus.Engine {
func CreateConsensusEngine(stack *node.Node, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database) consensus.Engine {
// If proof-of-authority is requested, set it up
if chainConfig.Clique != nil {
return clique.New(chainConfig.Clique, db)
@ -345,7 +339,7 @@ func CreateConsensusEngine(ctx *node.ServiceContext, chainConfig *params.ChainCo
return ethash.NewShared()
default:
engine := ethash.New(ethash.Config{
CacheDir: ctx.ResolvePath(config.CacheDir),
CacheDir: stack.ResolvePath(config.CacheDir),
CachesInMem: config.CachesInMem,
CachesOnDisk: config.CachesOnDisk,
CachesLockMmap: config.CachesLockMmap,
@ -367,18 +361,9 @@ func (s *Ethereum) APIs() []rpc.API {
}
apis := ethapi.GetAPIs(s.APIBackend)
// Append any APIs exposed explicitly by the les server
if s.lesServer != nil {
apis = append(apis, s.lesServer.APIs()...)
}
// Append any APIs exposed explicitly by the consensus engine
apis = append(apis, s.engine.APIs(s.BlockChain())...)
// Append any APIs exposed explicitly by the les server
if s.lesServer != nil {
apis = append(apis, s.lesServer.APIs()...)
}
// Append all the local APIs and return
return append(apis, []rpc.API{
{
@ -594,8 +579,9 @@ func (s *Ethereum) NetVersion() uint64 { return s.networkID }
func (s *Ethereum) Downloader() *downloader.Downloader { return s.protocolManager.downloader }
func (s *Ethereum) Synced() bool { return atomic.LoadUint32(&s.protocolManager.acceptTxs) == 1 }
func (s *Ethereum) ArchiveMode() bool { return !s.config.Pruning }
func (s *Ethereum) BloomIndexer() *core.ChainIndexer { return s.bloomIndexer }
// Protocols implements node.Service, returning all the currently configured
// Protocols returns all the currently configured
// network protocols to start.
func (s *Ethereum) Protocols() []p2p.Protocol {
protos := make([]p2p.Protocol, len(ProtocolVersions))
@ -616,10 +602,10 @@ func (s *Ethereum) Protocols() []p2p.Protocol {
return protos
}
// Start implements node.Service, starting all internal goroutines needed by the
// Start implements node.Lifecycle, starting all internal goroutines needed by the
// Ethereum protocol implementation.
func (s *Ethereum) Start(srvr *p2p.Server) error {
s.startEthEntryUpdate(srvr.LocalNode())
func (s *Ethereum) Start() error {
s.startEthEntryUpdate(s.p2pServer.LocalNode())
// Start the bloom bits servicing goroutines
if s.config.SyncMode != downloader.StagedSync {
@ -632,10 +618,10 @@ func (s *Ethereum) Start(srvr *p2p.Server) error {
}
// Figure out a max peers count based on the server limits
maxPeers := srvr.MaxPeers
maxPeers := s.p2pServer.MaxPeers
if s.config.LightServ > 0 {
if s.config.LightPeers >= srvr.MaxPeers {
return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, srvr.MaxPeers)
if s.config.LightPeers >= s.p2pServer.MaxPeers {
return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, s.p2pServer.MaxPeers)
}
maxPeers -= s.config.LightPeers
}
@ -643,11 +629,7 @@ func (s *Ethereum) Start(srvr *p2p.Server) error {
withTxPool := s.config.SyncMode != downloader.StagedSync
// Start the networking layer and the light server if requested
if err := s.protocolManager.Start(maxPeers, withTxPool); err != nil {
return err
}
if s.lesServer != nil {
s.lesServer.Start(srvr)
}
// Stop implements node.Lifecycle, terminating all internal goroutines used by the
return nil
}
@ -686,9 +668,6 @@ func (s *Ethereum) StopTxPool() error {
func (s *Ethereum) Stop() error {
// Stop all the peer-related stuff first.
s.protocolManager.Stop()
if s.lesServer != nil {
s.lesServer.Stop()
}
// Then stop everything else.
s.bloomIndexer.Close()

View File

@ -187,18 +187,19 @@ var (
func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
// Generate test chain.
genesis, blocks := generateTestChain()
// Start Ethereum service.
var ethservice *eth.Ethereum
// Create node
n, err := node.New(&node.Config{})
n.Register(func(ctx *node.ServiceContext) (node.Service, error) {
if err != nil {
t.Fatalf("can't create new node: %v", err)
}
// Create Ethereum Service
config := &eth.Config{Genesis: genesis}
config.Ethash.PowMode = ethash.ModeFake
config.Pruning = false
ethservice, err = eth.New(ctx, config)
return ethservice, err
})
ethservice, err := eth.New(n, config)
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)
}
// Import the test chain.
if err := n.Start(); err != nil {
t.Fatalf("can't start test node: %v", err)
@ -236,7 +237,7 @@ func generateTestChain() (*core.Genesis, []*types.Block) {
func TestHeader(t *testing.T) {
backend, chain := newTestBackend(t)
client, _ := backend.Attach()
defer backend.Stop()
defer backend.Close()
defer client.Close()
tests := map[string]struct {
@ -280,7 +281,7 @@ func TestHeader(t *testing.T) {
func TestBalanceAt(t *testing.T) {
backend, _ := newTestBackend(t)
client, _ := backend.Attach()
defer backend.Stop()
defer backend.Close()
defer client.Close()
tests := map[string]struct {
@ -326,7 +327,7 @@ func TestBalanceAt(t *testing.T) {
func TestTransactionInBlockInterrupted(t *testing.T) {
backend, _ := newTestBackend(t)
client, _ := backend.Attach()
defer backend.Stop()
defer backend.Close()
defer client.Close()
ec := NewClient(client)
@ -344,7 +345,7 @@ func TestTransactionInBlockInterrupted(t *testing.T) {
func TestChainID(t *testing.T) {
backend, _ := newTestBackend(t)
client, _ := backend.Attach()
defer backend.Stop()
defer backend.Close()
defer client.Close()
ec := NewClient(client)

View File

@ -55,21 +55,32 @@ const (
chainHeadChanSize = 10
)
type txPool interface {
// SubscribeNewTxsEvent should return an event subscription of
// NewTxsEvent and send events to the given channel.
SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription
// backend encompasses the bare-minimum functionality needed for ethstats reporting
type backend interface {
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription
CurrentHeader() *types.Header
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
GetTd(ctx context.Context, hash common.Hash) *big.Int
Stats() (pending int, queued int)
Downloader() *downloader.Downloader
}
type blockChain interface {
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
// fullNodeBackend encompasses the functionality necessary for a full node
// reporting to ethstats
type fullNodeBackend interface {
backend
Miner() *miner.Miner
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
CurrentBlock() *types.Block
SuggestPrice(ctx context.Context) (*big.Int, error)
}
// Service implements an Ethereum netstats reporting daemon that pushes local
// chain statistics up to a monitoring server.
type Service struct {
server *p2p.Server // Peer-to-peer server to retrieve networking infos
eth *eth.Ethereum // Full Ethereum service if monitoring a full node
backend backend
engine consensus.Engine // Consensus engine to retrieve variadic block fields
node string // Name of the node to display on the monitoring page
@ -81,47 +92,37 @@ type Service struct {
}
// New returns a monitoring service ready for stats reporting.
func New(url string, ethServ *eth.Ethereum) (*Service, error) {
func New(node *node.Node, backend backend, engine consensus.Engine, url string) error {
// Parse the netstats connection url
re := regexp.MustCompile("([^:@]*)(:([^@]*))?@(.+)")
parts := re.FindStringSubmatch(url)
if len(parts) != 5 {
return nil, fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url)
return fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url)
}
// Assemble and return the stats service
var engine consensus.Engine
if ethServ != nil {
engine = ethServ.Engine()
}
return &Service{
eth: ethServ,
ethstats := &Service{
backend: backend,
engine: engine,
server: node.Server(),
node: parts[1],
pass: parts[3],
host: parts[4],
pongCh: make(chan struct{}),
histCh: make(chan []uint64, 1),
}, nil
}
// Protocols implements node.Service, returning the P2P network protocols used
// by the stats service (nil as it doesn't use the devp2p overlay network).
func (s *Service) Protocols() []p2p.Protocol { return nil }
node.RegisterLifecycle(ethstats)
return nil
}
// APIs implements node.Service, returning the RPC API endpoints provided by the
// stats service (nil as it doesn't provide any user callable APIs).
func (s *Service) APIs() []rpc.API { return nil }
// Start implements node.Service, starting up the monitoring and reporting daemon.
func (s *Service) Start(server *p2p.Server) error {
s.server = server
// Start implements node.Lifecycle, starting up the monitoring and reporting daemon.
func (s *Service) Start() error {
go s.loop()
log.Info("Stats daemon started")
return nil
}
// Stop implements node.Service, terminating the monitoring and reporting daemon.
// Stop implements node.Lifecycle, terminating the monitoring and reporting daemon.
func (s *Service) Stop() error {
log.Info("Stats daemon stopped")
return nil
@ -131,19 +132,12 @@ func (s *Service) Stop() error {
// until termination.
func (s *Service) loop() {
// Subscribe to chain events to execute updates on
var blockchain blockChain
var txpool txPool
if s.eth != nil {
blockchain = s.eth.BlockChain()
txpool = s.eth.TxPool()
}
chainHeadCh := make(chan core.ChainHeadEvent, chainHeadChanSize)
headSub := blockchain.SubscribeChainHeadEvent(chainHeadCh)
headSub := s.backend.SubscribeChainHeadEvent(chainHeadCh)
defer headSub.Unsubscribe()
txEventCh := make(chan core.NewTxsEvent, txChanSize)
txSub := txpool.SubscribeNewTxsEvent(txEventCh)
txSub := s.backend.SubscribeNewTxsEvent(txEventCh)
defer txSub.Unsubscribe()
// Start a goroutine that exhausts the subscriptions to avoid events piling up
@ -549,13 +543,15 @@ func (s *Service) assembleBlockStats(block *types.Block) *blockStats {
txs []txStats
uncles []*types.Header
)
if s.eth != nil {
// Full nodes have all needed information available
// check if backend is a full node
fullBackend, ok := s.backend.(fullNodeBackend)
if ok {
if block == nil {
block = s.eth.BlockChain().CurrentBlock()
block = fullBackend.CurrentBlock()
}
header = block.Header()
td = s.eth.BlockChain().GetTd(header.Hash(), header.Number.Uint64())
td = fullBackend.GetTd(context.Background(), header.Hash())
txs = make([]txStats, len(block.Transactions()))
for i, tx := range block.Transactions() {
@ -563,6 +559,7 @@ func (s *Service) assembleBlockStats(block *types.Block) *blockStats {
}
uncles = block.Uncles()
}
// Assemble and return the block stats
author, _ := s.engine.Author(header)
@ -593,10 +590,7 @@ func (s *Service) reportHistory(conn *websocket.Conn, list []uint64) error {
indexes = append(indexes, list...)
} else {
// No indexes requested, send back the top ones
var head int64
if s.eth != nil {
head = s.eth.BlockChain().CurrentHeader().Number.Int64()
}
head := s.backend.CurrentHeader().Number.Int64()
start := head - historyUpdateRange + 1
if start < 0 {
start = 0
@ -608,10 +602,14 @@ func (s *Service) reportHistory(conn *websocket.Conn, list []uint64) error {
// Gather the batch of blocks to report
history := make([]*blockStats, len(indexes))
for i, number := range indexes {
fullBackend, ok := s.backend.(fullNodeBackend)
// Retrieve the next block if it's known to us
var block *types.Block
if s.eth != nil {
block = s.eth.BlockChain().GetBlockByNumber(number)
if ok {
block, _ = fullBackend.BlockByNumber(context.Background(), rpc.BlockNumber(number)) // TODO ignore error here ?
} else {
if header, _ := s.backend.HeaderByNumber(context.Background(), rpc.BlockNumber(number)); header != nil {
block = types.NewBlockWithHeader(header)
}
// If we do have the block, add to the history and continue
if block != nil {
@ -647,10 +645,7 @@ type pendStats struct {
// it to the stats server.
func (s *Service) reportPending(conn *websocket.Conn) error {
// Retrieve the pending count from the local blockchain
var pending int
if s.eth != nil {
pending, _ = s.eth.TxPool().Stats()
}
pending, _ := s.backend.Stats()
// Assemble the transaction stats and send it to the server
log.Trace("Sending pending transactions to ethstats", "count", pending)
@ -677,7 +672,7 @@ type nodeStats struct {
Uptime int `json:"uptime"`
}
// reportPending retrieves various stats about the node at the networking and
// reportStats retrieves various stats about the node at the networking and
// mining layer and reports it to the stats server.
func (s *Service) reportStats(conn *websocket.Conn) error {
// Gather the syncing and mining infos from the local miner instance
@ -687,14 +682,16 @@ func (s *Service) reportStats(conn *websocket.Conn) error {
syncing bool
gasprice int
)
if s.eth != nil {
mining = s.eth.Miner().Mining()
hashrate = int(s.eth.Miner().HashRate())
// check if backend is a full node
fullBackend, ok := s.backend.(fullNodeBackend)
if ok {
mining = fullBackend.Miner().Mining()
hashrate = int(fullBackend.Miner().HashRate())
sync := s.eth.Downloader().Progress()
syncing = s.eth.BlockChain().CurrentHeader().Number.Uint64() >= sync.HighestBlock
sync := fullBackend.Downloader().Progress()
syncing = fullBackend.CurrentHeader().Number.Uint64() >= sync.HighestBlock
price, _ := s.eth.APIBackend.SuggestPrice(context.Background())
price, _ := fullBackend.SuggestPrice(context.Background())
gasprice = int(price.Uint64())
}
// Assemble the node stats and send it to the server

View File

@ -17,12 +17,118 @@
package graphql
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/node"
"github.com/stretchr/testify/assert"
)
func TestBuildSchema(t *testing.T) {
stack, err := node.New(&node.DefaultConfig)
if err != nil {
t.Fatalf("could not create new node: %v", err)
}
// Make sure the schema can be parsed and matched up to the object model.
if _, err := newHandler(nil); err != nil {
if err := newHandler(stack, nil, []string{}, []string{}); err != nil {
t.Errorf("Could not construct GraphQL handler: %v", err)
}
}
// Tests that a graphQL request is successfully handled when graphql is enabled on the specified endpoint
func TestGraphQLHTTPOnSamePort_GQLRequest_Successful(t *testing.T) {
stack := createNode(t, true)
defer stack.Close()
// start node
if err := stack.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
}
// create http request
body := strings.NewReader("{\"query\": \"{block{number}}\",\"variables\": null}")
gqlReq, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s/graphql", "127.0.0.1:9393"), body)
if err != nil {
t.Error("could not issue new http request ", err)
}
gqlReq.Header.Set("Content-Type", "application/json")
// read from response
resp := doHTTPRequest(t, gqlReq)
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("could not read from response body: %v", err)
}
expected := "{\"data\":{\"block\":{\"number\":\"0x0\"}}}"
assert.Equal(t, expected, string(bodyBytes))
}
// Tests that a graphQL request is not handled successfully when graphql is not enabled on the specified endpoint
func TestGraphQLHTTPOnSamePort_GQLRequest_Unsuccessful(t *testing.T) {
stack := createNode(t, false)
defer stack.Close()
if err := stack.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
}
// create http request
body := strings.NewReader("{\"query\": \"{block{number}}\",\"variables\": null}")
gqlReq, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/graphql", "127.0.0.1:9393"), body)
if err != nil {
t.Error("could not issue new http request ", err)
}
gqlReq.Header.Set("Content-Type", "application/json")
// read from response
resp := doHTTPRequest(t, gqlReq)
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("could not read from response body: %v", err)
}
// make sure the request is not handled successfully
assert.Equal(t, 404, resp.StatusCode)
assert.Equal(t, "404 page not found\n", string(bodyBytes))
}
func createNode(t *testing.T, gqlEnabled bool) *node.Node {
stack, err := node.New(&node.Config{
HTTPHost: "127.0.0.1",
HTTPPort: 9393,
WSHost: "127.0.0.1",
WSPort: 9393,
})
if err != nil {
t.Fatalf("could not create node: %v", err)
}
if !gqlEnabled {
return stack
}
createGQLService(t, stack, "127.0.0.1:9393")
return stack
}
func createGQLService(t *testing.T, stack *node.Node, endpoint string) {
// create backend
ethBackend, err := eth.New(stack, &eth.DefaultConfig)
if err != nil {
t.Fatalf("could not create eth backend: %v", err)
}
// create gql service
err = New(stack, ethBackend.APIBackend, []string{}, []string{})
if err != nil {
t.Fatalf("could not create graphql service: %v", err)
}
}
func doHTTPRequest(t *testing.T, req *http.Request) *http.Response {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatal("could not issue a GET request to the given endpoint", err)
}
return resp
}

View File

@ -19,8 +19,6 @@ package graphql
import (
"fmt"
"net"
"net/http"
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
"github.com/ledgerwatch/turbo-geth/internal/ethapi"
@ -30,86 +28,30 @@ import (
"github.com/ledgerwatch/turbo-geth/rpc"
)
// Service encapsulates a GraphQL service.
type Service struct {
endpoint string // The host:port endpoint for this service.
cors []string // Allowed CORS domains
vhosts []string // Recognised vhosts
timeouts rpc.HTTPTimeouts // Timeout settings for HTTP requests.
backend ethapi.Backend // The backend that queries will operate on.
handler http.Handler // The `http.Handler` used to answer queries.
listener net.Listener // The listening socket.
}
// New constructs a new GraphQL service instance.
func New(backend ethapi.Backend, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) (*Service, error) {
return &Service{
endpoint: endpoint,
cors: cors,
vhosts: vhosts,
timeouts: timeouts,
backend: backend,
}, nil
func New(stack *node.Node, backend ethapi.Backend, cors, vhosts []string) error {
if backend == nil {
panic("missing backend")
}
// Protocols returns the list of protocols exported by this service.
func (s *Service) Protocols() []p2p.Protocol { return nil }
// APIs returns the list of APIs exported by this service.
func (s *Service) APIs() []rpc.API { return nil }
// Start is called after all services have been constructed and the networking
// layer was also initialized to spawn any goroutines required by the service.
func (s *Service) Start(server *p2p.Server) error {
var err error
s.handler, err = newHandler(s.backend)
if err != nil {
return err
}
if s.listener, err = net.Listen("tcp", s.endpoint); err != nil {
return err
}
// create handler stack and wrap the graphql handler
handler := node.NewHTTPHandlerStack(s.handler, s.cors, s.vhosts)
// make sure timeout values are meaningful
node.CheckTimeouts(&s.timeouts)
// create http server
httpSrv := &http.Server{
Handler: handler,
ReadTimeout: s.timeouts.ReadTimeout,
WriteTimeout: s.timeouts.WriteTimeout,
IdleTimeout: s.timeouts.IdleTimeout,
}
go httpSrv.Serve(s.listener)
log.Info("GraphQL endpoint opened", "url", fmt.Sprintf("http://%s", s.endpoint))
return nil
// check if http server with given endpoint exists and enable graphQL on it
return newHandler(stack, backend, cors, vhosts)
}
// newHandler returns a new `http.Handler` that will answer GraphQL queries.
// It additionally exports an interactive query browser on the / endpoint.
func newHandler(backend ethapi.Backend) (http.Handler, error) {
func newHandler(stack *node.Node, backend ethapi.Backend, cors, vhosts []string) error {
q := Resolver{backend}
s, err := graphql.ParseSchema(schema, &q)
if err != nil {
return nil, err
return err
}
h := &relay.Handler{Schema: s}
handler := node.NewHTTPHandlerStack(h, cors, vhosts)
mux := http.NewServeMux()
mux.Handle("/", GraphiQL{})
mux.Handle("/graphql", h)
mux.Handle("/graphql/", h)
return mux, nil
}
stack.RegisterHandler("GraphQL UI", "/graphql/ui", GraphiQL{})
stack.RegisterHandler("GraphQL", "/graphql", handler)
stack.RegisterHandler("GraphQL", "/graphql/", handler)
// Stop terminates all goroutines belonging to the service, blocking until they
// are all terminated.
func (s *Service) Stop() error {
if s.listener != nil {
s.listener.Close()
s.listener = nil
log.Info("GraphQL endpoint closed", "url", fmt.Sprintf("http://%s", s.endpoint))
}
return nil
}

View File

@ -45,14 +45,16 @@ type Backend interface {
ChainDb() ethdb.Database
AccountManager() *accounts.Manager
ExtRPCEnabled() bool
RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs
RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection
RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs
// Blockchain API
SetHead(number uint64)
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error)
CurrentHeader() *types.Header
CurrentBlock() *types.Block
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error)
@ -84,7 +86,7 @@ type Backend interface {
SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription
ChainConfig() *params.ChainConfig
CurrentBlock() *types.Block
Engine() consensus.Engine
}
func GetAPIs(apiBackend Backend) []rpc.API {

View File

@ -60,30 +60,31 @@ func main() {
genesis := makeGenesis(faucets, sealers)
var (
nodes []*node.Node
nodes []*eth.Ethereum
enodes []*enode.Node
)
for _, sealer := range sealers {
// Start the node and wait until it's up
node, err := makeSealer(genesis)
stack, ethBackend, err := makeSealer(genesis)
if err != nil {
panic(err)
}
defer node.Close()
defer stack.Close()
for node.Server().NodeInfo().Ports.Listener == 0 {
for stack.Server().NodeInfo().Ports.Listener == 0 {
time.Sleep(250 * time.Millisecond)
}
// Connect the node to al the previous ones
// Connect the node to all the previous ones
for _, n := range enodes {
node.Server().AddPeer(n)
stack.Server().AddPeer(n)
}
// Start tracking the node and it's enode
nodes = append(nodes, node)
enodes = append(enodes, node.Server().Self())
// Start tracking the node and its enode
nodes = append(nodes, ethBackend)
enodes = append(enodes, stack.Server().Self())
// Inject the signer key and start sealing with it
store := node.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
store := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
signer, err := store.ImportECDSA(sealer, "")
if err != nil {
panic(err)
@ -92,15 +93,11 @@ func main() {
panic(err)
}
}
// Iterate over all the nodes and start signing with them
time.Sleep(3 * time.Second)
// Iterate over all the nodes and start signing on them
time.Sleep(3 * time.Second)
for _, node := range nodes {
var ethereum *eth.Ethereum
if err := node.Service(&ethereum); err != nil {
panic(err)
}
if err := ethereum.StartMining(1); err != nil {
if err := node.StartMining(1); err != nil {
panic(err)
}
}
@ -109,25 +106,22 @@ func main() {
// Start injecting transactions from the faucet like crazy
nonces := make([]uint64, len(faucets))
for {
// Pick a random signer node
index := rand.Intn(len(faucets))
backend := nodes[index%len(nodes)]
// Fetch the accessor for the relevant signer
var ethereum *eth.Ethereum
if err := nodes[index%len(nodes)].Service(&ethereum); err != nil {
panic(err)
}
// Create a self transaction and inject into the pool
tx, err := types.SignTx(types.NewTransaction(nonces[index], crypto.PubkeyToAddress(faucets[index].PublicKey), new(big.Int), 21000, big.NewInt(100000000000), nil), types.HomesteadSigner{}, faucets[index])
if err != nil {
panic(err)
}
if err := ethereum.TxPool().AddLocal(tx); err != nil {
if err := backend.TxPool().AddLocal(tx); err != nil {
panic(err)
}
nonces[index]++
// Wait if we're too saturated
if pend, _ := ethereum.TxPool().Stats(); pend > 2048 {
if pend, _ := backend.TxPool().Stats(); pend > 2048 {
time.Sleep(100 * time.Millisecond)
}
}
@ -170,7 +164,7 @@ func makeGenesis(faucets []*ecdsa.PrivateKey, sealers []*ecdsa.PrivateKey) *core
return genesis
}
func makeSealer(genesis *core.Genesis) (*node.Node, error) {
func makeSealer(genesis *core.Genesis) (*node.Node, *eth.Ethereum, error) {
// Define the basic configurations for the Ethereum node
datadir, _ := ioutil.TempDir("", "")
@ -188,12 +182,12 @@ func makeSealer(genesis *core.Genesis) (*node.Node, error) {
// Start the node and configure a full Ethereum node on it
stack, err := node.New(config)
if err != nil {
return nil, err
return nil, nil, err
}
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return eth.New(ctx, &eth.Config{
// Create and register the backend
ethBackend, err := eth.New(stack, &eth.Config{
Genesis: genesis,
NetworkID: genesis.Config.ChainID.Uint64(),
NetworkId: genesis.Config.ChainID.Uint64(),
SyncMode: downloader.FullSync,
DatabaseCache: 256,
DatabaseHandles: 256,
@ -206,9 +200,10 @@ func makeSealer(genesis *core.Genesis) (*node.Node, error) {
Recommit: time.Second,
},
})
}); err != nil {
return nil, err
if err != nil {
return nil, nil, err
}
// Start the node and return if successful
return stack, stack.Start()
err = stack.Start()
return stack, ethBackend, err
}

View File

@ -65,43 +65,39 @@ func main() {
genesis := makeGenesis(faucets)
var (
nodes []*node.Node
nodes []*eth.Ethereum
enodes []*enode.Node
)
for i := 0; i < n; i++ {
// Start the node and wait until it's up
node, err := makeMiner(genesis)
stack, ethBackend, err := makeMiner(genesis)
if err != nil {
panic(err)
}
defer node.Close()
defer stack.Close()
for node.Server().NodeInfo().Ports.Listener == 0 {
for stack.Server().NodeInfo().Ports.Listener == 0 {
time.Sleep(250 * time.Millisecond)
}
// Connect the node to al the previous ones
// Connect the node to all the previous ones
for _, n := range enodes {
node.Server().AddPeer(n)
stack.Server().AddPeer(n)
}
// Start tracking the node and it's enode
nodes = append(nodes, node)
enodes = append(enodes, node.Server().Self())
// Start tracking the node and its enode
nodes = append(nodes, ethBackend)
enodes = append(enodes, stack.Server().Self())
// Inject the signer key and start sealing with it
store := node.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
store := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
if _, err := store.NewAccount(""); err != nil {
panic(err)
}
}
// Iterate over all the nodes and start signing with them
time.Sleep(3 * time.Second)
// Iterate over all the nodes and start mining
time.Sleep(3 * time.Second)
for _, node := range nodes {
var ethereum *eth.Ethereum
if err := node.Service(&ethereum); err != nil {
panic(err)
}
if err := ethereum.StartMining(1); err != nil {
if err := node.StartMining(1); err != nil {
panic(err)
}
}
@ -110,19 +106,16 @@ func main() {
// Start injecting transactions from the faucets like crazy
nonces := make([]uint64, len(faucets))
for {
// Pick a random mining node
index := rand.Intn(len(faucets))
backend := nodes[index%len(nodes)]
// Fetch the accessor for the relevant signer
var ethereum *eth.Ethereum
if err := nodes[index%len(nodes)].Service(&ethereum); err != nil {
panic(err)
}
// Create a self transaction and inject into the pool
tx, err := types.SignTx(types.NewTransaction(nonces[index], crypto.PubkeyToAddress(faucets[index].PublicKey), new(big.Int), 21000, big.NewInt(100000000000+rand.Int63n(65536)), nil), types.HomesteadSigner{}, faucets[index])
if err != nil {
panic(err)
}
if err := ethereum.TxPool().AddLocal(tx); err != nil {
if err := backend.TxPool().AddLocal(tx); err != nil {
panic(err)
}
nonces[index]++
@ -153,7 +146,7 @@ func makeGenesis(faucets []*ecdsa.PrivateKey) *core.Genesis {
return genesis
}
func makeMiner(genesis *core.Genesis) (*node.Node, error) {
func makeMiner(genesis *core.Genesis) (*node.Node, *eth.Ethereum, error) {
// Define the basic configurations for the Ethereum node
datadir, _ := ioutil.TempDir("", "")
@ -169,13 +162,12 @@ func makeMiner(genesis *core.Genesis) (*node.Node, error) {
NoUSB: true,
UseLightweightKDF: true,
}
// Start the node and configure a full Ethereum node on it
// Create the node and configure a full Ethereum node on it
stack, err := node.New(config)
if err != nil {
return nil, err
return nil, nil, err
}
ethConfig := &eth.Config{
ethBackend, err := eth.New(stack, &eth.Config{
Genesis: genesis,
NetworkID: genesis.Config.ChainID.Uint64(),
SyncMode: downloader.FullSync,
@ -193,10 +185,13 @@ func makeMiner(genesis *core.Genesis) (*node.Node, error) {
BlocksBeforePruning: 100,
BlocksToPrune: 10,
PruningTimeout: time.Second,
})
if err != nil {
return nil, nil, err
}
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return eth.New(ctx, ethConfig)
err = stack.Start()
return stack, ethBackend, err
}); err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("cannot register stress test miner. config %v", ethConfig))
}

View File

@ -28,21 +28,40 @@ import (
"github.com/ledgerwatch/turbo-geth/rpc"
)
// PrivateAdminAPI is the collection of administrative API methods exposed only
// over a secure RPC channel.
type PrivateAdminAPI struct {
node *Node // Node interfaced by this API
// apis returns the collection of built-in RPC APIs.
func (n *Node) apis() []rpc.API {
return []rpc.API{
{
Namespace: "admin",
Version: "1.0",
Service: &privateAdminAPI{n},
}, {
Namespace: "admin",
Version: "1.0",
Service: &publicAdminAPI{n},
Public: true,
}, {
Namespace: "debug",
Version: "1.0",
Service: debug.Handler,
}, {
Namespace: "web3",
Version: "1.0",
Service: &publicWeb3API{n},
Public: true,
},
}
}
// NewPrivateAdminAPI creates a new API definition for the private admin methods
// of the node itself.
func NewPrivateAdminAPI(node *Node) *PrivateAdminAPI {
return &PrivateAdminAPI{node: node}
// privateAdminAPI is the collection of administrative API methods exposed only
// over a secure RPC channel.
type privateAdminAPI struct {
node *Node // Node interfaced by this API
}
// AddPeer requests connecting to a remote node, and also maintaining the new
// connection at all times, even reconnecting if it is lost.
func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
func (api *privateAdminAPI) AddPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -58,7 +77,7 @@ func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
}
// RemovePeer disconnects from a remote node if the connection exists
func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
func (api *privateAdminAPI) RemovePeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -74,7 +93,7 @@ func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
}
// AddTrustedPeer allows a remote node to always connect, even if slots are full
func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
func (api *privateAdminAPI) AddTrustedPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -90,7 +109,7 @@ func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
// RemoveTrustedPeer removes a remote node from the trusted peer set, but it
// does not disconnect it automatically.
func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
func (api *privateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -106,7 +125,7 @@ func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
// PeerEvents creates an RPC subscription which receives peer events from the
// node's p2p.Server
func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
func (api *privateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
@ -143,14 +162,11 @@ func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription,
}
// StartRPC starts the HTTP RPC API server.
func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
func (api *privateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.httpHandler != nil {
return false, fmt.Errorf("HTTP RPC already running on %s", api.node.httpEndpoint)
}
// Determine host and port.
if host == nil {
h := DefaultHTTPHost
if api.node.config.HTTPHost != "" {
@ -162,57 +178,55 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
port = &api.node.config.HTTPPort
}
allowedOrigins := api.node.config.HTTPCors
// Determine config.
config := httpConfig{
CorsAllowedOrigins: api.node.config.HTTPCors,
Vhosts: api.node.config.HTTPVirtualHosts,
Modules: api.node.config.HTTPModules,
}
if cors != nil {
allowedOrigins = nil
config.CorsAllowedOrigins = nil
for _, origin := range strings.Split(*cors, ",") {
allowedOrigins = append(allowedOrigins, strings.TrimSpace(origin))
config.CorsAllowedOrigins = append(config.CorsAllowedOrigins, strings.TrimSpace(origin))
}
}
allowedVHosts := api.node.config.HTTPVirtualHosts
if vhosts != nil {
allowedVHosts = nil
config.Vhosts = nil
for _, vhost := range strings.Split(*host, ",") {
allowedVHosts = append(allowedVHosts, strings.TrimSpace(vhost))
config.Vhosts = append(config.Vhosts, strings.TrimSpace(vhost))
}
}
modules := api.node.httpWhitelist
if apis != nil {
modules = nil
config.Modules = nil
for _, m := range strings.Split(*apis, ",") {
modules = append(modules, strings.TrimSpace(m))
config.Modules = append(config.Modules, strings.TrimSpace(m))
}
}
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedVHosts, api.node.config.HTTPTimeouts, api.node.config.WSOrigins); err != nil {
if err := api.node.http.setListenAddr(*host, *port); err != nil {
return false, err
}
if err := api.node.http.enableRPC(api.node.rpcAPIs, config); err != nil {
return false, err
}
if err := api.node.http.start(); err != nil {
return false, err
}
return true, nil
}
// StopRPC terminates an already running HTTP RPC API endpoint.
func (api *PrivateAdminAPI) StopRPC() (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.httpHandler == nil {
return false, fmt.Errorf("HTTP RPC not running")
}
api.node.stopHTTP()
// StopRPC shuts down the HTTP server.
func (api *privateAdminAPI) StopRPC() (bool, error) {
api.node.http.stop()
return true, nil
}
// StartWS starts the websocket RPC API server.
func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) {
func (api *privateAdminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.wsHandler != nil {
return false, fmt.Errorf("WebSocket RPC already running on %s", api.node.wsEndpoint)
}
// Determine host and port.
if host == nil {
h := DefaultWSHost
if api.node.config.WSHost != "" {
@ -224,43 +238,50 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str
port = &api.node.config.WSPort
}
origins := api.node.config.WSOrigins
if allowedOrigins != nil {
origins = nil
for _, origin := range strings.Split(*allowedOrigins, ",") {
origins = append(origins, strings.TrimSpace(origin))
// Determine config.
config := wsConfig{
Modules: api.node.config.WSModules,
Origins: api.node.config.WSOrigins,
// ExposeAll: api.node.config.WSExposeAll,
}
}
modules := api.node.config.WSModules
if apis != nil {
modules = nil
config.Modules = nil
for _, m := range strings.Split(*apis, ",") {
modules = append(modules, strings.TrimSpace(m))
config.Modules = append(config.Modules, strings.TrimSpace(m))
}
}
if allowedOrigins != nil {
config.Origins = nil
for _, origin := range strings.Split(*allowedOrigins, ",") {
config.Origins = append(config.Origins, strings.TrimSpace(origin))
}
}
if err := api.node.startWS(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, origins, api.node.config.WSExposeAll); err != nil {
// Enable WebSocket on the server.
server := api.node.wsServerForPort(*port)
if err := server.setListenAddr(*host, *port); err != nil {
return false, err
}
if err := server.enableWS(api.node.rpcAPIs, config); err != nil {
return false, err
}
if err := server.start(); err != nil {
return false, err
}
api.node.http.log.Info("WebSocket endpoint opened", "url", api.node.WSEndpoint())
return true, nil
}
// StopWS terminates an already running websocket RPC API endpoint.
func (api *PrivateAdminAPI) StopWS() (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
if api.node.wsHandler == nil {
return false, fmt.Errorf("WebSocket RPC not running")
}
api.node.stopWS()
// StopWS terminates all WebSocket servers.
func (api *privateAdminAPI) StopWS() (bool, error) {
api.node.http.stopWS()
api.node.ws.stop()
return true, nil
}
// PublicAdminAPI is the collection of administrative API methods exposed over
// publicAdminAPI is the collection of administrative API methods exposed over
// both secure and unsecure RPC channels.
type PublicAdminAPI struct {
type publicAdminAPI struct {
node *Node // Node interfaced by this API
}
@ -272,7 +293,7 @@ func NewPublicAdminAPI(node *Node) *PublicAdminAPI {
// Peers retrieves all the information we know about each individual peer at the
// protocol granularity.
func (api *PublicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
func (api *publicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
@ -282,7 +303,7 @@ func (api *PublicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
// NodeInfo retrieves all the information we know about the host node at the
// protocol granularity.
func (api *PublicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
func (api *publicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
@ -291,27 +312,22 @@ func (api *PublicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
}
// Datadir retrieves the current data directory the node is using.
func (api *PublicAdminAPI) Datadir() string {
func (api *publicAdminAPI) Datadir() string {
return api.node.DataDir()
}
// PublicWeb3API offers helper utils
type PublicWeb3API struct {
// publicWeb3API offers helper utils
type publicWeb3API struct {
stack *Node
}
// NewPublicWeb3API creates a new Web3Service instance
func NewPublicWeb3API(stack *Node) *PublicWeb3API {
return &PublicWeb3API{stack}
}
// ClientVersion returns the node name
func (s *PublicWeb3API) ClientVersion() string {
func (s *publicWeb3API) ClientVersion() string {
return s.stack.Server().Name
}
// Sha3 applies the ethereum sha3 implementation on the input.
// It assumes the input is hex encoded.
func (s *PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
func (s *publicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
return crypto.Keccak256(input)
}

350
node/api_test.go Normal file
View File

@ -0,0 +1,350 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"io"
"net"
"net/http"
"net/url"
"strings"
"testing"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
)
// This test uses the admin_startRPC and admin_startWS APIs,
// checking whether the HTTP server is started correctly.
func TestStartRPC(t *testing.T) {
type test struct {
name string
cfg Config
fn func(*testing.T, *Node, *privateAdminAPI)
// Checks. These run after the node is configured and all API calls have been made.
wantReachable bool // whether the HTTP server should be reachable at all
wantHandlers bool // whether RegisterHandler handlers should be accessible
wantRPC bool // whether JSON-RPC/HTTP should be accessible
wantWS bool // whether JSON-RPC/WS should be accessible
}
tests := []test{
{
name: "all off",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "rpc enabled through config",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc enabled through API",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StartRPC(sp("127.0.0.1"), ip(0), nil, nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc start again after failure",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
// Listen on a random port.
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("can't listen:", err)
}
defer listener.Close()
port := listener.Addr().(*net.TCPAddr).Port
// Now try to start RPC on that port. This should fail.
_, err = api.StartRPC(sp("127.0.0.1"), ip(port), nil, nil, nil)
if err == nil {
t.Fatal("StartRPC should have failed on port", port)
}
// Try again after unblocking the port. It should work this time.
listener.Close()
_, err = api.StartRPC(sp("127.0.0.1"), ip(port), nil, nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc stopped through API",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StopRPC()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "rpc stopped twice",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StopRPC()
assert.NoError(t, err)
_, err = api.StopRPC()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "ws enabled through config",
cfg: Config{WSHost: "127.0.0.1"},
wantReachable: true,
wantHandlers: false,
wantRPC: false,
wantWS: true,
},
{
name: "ws enabled through API",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: false,
wantRPC: false,
wantWS: true,
},
{
name: "ws stopped through API",
cfg: Config{WSHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StopWS()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "ws stopped twice",
cfg: Config{WSHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StopWS()
assert.NoError(t, err)
_, err = api.StopWS()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "ws enabled after RPC",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
wsport := n.http.port
_, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: true,
},
{
name: "ws enabled after RPC then stopped",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
wsport := n.http.port
_, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
assert.NoError(t, err)
_, err = api.StopWS()
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc stopped with ws enabled",
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StartRPC(sp("127.0.0.1"), ip(0), nil, nil, nil)
assert.NoError(t, err)
wsport := n.http.port
_, err = api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
assert.NoError(t, err)
_, err = api.StopRPC()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "rpc enabled after ws",
fn: func(t *testing.T, n *Node, api *privateAdminAPI) {
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
assert.NoError(t, err)
wsport := n.http.port
_, err = api.StartRPC(sp("127.0.0.1"), ip(wsport), nil, nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Apply some sane defaults.
config := test.cfg
// config.Logger = testlog.Logger(t, log.LvlDebug)
config.NoUSB = true
config.P2P.NoDiscovery = true
// Create Node.
stack, err := New(&config)
if err != nil {
t.Fatal("can't create node:", err)
}
defer stack.Close()
// Register the test handler.
stack.RegisterHandler("test", "/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
if err := stack.Start(); err != nil {
t.Fatal("can't start node:", err)
}
// Run the API call hook.
if test.fn != nil {
test.fn(t, stack, &privateAdminAPI{stack})
}
// Check if the HTTP endpoints are available.
baseURL := stack.HTTPEndpoint()
reachable := checkReachable(baseURL)
handlersAvailable := checkBodyOK(baseURL + "/test")
rpcAvailable := checkRPC(baseURL)
wsAvailable := checkRPC(strings.Replace(baseURL, "http://", "ws://", 1))
if reachable != test.wantReachable {
t.Errorf("HTTP server is %sreachable, want it %sreachable", not(reachable), not(test.wantReachable))
}
if handlersAvailable != test.wantHandlers {
t.Errorf("RegisterHandler handlers %savailable, want them %savailable", not(handlersAvailable), not(test.wantHandlers))
}
if rpcAvailable != test.wantRPC {
t.Errorf("HTTP RPC %savailable, want it %savailable", not(rpcAvailable), not(test.wantRPC))
}
if wsAvailable != test.wantWS {
t.Errorf("WS RPC %savailable, want it %savailable", not(wsAvailable), not(test.wantWS))
}
})
}
}
// checkReachable checks if the TCP endpoint in rawurl is open.
func checkReachable(rawurl string) bool {
u, err := url.Parse(rawurl)
if err != nil {
panic(err)
}
conn, err := net.Dial("tcp", u.Host)
if err != nil {
return false
}
conn.Close()
return true
}
// checkBodyOK checks whether the given HTTP URL responds with 200 OK and body "OK".
func checkBodyOK(url string) bool {
resp, err := http.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return false
}
buf := make([]byte, 2)
if _, err = io.ReadFull(resp.Body, buf); err != nil {
return false
}
return bytes.Equal(buf, []byte("OK"))
}
// checkRPC checks whether JSON-RPC works against the given URL.
func checkRPC(url string) bool {
c, err := rpc.Dial(url)
if err != nil {
return false
}
defer c.Close()
_, err = c.SupportedModules()
return err == nil
}
// string/int pointer helpers.
func sp(s string) *string { return &s }
func ip(i int) *int { return &i }
func not(ok bool) string {
if ok {
return ""
}
return "not "
}

View File

@ -162,15 +162,6 @@ type Config struct {
// private APIs to untrusted users is a major security risk.
WSExposeAll bool `toml:",omitempty"`
// GraphQLHost is the host interface on which to start the GraphQL server. If this
// field is empty, no GraphQL API endpoint will be started.
GraphQLHost string
// GraphQLPort is the TCP port number on which to start the GraphQL server. The
// default zero value is/ valid and will pick a port number randomly (useful
// for ephemeral nodes).
GraphQLPort int `toml:",omitempty"`
// GraphQLCors is the Cross-Origin Resource Sharing header to send to requesting
// clients. Please be aware that CORS is a browser enforced security, it's fully
// useless for custom HTTP clients.
@ -255,15 +246,6 @@ func (c *Config) HTTPEndpoint() string {
return fmt.Sprintf("%s:%d", c.HTTPHost, c.HTTPPort)
}
// GraphQLEndpoint resolves a GraphQL endpoint based on the configured host interface
// and port parameters.
func (c *Config) GraphQLEndpoint() string {
if c.GraphQLHost == "" {
return ""
}
return fmt.Sprintf("%s:%d", c.GraphQLHost, c.GraphQLPort)
}
// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
func DefaultHTTPEndpoint() string {
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort}
@ -288,7 +270,7 @@ func DefaultWSEndpoint() string {
// ExtRPCEnabled returns the indicator whether node enables the external
// RPC(http, ws or graphql).
func (c *Config) ExtRPCEnabled() bool {
return c.HTTPHost != "" || c.WSHost != "" || c.GraphQLHost != ""
return c.HTTPHost != "" || c.WSHost != ""
}
// NodeName returns the devp2p node identifier.

View File

@ -46,7 +46,6 @@ var DefaultConfig = Config{
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
WSPort: DefaultWSPort,
WSModules: []string{"net", "web3"},
GraphQLPort: DefaultGraphQLPort,
GraphQLVirtualHosts: []string{"localhost"},
P2P: p2p.Config{
ListenAddr: ":30303",

View File

@ -22,6 +22,43 @@ resources to provide RPC APIs. Services can also offer devp2p protocols, which a
up to the devp2p network when the node instance is started.
Node Lifecycle
The Node object has a lifecycle consisting of three basic states, INITIALIZING, RUNNING
and CLOSED.
New()
INITIALIZING Start()
Close() RUNNING
CLOSED Close()
Creating a Node allocates basic resources such as the data directory and returns the node
in its INITIALIZING state. Lifecycle objects, RPC APIs and peer-to-peer networking
protocols can be registered in this state. Basic operations such as opening a key-value
database are permitted while initializing.
Once everything is registered, the node can be started, which moves it into the RUNNING
state. Starting the node starts all registered Lifecycle objects and enables RPC and
peer-to-peer networking. Note that no additional Lifecycles, APIs or p2p protocols can be
registered while the node is running.
Closing the node releases all held resources. The actions performed by Close depend on the
state it was in. When closing a node in INITIALIZING state, resources related to the data
directory are released. If the node was RUNNING, closing it also stops all Lifecycle
objects and shuts down RPC and peer-to-peer networking.
You must always call Close on Node, even if the node was not started.
Resources Managed By Node
All file-system resources used by a node instance are located in a directory called the

View File

@ -48,21 +48,6 @@ func StartHTTPEndpoint(endpoint string, timeouts rpc.HTTPTimeouts, handler http.
return httpSrv, listener.Addr(), err
}
// startWSEndpoint starts a websocket endpoint.
func startWSEndpoint(endpoint string, handler http.Handler) (*http.Server, net.Addr, error) {
// start the HTTP listener
var (
listener net.Listener
err error
)
if listener, err = net.Listen("tcp", endpoint); err != nil {
return nil, nil, err
}
wsSrv := &http.Server{Handler: handler}
go wsSrv.Serve(listener)
return wsSrv, listener.Addr(), err
}
// checkModuleAvailability checks that all names given in modules are actually
// available API services. It assumes that the MetadataApi module ("rpc") is always available;
// the registration of this "rpc" module happens in NewServer() and is thus common to all endpoints.

View File

@ -39,17 +39,6 @@ func convertFileLockError(err error) error {
return err
}
// DuplicateServiceError is returned during Node startup if a registered service
// constructor returns a service of the same type that was already started.
type DuplicateServiceError struct {
Kind reflect.Type
}
// Error generates a textual representation of the duplicate service error.
func (e *DuplicateServiceError) Error() string {
return fmt.Sprintf("duplicate service: %v", e.Kind)
}
// StopError is returned if a Node fails to stop either any of its registered
// services or itself.
type StopError struct {

31
node/lifecycle.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
// Lifecycle encompasses the behavior of services that can be started and stopped
// on the node. Lifecycle management is delegated to the node, but it is the
// responsibility of the service-specific package to configure and register the
// service on the node using the `RegisterLifecycle` method.
type Lifecycle interface {
// Start is called after all services have been constructed and the networking
// layer was also initialized to spawn any goroutines required by the service.
Start() error
// Stop terminates all goroutines belonging to the service, blocking until they
// are all terminated.
Stop() error
}

File diff suppressed because it is too large Load Diff

View File

@ -21,26 +21,20 @@ import (
"log"
"github.com/ledgerwatch/turbo-geth/node"
"github.com/ledgerwatch/turbo-geth/p2p"
"github.com/ledgerwatch/turbo-geth/rpc"
)
// SampleService is a trivial network service that can be attached to a node for
// SampleLifecycle is a trivial network service that can be attached to a node for
// life cycle management.
//
// The following methods are needed to implement a node.Service:
// - Protocols() []p2p.Protocol - devp2p protocols the service can communicate on
// - APIs() []rpc.API - api methods the service wants to expose on rpc channels
// The following methods are needed to implement a node.Lifecycle:
// - Start() error - method invoked when the node is ready to start the service
// - Stop() error - method invoked when the node terminates the service
type SampleService struct{}
type SampleLifecycle struct{}
func (s *SampleService) Protocols() []p2p.Protocol { return nil }
func (s *SampleService) APIs() []rpc.API { return nil }
func (s *SampleService) Start(*p2p.Server) error { fmt.Println("Service starting..."); return nil }
func (s *SampleService) Stop() error { fmt.Println("Service stopping..."); return nil }
func (s *SampleLifecycle) Start() error { fmt.Println("Service starting..."); return nil }
func (s *SampleLifecycle) Stop() error { fmt.Println("Service stopping..."); return nil }
func ExampleService() {
func ExampleLifecycle() {
// Create a network node to run protocols with the default values.
stack, err := node.New(&node.Config{})
if err != nil {
@ -48,29 +42,18 @@ func ExampleService() {
}
defer stack.Close()
// Create and register a simple network service. This is done through the definition
// of a node.ServiceConstructor that will instantiate a node.Service. The reason for
// the factory method approach is to support service restarts without relying on the
// individual implementations' support for such operations.
constructor := func(context *node.ServiceContext) (node.Service, error) {
return new(SampleService), nil
}
if err := stack.Register(constructor); err != nil {
log.Fatalf("Failed to register service: %v", err)
}
// Create and register a simple network Lifecycle.
service := new(SampleLifecycle)
stack.RegisterLifecycle(service)
// Boot up the entire protocol stack, do a restart and terminate
if err := stack.Start(); err != nil {
log.Fatalf("Failed to start the protocol stack: %v", err)
}
if err := stack.Restart(); err != nil {
log.Fatalf("Failed to restart the protocol stack: %v", err)
}
if err := stack.Stop(); err != nil {
if err := stack.Close(); err != nil {
log.Fatalf("Failed to stop the protocol stack: %v", err)
}
// Output:
// Service starting...
// Service stopping...
// Service starting...
// Service stopping...
}

View File

@ -18,12 +18,15 @@ package node
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/ledgerwatch/turbo-geth/crypto"
"github.com/ledgerwatch/turbo-geth/p2p"
@ -43,20 +46,28 @@ func testNodeConfig() *Config {
}
}
// Tests that an empty protocol stack can be started, restarted and stopped.
func TestNodeLifeCycle(t *testing.T) {
// Tests that an empty protocol stack can be closed more than once.
func TestNodeCloseMultipleTimes(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
stack.Close()
// Ensure that a stopped node can be stopped again
for i := 0; i < 3; i++ {
if err := stack.Stop(); err != ErrNodeStopped {
if err := stack.Close(); err != ErrNodeStopped {
t.Fatalf("iter %d: stop failure mismatch: have %v, want %v", i, err, ErrNodeStopped)
}
}
}
func TestNodeStartMultipleTimes(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
// Ensure that a node can be successfully started, but only once
if err := stack.Start(); err != nil {
t.Fatalf("failed to start node: %v", err)
@ -64,17 +75,11 @@ func TestNodeLifeCycle(t *testing.T) {
if err := stack.Start(); err != ErrNodeRunning {
t.Fatalf("start failure mismatch: have %v, want %v ", err, ErrNodeRunning)
}
// Ensure that a node can be restarted arbitrarily many times
for i := 0; i < 3; i++ {
if err := stack.Restart(); err != nil {
t.Fatalf("iter %d: failed to restart node: %v", i, err)
}
}
// Ensure that a node can be stopped, but only once
if err := stack.Stop(); err != nil {
if err := stack.Close(); err != nil {
t.Fatalf("failed to stop node: %v", err)
}
if err := stack.Stop(); err != ErrNodeStopped {
if err := stack.Close(); err != ErrNodeStopped {
t.Fatalf("stop failure mismatch: have %v, want %v ", err, ErrNodeStopped)
}
}
@ -94,92 +99,152 @@ func TestNodeUsedDataDir(t *testing.T) {
t.Fatalf("failed to create original protocol stack: %v", err)
}
defer original.Close()
if err := original.Start(); err != nil {
t.Fatalf("failed to start original protocol stack: %v", err)
}
defer original.Stop()
// Create a second node based on the same data directory and ensure failure
duplicate, err := New(&Config{DataDir: dir})
if err != nil {
t.Fatalf("failed to create duplicate protocol stack: %v", err)
}
defer duplicate.Close()
if err := duplicate.Start(); err != ErrDatadirUsed {
_, err = New(&Config{DataDir: dir})
if err != ErrDatadirUsed {
t.Fatalf("duplicate datadir failure mismatch: have %v, want %v", err, ErrDatadirUsed)
}
}
// Tests whether services can be registered and duplicates caught.
func TestServiceRegistry(t *testing.T) {
// Tests whether a Lifecycle can be registered.
func TestLifecycleRegistry_Successful(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of unique services and ensure they start successfully
services := []ServiceConstructor{NewNoopServiceA, NewNoopServiceB, NewNoopServiceC}
for i, constructor := range services {
if err := stack.Register(constructor); err != nil {
t.Fatalf("service #%d: registration failed: %v", i, err)
}
}
if err := stack.Start(); err != nil {
t.Fatalf("failed to start original service stack: %v", err)
}
if err := stack.Stop(); err != nil {
t.Fatalf("failed to stop original service stack: %v", err)
}
// Duplicate one of the services and retry starting the node
if err := stack.Register(NewNoopServiceB); err != nil {
t.Fatalf("duplicate registration failed: %v", err)
}
if err := stack.Start(); err == nil {
t.Fatalf("duplicate service started")
} else {
if _, ok := err.(*DuplicateServiceError); !ok {
t.Fatalf("duplicate error mismatch: have %v, want %v", err, DuplicateServiceError{})
}
noop := NewNoop()
stack.RegisterLifecycle(noop)
if !containsLifecycle(stack.lifecycles, noop) {
t.Fatalf("lifecycle was not properly registered on the node, %v", err)
}
}
// Tests that registered services get started and stopped correctly.
func TestServiceLifeCycle(t *testing.T) {
// Tests whether a service's protocols can be registered properly on the node's p2p server.
func TestRegisterProtocols(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of life-cycle instrumented services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
fs, err := NewFullService(stack)
if err != nil {
t.Fatalf("could not create full service: %v", err)
}
for _, protocol := range fs.Protocols() {
if !containsProtocol(stack.server.Protocols, protocol) {
t.Fatalf("protocol %v was not successfully registered", protocol)
}
}
for _, api := range fs.APIs() {
if !containsAPI(stack.rpcAPIs, api) {
t.Fatalf("api %v was not successfully registered", api)
}
}
}
// This test checks that open databases are closed with node.
func TestNodeCloseClosesDB(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
db, err := stack.OpenDatabase("mydb", 0, 0, "")
if err != nil {
t.Fatal("can't open DB:", err)
}
if err = db.Put([]byte{}, []byte{}); err != nil {
t.Fatal("can't Put on open DB:", err)
}
stack.Close()
if err = db.Put([]byte{}, []byte{}); err == nil {
t.Fatal("Put succeeded after node is closed")
}
}
// This test checks that OpenDatabase can be used from within a Lifecycle Start method.
func TestNodeOpenDatabaseFromLifecycleStart(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
var db ethdb.Database
var err error
stack.RegisterLifecycle(&InstrumentedService{
startHook: func() {
db, err = stack.OpenDatabase("mydb", 0, 0, "")
if err != nil {
t.Fatal("can't open DB:", err)
}
},
stopHook: func() {
db.Close()
},
})
stack.Start()
stack.Close()
}
// This test checks that OpenDatabase can be used from within a Lifecycle Stop method.
func TestNodeOpenDatabaseFromLifecycleStop(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
stack.RegisterLifecycle(&InstrumentedService{
stopHook: func() {
db, err := stack.OpenDatabase("mydb", 0, 0, "")
if err != nil {
t.Fatal("can't open DB:", err)
}
db.Close()
},
})
stack.Start()
stack.Close()
}
// Tests that registered Lifecycles get started and stopped correctly.
func TestLifecycleLifeCycle(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
started := make(map[string]bool)
stopped := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
stopHook: func() { stopped[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
// Create a batch of instrumented services
lifecycles := map[string]Lifecycle{
"A": &InstrumentedService{
startHook: func() { started["A"] = true },
stopHook: func() { stopped["A"] = true },
},
"B": &InstrumentedService{
startHook: func() { started["B"] = true },
stopHook: func() { stopped["B"] = true },
},
"C": &InstrumentedService{
startHook: func() { started["C"] = true },
stopHook: func() { stopped["C"] = true },
},
}
// register lifecycles on node
for _, lifecycle := range lifecycles {
stack.RegisterLifecycle(lifecycle)
}
// Start the node and check that all services are running
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
for id := range services {
for id := range lifecycles {
if !started[id] {
t.Fatalf("service %s: freshly started service not running", id)
}
@ -188,160 +253,58 @@ func TestServiceLifeCycle(t *testing.T) {
}
}
// Stop the node and check that all services have been stopped
if err := stack.Stop(); err != nil {
if err := stack.Close(); err != nil {
t.Fatalf("failed to stop protocol stack: %v", err)
}
for id := range services {
for id := range lifecycles {
if !stopped[id] {
t.Fatalf("service %s: freshly terminated service still running", id)
}
}
}
// Tests that services are restarted cleanly as new instances.
func TestServiceRestarts(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Define a service that does not support restarts
var (
running bool
started int
)
constructor := func(*ServiceContext) (Service, error) {
running = false
return &InstrumentedService{
startHook: func(*p2p.Server) {
if running {
panic("already running")
}
running = true
started++
},
}, nil
}
// Register the service and start the protocol stack
if err := stack.Register(constructor); err != nil {
t.Fatalf("failed to register the service: %v", err)
}
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
if !running || started != 1 {
t.Fatalf("running/started mismatch: have %v/%d, want true/1", running, started)
}
// Restart the stack a few times and check successful service restarts
for i := 0; i < 3; i++ {
if err := stack.Restart(); err != nil {
t.Fatalf("iter %d: failed to restart stack: %v", i, err)
}
}
if !running || started != 4 {
t.Fatalf("running/started mismatch: have %v/%d, want true/4", running, started)
}
}
// Tests that if a service fails to initialize itself, none of the other services
// will be allowed to even start.
func TestServiceConstructionAbortion(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Define a batch of good services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
}
started := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Register a service that fails to construct itself
failure := errors.New("fail")
failer := func(*ServiceContext) (Service, error) {
return nil, failure
}
if err := stack.Register(failer); err != nil {
t.Fatalf("failer registration failed: %v", err)
}
// Start the protocol stack and ensure none of the services get started
for i := 0; i < 100; i++ {
if err := stack.Start(); err != failure {
t.Fatalf("iter %d: stack startup failure mismatch: have %v, want %v", i, err, failure)
}
for id := range services {
if started[id] {
t.Fatalf("service %s: started should not have", id)
}
delete(started, id)
}
}
}
// Tests that if a service fails to start, all others started before it will be
// Tests that if a Lifecycle fails to start, all others started before it will be
// shut down.
func TestServiceStartupAbortion(t *testing.T) {
func TestLifecycleStartupError(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of good services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
}
started := make(map[string]bool)
stopped := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
stopHook: func() { stopped[id] = true },
}, nil
// Create a batch of instrumented services
lifecycles := map[string]Lifecycle{
"A": &InstrumentedService{
startHook: func() { started["A"] = true },
stopHook: func() { stopped["A"] = true },
},
"B": &InstrumentedService{
startHook: func() { started["B"] = true },
stopHook: func() { stopped["B"] = true },
},
"C": &InstrumentedService{
startHook: func() { started["C"] = true },
stopHook: func() { stopped["C"] = true },
},
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
// register lifecycles on node
for _, lifecycle := range lifecycles {
stack.RegisterLifecycle(lifecycle)
}
}
// Register a service that fails to start
// Register a service that fails to construct itself
failure := errors.New("fail")
failer := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
start: failure,
}, nil
}
if err := stack.Register(failer); err != nil {
t.Fatalf("failer registration failed: %v", err)
}
failer := &InstrumentedService{start: failure}
stack.RegisterLifecycle(failer)
// Start the protocol stack and ensure all started services stop
for i := 0; i < 100; i++ {
if err := stack.Start(); err != failure {
t.Fatalf("iter %d: stack startup failure mismatch: have %v, want %v", i, err, failure)
t.Fatalf("stack startup failure mismatch: have %v, want %v", err, failure)
}
for id := range services {
for id := range lifecycles {
if started[id] && !stopped[id] {
t.Fatalf("service %s: started but not stopped", id)
}
@ -349,309 +312,227 @@ func TestServiceStartupAbortion(t *testing.T) {
delete(stopped, id)
}
}
}
// Tests that even if a registered service fails to shut down cleanly, it does
// Tests that even if a registered Lifecycle fails to shut down cleanly, it does
// not influence the rest of the shutdown invocations.
func TestServiceTerminationGuarantee(t *testing.T) {
func TestLifecycleTerminationGuarantee(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of good services
services := map[string]InstrumentingWrapper{
"A": InstrumentedServiceMakerA,
"B": InstrumentedServiceMakerB,
"C": InstrumentedServiceMakerC,
}
started := make(map[string]bool)
stopped := make(map[string]bool)
for id, maker := range services {
id := id // Closure for the constructor
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
startHook: func(*p2p.Server) { started[id] = true },
stopHook: func() { stopped[id] = true },
}, nil
}
if err := stack.Register(maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
// Create a batch of instrumented services
lifecycles := map[string]Lifecycle{
"A": &InstrumentedService{
startHook: func() { started["A"] = true },
stopHook: func() { stopped["A"] = true },
},
"B": &InstrumentedService{
startHook: func() { started["B"] = true },
stopHook: func() { stopped["B"] = true },
},
"C": &InstrumentedService{
startHook: func() { started["C"] = true },
stopHook: func() { stopped["C"] = true },
},
}
// register lifecycles on node
for _, lifecycle := range lifecycles {
stack.RegisterLifecycle(lifecycle)
}
// Register a service that fails to shot down cleanly
failure := errors.New("fail")
failer := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
stop: failure,
}, nil
}
if err := stack.Register(failer); err != nil {
t.Fatalf("failer registration failed: %v", err)
}
failer := &InstrumentedService{stop: failure}
stack.RegisterLifecycle(failer)
// Start the protocol stack, and ensure that a failing shut down terminates all
for i := 0; i < 100; i++ {
// Start the stack and make sure all is online
if err := stack.Start(); err != nil {
t.Fatalf("iter %d: failed to start protocol stack: %v", i, err)
t.Fatalf("failed to start protocol stack: %v", err)
}
for id := range services {
for id := range lifecycles {
if !started[id] {
t.Fatalf("iter %d, service %s: service not running", i, id)
t.Fatalf("service %s: service not running", id)
}
if stopped[id] {
t.Fatalf("iter %d, service %s: service already stopped", i, id)
t.Fatalf("service %s: service already stopped", id)
}
}
// Stop the stack, verify failure and check all terminations
err := stack.Stop()
err = stack.Close()
if err, ok := err.(*StopError); !ok {
t.Fatalf("iter %d: termination failure mismatch: have %v, want StopError", i, err)
t.Fatalf("termination failure mismatch: have %v, want StopError", err)
} else {
failer := reflect.TypeOf(&InstrumentedService{})
if err.Services[failer] != failure {
t.Fatalf("iter %d: failer termination failure mismatch: have %v, want %v", i, err.Services[failer], failure)
t.Fatalf("failer termination failure mismatch: have %v, want %v", err.Services[failer], failure)
}
if len(err.Services) != 1 {
t.Fatalf("iter %d: failure count mismatch: have %d, want %d", i, len(err.Services), 1)
t.Fatalf("failure count mismatch: have %d, want %d", len(err.Services), 1)
}
}
for id := range services {
for id := range lifecycles {
if !stopped[id] {
t.Fatalf("iter %d, service %s: service not terminated", i, id)
t.Fatalf("service %s: service not terminated", id)
}
delete(started, id)
delete(stopped, id)
}
}
stack.server = &p2p.Server{}
stack.server.PrivateKey = testNodeKey
}
// TestServiceRetrieval tests that individual services can be retrieved.
func TestServiceRetrieval(t *testing.T) {
// Create a simple stack and register two service types
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Tests whether a handler can be successfully mounted on the canonical HTTP server
// on the given path
func TestRegisterHandler_Successful(t *testing.T) {
node := createNode(t, 7878, 7979)
if err := stack.Register(NewNoopService); err != nil {
t.Fatalf("noop service registration failed: %v", err)
}
if err := stack.Register(NewInstrumentedService); err != nil {
t.Fatalf("instrumented service registration failed: %v", err)
}
// Make sure none of the services can be retrieved until started
var noopServ *NoopService
if err := stack.Service(&noopServ); err != ErrNodeStopped {
t.Fatalf("noop service retrieval mismatch: have %v, want %v", err, ErrNodeStopped)
}
var instServ *InstrumentedService
if err := stack.Service(&instServ); err != ErrNodeStopped {
t.Fatalf("instrumented service retrieval mismatch: have %v, want %v", err, ErrNodeStopped)
}
// Start the stack and ensure everything is retrievable now
if err := stack.Start(); err != nil {
t.Fatalf("failed to start stack: %v", err)
}
defer stack.Stop()
// create and mount handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("success"))
})
node.RegisterHandler("test", "/test", handler)
if err := stack.Service(&noopServ); err != nil {
t.Fatalf("noop service retrieval mismatch: have %v, want %v", err, nil)
}
if err := stack.Service(&instServ); err != nil {
t.Fatalf("instrumented service retrieval mismatch: have %v, want %v", err, nil)
}
// start node
if err := node.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
}
// Tests that all protocols defined by individual services get launched.
func TestProtocolGather(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of services with some configured number of protocols
services := map[string]struct {
Count int
Maker InstrumentingWrapper
}{
"zero": {0, InstrumentedServiceMakerA},
"one": {1, InstrumentedServiceMakerB},
"many": {10, InstrumentedServiceMakerC},
}
for id, config := range services {
protocols := make([]p2p.Protocol, config.Count)
for i := 0; i < len(protocols); i++ {
protocols[i].Name = id
protocols[i].Version = uint(i)
}
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
protocols: protocols,
}, nil
}
if err := stack.Register(config.Maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Start the services and ensure all protocols start successfully
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
protocols := stack.Server().Protocols
if len(protocols) != 11 {
t.Fatalf("mismatching number of protocols launched: have %d, want %d", len(protocols), 26)
}
for id, config := range services {
for ver := 0; ver < config.Count; ver++ {
launched := false
for i := 0; i < len(protocols); i++ {
if protocols[i].Name == id && protocols[i].Version == uint(ver) {
launched = true
break
}
}
if !launched {
t.Errorf("configured protocol not launched: %s v%d", id, ver)
}
}
}
}
// Tests that all APIs defined by individual services get exposed.
func TestAPIGather(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Register a batch of services with some configured APIs
calls := make(chan string, 1)
makeAPI := func(result string) *OneMethodAPI {
return &OneMethodAPI{fun: func() { calls <- result }}
}
services := map[string]struct {
APIs []rpc.API
Maker InstrumentingWrapper
}{
"Zero APIs": {
[]rpc.API{}, InstrumentedServiceMakerA},
"Single API": {
[]rpc.API{
{Namespace: "single", Version: "1", Service: makeAPI("single.v1"), Public: true},
}, InstrumentedServiceMakerB},
"Many APIs": {
[]rpc.API{
{Namespace: "multi", Version: "1", Service: makeAPI("multi.v1"), Public: true},
{Namespace: "multi.v2", Version: "2", Service: makeAPI("multi.v2"), Public: true},
{Namespace: "multi.v2.nested", Version: "2", Service: makeAPI("multi.v2.nested"), Public: true},
}, InstrumentedServiceMakerC},
}
for id, config := range services {
config := config
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{apis: config.APIs}, nil
}
if err := stack.Register(config.Maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Start the services and ensure all API start successfully
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
// Connect to the RPC server and verify the various registered endpoints
client, err := stack.Attach()
if err != nil {
t.Fatalf("failed to connect to the inproc API server: %v", err)
}
defer client.Close()
tests := []struct {
Method string
Result string
}{
{"single_theOneMethod", "single.v1"},
{"multi_theOneMethod", "multi.v1"},
{"multi.v2_theOneMethod", "multi.v2"},
{"multi.v2.nested_theOneMethod", "multi.v2.nested"},
}
for i, test := range tests {
if err := client.Call(nil, test.Method); err != nil {
t.Errorf("test %d: API request failed: %v", i, err)
}
select {
case result := <-calls:
if result != test.Result {
t.Errorf("test %d: result mismatch: have %s, want %s", i, result, test.Result)
}
case <-time.After(time.Second):
t.Fatalf("test %d: rpc execution timeout", i)
}
}
}
func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) {
node := startHTTP(t)
defer node.stopHTTP()
wsReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7453", nil)
// create HTTP request
httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7878/test", nil)
if err != nil {
t.Error("could not issue new http request ", err)
}
wsReq.Header.Set("Connection", "upgrade")
wsReq.Header.Set("Upgrade", "websocket")
wsReq.Header.Set("Sec-WebSocket-Version", "13")
wsReq.Header.Set("Sec-Websocket-Key", "SGVsbG8sIHdvcmxkIQ==")
resp := doHTTPRequest(t, wsReq)
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
}
func TestWebsocketHTTPOnSamePort_HTTPRequest(t *testing.T) {
node := startHTTP(t)
defer node.stopHTTP()
httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7453", nil)
if err != nil {
t.Error("could not issue new http request ", err)
}
httpReq.Header.Set("Accept-Encoding", "gzip")
// check response
resp := doHTTPRequest(t, httpReq)
assert.Equal(t, "gzip", resp.Header.Get("Content-Encoding"))
buf := make([]byte, 7)
_, err = io.ReadFull(resp.Body, buf)
if err != nil {
t.Fatalf("could not read response: %v", err)
}
assert.Equal(t, "success", string(buf))
}
func startHTTP(t *testing.T) *Node {
conf := &Config{HTTPPort: 7453, WSPort: 7453}
// Tests that the given handler will not be successfully mounted since no HTTP server
// is enabled for RPC
func TestRegisterHandler_Unsuccessful(t *testing.T) {
node, err := New(&DefaultConfig)
if err != nil {
t.Fatalf("could not create new node: %v", err)
}
// create and mount handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("success"))
})
node.RegisterHandler("test", "/test", handler)
}
// Tests whether websocket requests can be handled on the same port as a regular http server.
func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) {
node := startHTTP(t, 0, 0)
defer node.Close()
ws := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1)
if node.WSEndpoint() != ws {
t.Fatalf("endpoints should be the same")
}
if !checkRPC(ws) {
t.Fatalf("ws request failed")
}
if !checkRPC(node.HTTPEndpoint()) {
t.Fatalf("http request failed")
}
}
func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) {
// try and get a free port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("can't listen:", err)
}
port := listener.Addr().(*net.TCPAddr).Port
listener.Close()
node := startHTTP(t, 0, port)
defer node.Close()
wsOnHTTP := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1)
ws := fmt.Sprintf("ws://127.0.0.1:%d", port)
if node.WSEndpoint() == wsOnHTTP {
t.Fatalf("endpoints should not be the same")
}
// ensure ws endpoint matches the expected endpoint
if node.WSEndpoint() != ws {
t.Fatalf("ws endpoint is incorrect: expected %s, got %s", ws, node.WSEndpoint())
}
if !checkRPC(ws) {
t.Fatalf("ws request failed")
}
if !checkRPC(node.HTTPEndpoint()) {
t.Fatalf("http request failed")
}
}
func createNode(t *testing.T, httpPort, wsPort int) *Node {
conf := &Config{
HTTPHost: "127.0.0.1",
HTTPPort: httpPort,
WSHost: "127.0.0.1",
WSPort: wsPort,
}
node, err := New(conf)
if err != nil {
t.Error("could not create a new node ", err)
t.Fatalf("could not create a new node: %v", err)
}
return node
}
err = node.startHTTP("127.0.0.1:7453", []rpc.API{}, []string{}, []string{}, []string{}, rpc.HTTPTimeouts{}, []string{})
func startHTTP(t *testing.T, httpPort, wsPort int) *Node {
node := createNode(t, httpPort, wsPort)
err := node.Start()
if err != nil {
t.Error("could not start http service on node ", err)
t.Fatalf("could not start http service on node: %v", err)
}
return node
}
func doHTTPRequest(t *testing.T, req *http.Request) *http.Response {
client := &http.Client{}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
t.Error("could not issue a GET request to the given endpoint", err)
t.Fatalf("could not issue a GET request to the given endpoint: %v", err)
}
return resp
}
func containsProtocol(stackProtocols []p2p.Protocol, protocol p2p.Protocol) bool {
for _, a := range stackProtocols {
if reflect.DeepEqual(a, protocol) {
return true
}
}
return false
}
func containsAPI(stackAPIs []rpc.API, api rpc.API) bool {
for _, a := range stackAPIs {
if reflect.DeepEqual(a, api) {
return true
}
}
return false
}

View File

@ -18,17 +18,304 @@ package node
import (
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"sort"
"strings"
"sync"
"sync/atomic"
"github.com/ledgerwatch/turbo-geth/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/rs/cors"
)
// httpConfig is the JSON-RPC/HTTP configuration.
type httpConfig struct {
Modules []string
CorsAllowedOrigins []string
Vhosts []string
}
// wsConfig is the JSON-RPC/Websocket configuration
type wsConfig struct {
Origins []string
Modules []string
}
type rpcHandler struct {
http.Handler
server *rpc.Server
}
type httpServer struct {
log log.Logger
timeouts rpc.HTTPTimeouts
mux http.ServeMux // registered handlers go here
mu sync.Mutex
server *http.Server
listener net.Listener // non-nil when server is running
// HTTP RPC handler things.
httpConfig httpConfig
httpHandler atomic.Value // *rpcHandler
// WebSocket handler things.
wsConfig wsConfig
wsHandler atomic.Value // *rpcHandler
// These are set by setListenAddr.
endpoint string
host string
port int
handlerNames map[string]string
}
func newHTTPServer(log log.Logger, timeouts rpc.HTTPTimeouts) *httpServer {
h := &httpServer{log: log, timeouts: timeouts, handlerNames: make(map[string]string)}
h.httpHandler.Store((*rpcHandler)(nil))
h.wsHandler.Store((*rpcHandler)(nil))
return h
}
// setListenAddr configures the listening address of the server.
// The address can only be set while the server isn't running.
func (h *httpServer) setListenAddr(host string, port int) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.listener != nil && (host != h.host || port != h.port) {
return fmt.Errorf("HTTP server already running on %s", h.endpoint)
}
h.host, h.port = host, port
h.endpoint = fmt.Sprintf("%s:%d", host, port)
return nil
}
// listenAddr returns the listening address of the server.
func (h *httpServer) listenAddr() string {
h.mu.Lock()
defer h.mu.Unlock()
if h.listener != nil {
return h.listener.Addr().String()
}
return h.endpoint
}
// start starts the HTTP server if it is enabled and not already running.
func (h *httpServer) start() error {
h.mu.Lock()
defer h.mu.Unlock()
if h.endpoint == "" || h.listener != nil {
return nil // already running or not configured
}
// Initialize the server.
h.server = &http.Server{Handler: h}
if h.timeouts != (rpc.HTTPTimeouts{}) {
CheckTimeouts(&h.timeouts)
h.server.ReadTimeout = h.timeouts.ReadTimeout
h.server.WriteTimeout = h.timeouts.WriteTimeout
h.server.IdleTimeout = h.timeouts.IdleTimeout
}
// Start the server.
listener, err := net.Listen("tcp", h.endpoint)
if err != nil {
// If the server fails to start, we need to clear out the RPC and WS
// configuration so they can be configured another time.
h.disableRPC()
h.disableWS()
return err
}
h.listener = listener
go h.server.Serve(listener)
// if server is websocket only, return after logging
if h.wsAllowed() && !h.rpcAllowed() {
h.log.Info("WebSocket enabled", "url", fmt.Sprintf("ws://%v", listener.Addr()))
return nil
}
// Log http endpoint.
h.log.Info("HTTP server started",
"endpoint", listener.Addr(),
"cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
"vhosts", strings.Join(h.httpConfig.Vhosts, ","),
)
// Log all handlers mounted on server.
var paths []string
for path := range h.handlerNames {
paths = append(paths, path)
}
sort.Strings(paths)
logged := make(map[string]bool, len(paths))
for _, path := range paths {
name := h.handlerNames[path]
if !logged[name] {
log.Info(name+" enabled", "url", "http://"+listener.Addr().String()+path)
logged[name] = true
}
}
return nil
}
func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rpc := h.httpHandler.Load().(*rpcHandler)
if r.RequestURI == "/" {
// Serve JSON-RPC on the root path.
ws := h.wsHandler.Load().(*rpcHandler)
if ws != nil && isWebsocket(r) {
ws.ServeHTTP(w, r)
return
}
if rpc != nil {
rpc.ServeHTTP(w, r)
return
}
} else if rpc != nil {
// Requests to a path below root are handled by the mux,
// which has all the handlers registered via Node.RegisterHandler.
// These are made available when RPC is enabled.
h.mux.ServeHTTP(w, r)
return
}
w.WriteHeader(404)
}
// stop shuts down the HTTP server.
func (h *httpServer) stop() {
h.mu.Lock()
defer h.mu.Unlock()
h.doStop()
}
func (h *httpServer) doStop() {
if h.listener == nil {
return // not running
}
// Shut down the server.
httpHandler := h.httpHandler.Load().(*rpcHandler)
wsHandler := h.httpHandler.Load().(*rpcHandler)
if httpHandler != nil {
h.httpHandler.Store((*rpcHandler)(nil))
httpHandler.server.Stop()
}
if wsHandler != nil {
h.wsHandler.Store((*rpcHandler)(nil))
wsHandler.server.Stop()
}
h.server.Shutdown(context.Background())
h.listener.Close()
h.log.Info("HTTP server stopped", "endpoint", h.listener.Addr())
// Clear out everything to allow re-configuring it later.
h.host, h.port, h.endpoint = "", 0, ""
h.server, h.listener = nil, nil
}
// enableRPC turns on JSON-RPC over HTTP on the server.
func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.rpcAllowed() {
return fmt.Errorf("JSON-RPC over HTTP is already enabled")
}
// Create RPC server and handler.
srv := rpc.NewServer()
if err := RegisterApisFromWhitelist(apis, config.Modules, srv, false); err != nil {
return err
}
h.httpConfig = config
h.httpHandler.Store(&rpcHandler{
Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts),
server: srv,
})
return nil
}
// disableRPC stops the HTTP RPC handler. This is internal, the caller must hold h.mu.
func (h *httpServer) disableRPC() bool {
handler := h.httpHandler.Load().(*rpcHandler)
if handler != nil {
h.httpHandler.Store((*rpcHandler)(nil))
handler.server.Stop()
}
return handler != nil
}
// enableWS turns on JSON-RPC over WebSocket on the server.
func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.wsAllowed() {
return fmt.Errorf("JSON-RPC over WebSocket is already enabled")
}
// Create RPC server and handler.
srv := rpc.NewServer()
if err := RegisterApisFromWhitelist(apis, config.Modules, srv, false); err != nil {
return err
}
h.wsConfig = config
h.wsHandler.Store(&rpcHandler{
Handler: srv.WebsocketHandler(config.Origins),
server: srv,
})
return nil
}
// stopWS disables JSON-RPC over WebSocket and also stops the server if it only serves WebSocket.
func (h *httpServer) stopWS() {
h.mu.Lock()
defer h.mu.Unlock()
if h.disableWS() {
if !h.rpcAllowed() {
h.doStop()
}
}
}
// disableWS disables the WebSocket handler. This is internal, the caller must hold h.mu.
func (h *httpServer) disableWS() bool {
ws := h.wsHandler.Load().(*rpcHandler)
if ws != nil {
h.wsHandler.Store((*rpcHandler)(nil))
ws.server.Stop()
}
return ws != nil
}
// rpcAllowed returns true when JSON-RPC over HTTP is enabled.
func (h *httpServer) rpcAllowed() bool {
return h.httpHandler.Load().(*rpcHandler) != nil
}
// wsAllowed returns true when JSON-RPC over WebSocket is enabled.
func (h *httpServer) wsAllowed() bool {
return h.wsHandler.Load().(*rpcHandler) != nil
}
// isWebsocket checks the header of an http request for a websocket upgrade request.
func isWebsocket(r *http.Request) bool {
return strings.ToLower(r.Header.Get("Upgrade")) == "websocket" &&
strings.ToLower(r.Header.Get("Connection")) == "upgrade"
}
// NewHTTPHandlerStack returns wrapped http-related handlers
func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string) http.Handler {
// Wrap the CORS-handler within a host-handler
@ -45,8 +332,8 @@ func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
c := cors.New(cors.Options{
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{http.MethodPost, http.MethodGet},
MaxAge: 600,
AllowedHeaders: []string{"*"},
MaxAge: 600,
})
return c.Handler(srv)
}
@ -138,22 +425,68 @@ func newGzipHandler(next http.Handler) http.Handler {
})
}
// NewWebsocketUpgradeHandler returns a websocket handler that serves an incoming request only if it contains an upgrade
// request to the websocket protocol. If not, serves the the request with the http handler.
func NewWebsocketUpgradeHandler(h http.Handler, ws http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isWebsocket(r) {
ws.ServeHTTP(w, r)
log.Debug("serving websocket request")
return
type ipcServer struct {
log log.Logger
endpoint string
mu sync.Mutex
listener net.Listener
srv *rpc.Server
}
h.ServeHTTP(w, r)
})
func newIPCServer(log log.Logger, endpoint string) *ipcServer {
return &ipcServer{log: log, endpoint: endpoint}
}
// isWebsocket checks the header of an http request for a websocket upgrade request.
func isWebsocket(r *http.Request) bool {
return strings.ToLower(r.Header.Get("Upgrade")) == "websocket" &&
strings.ToLower(r.Header.Get("Connection")) == "upgrade"
// Start starts the httpServer's http.Server
func (is *ipcServer) start(apis []rpc.API) error {
is.mu.Lock()
defer is.mu.Unlock()
if is.listener != nil {
return nil // already running
}
listener, srv, err := rpc.StartIPCEndpoint(is.endpoint, apis)
if err != nil {
return err
}
is.log.Info("IPC endpoint opened", "url", is.endpoint)
is.listener, is.srv = listener, srv
return nil
}
func (is *ipcServer) stop() error {
is.mu.Lock()
defer is.mu.Unlock()
if is.listener == nil {
return nil // not running
}
err := is.listener.Close()
is.srv.Stop()
is.listener, is.srv = nil, nil
is.log.Info("IPC endpoint closed", "url", is.endpoint)
return err
}
// RegisterApisFromWhitelist checks the given modules' availability, generates a whitelist based on the allowed modules,
// and then registers all of the APIs exposed by the services.
func RegisterApisFromWhitelist(apis []rpc.API, modules []string, srv *rpc.Server, exposeAll bool) error {
if bad, available := checkModuleAvailability(modules, apis); len(bad) > 0 {
log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available)
}
// Generate the whitelist based on the allowed modules
whitelist := make(map[string]bool)
for _, module := range modules {
whitelist[module] = true
}
// Register all the APIs exposed by the services
for _, api := range apis {
if exposeAll || whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) {
if err := srv.RegisterName(api.Namespace, api.Service); err != nil {
return err
}
}
}
return nil
}

View File

@ -1,38 +1,107 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/ledgerwatch/turbo-geth/rpc"
"github.com/stretchr/testify/assert"
)
func TestNewWebsocketUpgradeHandler_websocket(t *testing.T) {
srv := rpc.NewServer()
// TestCorsHandler makes sure CORS are properly handled on the http server.
func TestCorsHandler(t *testing.T) {
srv := createAndStartServer(t, httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, wsConfig{})
defer srv.stop()
handler := NewWebsocketUpgradeHandler(nil, srv.WebsocketHandler([]string{}))
ts := httptest.NewServer(handler)
defer ts.Close()
resp := testRequest(t, "origin", "test.com", "", srv)
assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
responses := make(chan *http.Response)
go func(responses chan *http.Response) {
client := &http.Client{}
resp2 := testRequest(t, "origin", "bad", "", srv)
assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin"))
}
req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
req.Header.Set("Connection", "upgrade")
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Sec-WebSocket-Version", "13")
req.Header.Set("Sec-Websocket-Key", "SGVsbG8sIHdvcmxkIQ==")
// TestVhosts makes sure vhosts are properly handled on the http server.
func TestVhosts(t *testing.T) {
srv := createAndStartServer(t, httpConfig{Vhosts: []string{"test"}}, false, wsConfig{})
defer srv.stop()
resp := testRequest(t, "", "", "test", srv)
assert.Equal(t, resp.StatusCode, http.StatusOK)
resp2 := testRequest(t, "", "", "bad", srv)
assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
}
// TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
func TestWebsocketOrigins(t *testing.T) {
srv := createAndStartServer(t, httpConfig{}, true, wsConfig{Origins: []string{"test"}})
defer srv.stop()
dialer := websocket.DefaultDialer
_, _, err := dialer.Dial("ws://"+srv.listenAddr(), http.Header{
"Content-type": []string{"application/json"},
"Sec-WebSocket-Version": []string{"13"},
"Origin": []string{"test"},
})
assert.NoError(t, err)
_, _, err = dialer.Dial("ws://"+srv.listenAddr(), http.Header{
"Content-type": []string{"application/json"},
"Sec-WebSocket-Version": []string{"13"},
"Origin": []string{"bad"},
})
assert.Error(t, err)
}
func createAndStartServer(t *testing.T, conf httpConfig, ws bool, wsConf wsConfig) *httpServer {
t.Helper()
srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), rpc.DefaultHTTPTimeouts)
assert.NoError(t, srv.enableRPC(nil, conf))
if ws {
assert.NoError(t, srv.enableWS(nil, wsConf))
}
assert.NoError(t, srv.setListenAddr("localhost", 0))
assert.NoError(t, srv.start())
return srv
}
func testRequest(t *testing.T, key, value, host string, srv *httpServer) *http.Response {
t.Helper()
body := bytes.NewReader([]byte(`{"jsonrpc":"2.0","id":1,method":"rpc_modules"}`))
req, _ := http.NewRequest("POST", "http://"+srv.listenAddr(), body)
req.Header.Set("content-type", "application/json")
if key != "" && value != "" {
req.Header.Set(key, value)
}
if host != "" {
req.Host = host
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
t.Error("could not issue a GET request to the test http server", err)
t.Fatal(err)
}
responses <- resp
}(responses)
response := <-responses
assert.Equal(t, "websocket", response.Header.Get("Upgrade"))
return resp
}

View File

@ -1,129 +0,0 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"reflect"
"github.com/ledgerwatch/turbo-geth/accounts"
"github.com/ledgerwatch/turbo-geth/ethdb"
"github.com/ledgerwatch/turbo-geth/event"
"github.com/ledgerwatch/turbo-geth/log"
"github.com/ledgerwatch/turbo-geth/p2p"
"github.com/ledgerwatch/turbo-geth/rpc"
)
// ServiceContext is a collection of service independent options inherited from
// the protocol stack, that is passed to all constructors to be optionally used;
// as well as utility methods to operate on the service environment.
type ServiceContext struct {
services map[reflect.Type]Service // Index of the already constructed services
Config Config
EventMux *event.TypeMux // Event multiplexer used for decoupled notifications
AccountManager *accounts.Manager // Account manager created by the node.
}
// OpenDatabaseWithFreezer
// FIXME: implement the functionality
func (ctx *ServiceContext) OpenDatabaseWithFreezer(name string, freezer string) (*ethdb.ObjectDatabase, error) {
return ctx.OpenDatabase(name)
}
// OpenDatabase opens an existing database with the given name (or creates one
// if no previous can be found) from within the node's data directory. If the
// node is an ephemeral one, a memory database is returned.
func (ctx *ServiceContext) OpenDatabase(name string) (*ethdb.ObjectDatabase, error) {
if ctx.Config.DataDir == "" {
return ethdb.NewMemDatabase(), nil
}
if ctx.Config.Bolt {
log.Info("Opening Database (Bolt)")
return ethdb.Open(ctx.Config.ResolvePath(name + "_bolt"))
}
log.Info("Opening Database (LMDB)")
return ethdb.Open(ctx.Config.ResolvePath(name))
/*
if err != nil {
return nil, err
}
root := ctx.config.ResolvePath(name)
FIXME: restore and move to OpenDatabaseWithFreezer
switch {
case freezer == "":
freezer = filepath.Join(root, "ancient")
case !filepath.IsAbs(freezer):
freezer = ctx.config.ResolvePath(freezer)
}
return ethdb.NewBoltDatabase(root)
*/
}
// ResolvePath resolves a user path into the data directory if that was relative
// and if the user actually uses persistent storage. It will return an empty string
// for emphemeral storage and the user's own input for absolute paths.
func (ctx *ServiceContext) ResolvePath(path string) string {
return ctx.Config.ResolvePath(path)
}
// Service retrieves a currently running service registered of a specific type.
func (ctx *ServiceContext) Service(service interface{}) error {
element := reflect.ValueOf(service).Elem()
if running, ok := ctx.services[element.Type()]; ok {
element.Set(reflect.ValueOf(running))
return nil
}
return ErrServiceUnknown
}
// ExtRPCEnabled returns the indicator whether node enables the external
// RPC(http, ws or graphql).
func (ctx *ServiceContext) ExtRPCEnabled() bool {
return ctx.Config.ExtRPCEnabled()
}
// ServiceConstructor is the function signature of the constructors needed to be
// registered for service instantiation.
type ServiceConstructor func(ctx *ServiceContext) (Service, error)
// Service is an individual protocol that can be registered into a node.
//
// Notes:
//
// • Service life-cycle management is delegated to the node. The service is allowed to
// initialize itself upon creation, but no goroutines should be spun up outside of the
// Start method.
//
// • Restart logic is not required as the node will create a fresh instance
// every time a service is started.
type Service interface {
// Protocols retrieves the P2P protocols the service wishes to start.
Protocols() []p2p.Protocol
// APIs retrieves the list of RPC descriptors the service provides
APIs() []rpc.API
// Start is called after all services have been constructed and the networking
// layer was also initialized to spawn any goroutines required by the service.
Start(server *p2p.Server) error
// Stop terminates all goroutines belonging to the service, blocking until they
// are all terminated.
Stop() error
}

View File

@ -1,98 +0,0 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
// Tests that databases are correctly created persistent or ephemeral based on
// the configured service context.
func TestContextDatabases(t *testing.T) {
// Create a temporary folder and ensure no database is contained within
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("failed to create temporary data directory: %v", err)
}
defer os.RemoveAll(dir)
if _, err := os.Stat(filepath.Join(dir, "database")); err == nil {
t.Fatalf("non-created database already exists")
}
// Request the opening/creation of a database and ensure it persists to disk
ctx := &ServiceContext{Config: Config{Name: "unit-test", DataDir: dir}}
db, err := ctx.OpenDatabase("persistent")
if err != nil {
t.Fatalf("failed to open persistent database: %v", err)
}
db.Close()
if _, err := os.Stat(filepath.Join(dir, "unit-test", "persistent")); err != nil {
t.Fatalf("persistent database doesn't exists: %v", err)
}
// Request th opening/creation of an ephemeral database and ensure it's not persisted
ctx = &ServiceContext{Config: Config{DataDir: ""}}
db, err = ctx.OpenDatabase("ephemeral")
if err != nil {
t.Fatalf("failed to open ephemeral database: %v", err)
}
db.Close()
if _, err := os.Stat(filepath.Join(dir, "ephemeral")); err == nil {
t.Fatalf("ephemeral database exists")
}
}
// Tests that already constructed services can be retrieves by later ones.
func TestContextServices(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
// Define a verifier that ensures a NoopA is before it and NoopB after
verifier := func(ctx *ServiceContext) (Service, error) {
var objA *NoopServiceA
if ctx.Service(&objA) != nil {
return nil, fmt.Errorf("former service not found")
}
var objB *NoopServiceB
if err := ctx.Service(&objB); err != ErrServiceUnknown {
return nil, fmt.Errorf("latters lookup error mismatch: have %v, want %v", err, ErrServiceUnknown)
}
return new(NoopService), nil
}
// Register the collection of services
if err := stack.Register(NewNoopServiceA); err != nil {
t.Fatalf("former failed to register service: %v", err)
}
if err := stack.Register(verifier); err != nil {
t.Fatalf("failed to register service verifier: %v", err)
}
if err := stack.Register(NewNoopServiceB); err != nil {
t.Fatalf("latter failed to register service: %v", err)
}
// Start the protocol stack and ensure services are constructed in order
if err := stack.Start(); err != nil {
t.Fatalf("failed to start stack: %v", err)
}
defer stack.Stop()
}

View File

@ -20,61 +20,40 @@
package node
import (
"reflect"
"github.com/ledgerwatch/turbo-geth/p2p"
"github.com/ledgerwatch/turbo-geth/rpc"
)
// NoopService is a trivial implementation of the Service interface.
type NoopService struct{}
// NoopLifecycle is a trivial implementation of the Service interface.
type NoopLifecycle struct{}
func (s *NoopService) Protocols() []p2p.Protocol { return nil }
func (s *NoopService) APIs() []rpc.API { return nil }
func (s *NoopService) Start(*p2p.Server) error { return nil }
func (s *NoopService) Stop() error { return nil }
func (s *NoopLifecycle) Start() error { return nil }
func (s *NoopLifecycle) Stop() error { return nil }
func NewNoopService(*ServiceContext) (Service, error) { return new(NoopService), nil }
func NewNoop() *Noop {
noop := new(Noop)
return noop
}
// Set of services all wrapping the base NoopService resulting in the same method
// Set of services all wrapping the base NoopLifecycle resulting in the same method
// signatures but different outer types.
type NoopServiceA struct{ NoopService }
type NoopServiceB struct{ NoopService }
type NoopServiceC struct{ NoopService }
type Noop struct{ NoopLifecycle }
func NewNoopServiceA(*ServiceContext) (Service, error) { return new(NoopServiceA), nil }
func NewNoopServiceB(*ServiceContext) (Service, error) { return new(NoopServiceB), nil }
func NewNoopServiceC(*ServiceContext) (Service, error) { return new(NoopServiceC), nil }
// InstrumentedService is an implementation of Service for which all interface
// InstrumentedService is an implementation of Lifecycle for which all interface
// methods can be instrumented both return value as well as event hook wise.
type InstrumentedService struct {
protocols []p2p.Protocol
apis []rpc.API
start error
stop error
protocolsHook func()
startHook func(*p2p.Server)
startHook func()
stopHook func()
protocols []p2p.Protocol
}
func NewInstrumentedService(*ServiceContext) (Service, error) { return new(InstrumentedService), nil }
func (s *InstrumentedService) Protocols() []p2p.Protocol {
if s.protocolsHook != nil {
s.protocolsHook()
}
return s.protocols
}
func (s *InstrumentedService) APIs() []rpc.API {
return s.apis
}
func (s *InstrumentedService) Start(server *p2p.Server) error {
func (s *InstrumentedService) Start() error {
if s.startHook != nil {
s.startHook(server)
s.startHook()
}
return s.start
}
@ -86,48 +65,49 @@ func (s *InstrumentedService) Stop() error {
return s.stop
}
// InstrumentingWrapper is a method to specialize a service constructor returning
// a generic InstrumentedService into one returning a wrapping specific one.
type InstrumentingWrapper func(base ServiceConstructor) ServiceConstructor
type FullService struct{}
func InstrumentingWrapperMaker(base ServiceConstructor, kind reflect.Type) ServiceConstructor {
return func(ctx *ServiceContext) (Service, error) {
obj, err := base(ctx)
if err != nil {
return nil, err
func NewFullService(stack *Node) (*FullService, error) {
fs := new(FullService)
stack.RegisterProtocols(fs.Protocols())
stack.RegisterAPIs(fs.APIs())
stack.RegisterLifecycle(fs)
return fs, nil
}
wrapper := reflect.New(kind)
wrapper.Elem().Field(0).Set(reflect.ValueOf(obj).Elem())
return wrapper.Interface().(Service), nil
func (f *FullService) Start() error { return nil }
func (f *FullService) Stop() error { return nil }
func (f *FullService) Protocols() []p2p.Protocol {
return []p2p.Protocol{
p2p.Protocol{
Name: "test1",
Version: uint(1),
},
p2p.Protocol{
Name: "test2",
Version: uint(2),
},
}
}
// Set of services all wrapping the base InstrumentedService resulting in the
// same method signatures but different outer types.
type InstrumentedServiceA struct{ InstrumentedService }
type InstrumentedServiceB struct{ InstrumentedService }
type InstrumentedServiceC struct{ InstrumentedService }
func InstrumentedServiceMakerA(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceA{}))
}
func InstrumentedServiceMakerB(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceB{}))
}
func InstrumentedServiceMakerC(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceC{}))
}
// OneMethodAPI is a single-method API handler to be returned by test services.
type OneMethodAPI struct {
fun func()
}
func (api *OneMethodAPI) TheOneMethod() {
if api.fun != nil {
api.fun()
func (f *FullService) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "admin",
Version: "1.0",
},
{
Namespace: "debug",
Version: "1.0",
Public: true,
},
{
Namespace: "net",
Version: "1.0",
Public: true,
},
}
}

View File

@ -77,11 +77,11 @@ func (e *ExecAdapter) Name() string {
// NewNode returns a new ExecNode using the given config
func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) {
if len(config.Services) == 0 {
return nil, errors.New("node must have at least one service")
if len(config.Lifecycles) == 0 {
return nil, errors.New("node must have at least one service lifecycle")
}
for _, service := range config.Services {
if _, exists := serviceFuncs[service]; !exists {
for _, service := range config.Lifecycles {
if _, exists := lifecycleConstructorFuncs[service]; !exists {
return nil, fmt.Errorf("unknown node service %q", service)
}
}
@ -265,7 +265,7 @@ func (n *ExecNode) waitForStartupJSON(ctx context.Context) (string, chan nodeSta
func (n *ExecNode) execCommand() *exec.Cmd {
return &exec.Cmd{
Path: reexec.Self(),
Args: []string{"p2p-node", strings.Join(n.Config.Node.Services, ","), n.ID.String()},
Args: []string{"p2p-node", strings.Join(n.Config.Node.Lifecycles, ","), n.ID.String()},
}
}
@ -402,7 +402,7 @@ func execP2PNode() {
defer signal.Stop(sigc)
<-sigc
log.Info("Received SIGTERM, shutting down...")
stack.Stop()
stack.Close()
}()
stack.Wait() // Wait for the stack to exit.
}
@ -436,44 +436,36 @@ func startExecNodeStack() (*node.Node, error) {
return nil, fmt.Errorf("error creating node stack: %v", err)
}
// register the services, collecting them into a map so we can wrap
// them in a snapshot service
services := make(map[string]node.Service, len(serviceNames))
// Register the services, collecting them into a map so they can
// be accessed by the snapshot API.
services := make(map[string]node.Lifecycle, len(serviceNames))
for _, name := range serviceNames {
serviceFunc, exists := serviceFuncs[name]
lifecycleFunc, exists := lifecycleConstructorFuncs[name]
if !exists {
return nil, fmt.Errorf("unknown node service %q", err)
}
constructor := func(nodeCtx *node.ServiceContext) (node.Service, error) {
ctx := &ServiceContext{
RPCDialer: &wsRPCDialer{addrs: conf.PeerAddrs},
NodeContext: nodeCtx,
Config: conf.Node,
}
if conf.Snapshots != nil {
ctx.Snapshot = conf.Snapshots[name]
}
service, err := serviceFunc(ctx)
service, err := lifecycleFunc(ctx, stack)
if err != nil {
return nil, err
}
services[name] = service
return service, nil
}
if err := stack.Register(constructor); err != nil {
return stack, fmt.Errorf("error registering service %q: %v", name, err)
}
stack.RegisterLifecycle(service)
}
// register the snapshot service
err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return &snapshotService{services}, nil
})
if err != nil {
return stack, fmt.Errorf("error starting snapshot service: %v", err)
}
// Add the snapshot API.
stack.RegisterAPIs([]rpc.API{{
Namespace: "simulation",
Version: "1.0",
Service: SnapshotAPI{services},
}})
// start the stack
if err = stack.Start(); err != nil {
err = fmt.Errorf("error starting stack: %v", err)
}
@ -492,35 +484,9 @@ type nodeStartupJSON struct {
NodeInfo *p2p.NodeInfo
}
// snapshotService is a node.Service which wraps a list of services and
// exposes an API to generate a snapshot of those services
type snapshotService struct {
services map[string]node.Service
}
func (s *snapshotService) APIs() []rpc.API {
return []rpc.API{{
Namespace: "simulation",
Version: "1.0",
Service: SnapshotAPI{s.services},
}}
}
func (s *snapshotService) Protocols() []p2p.Protocol {
return nil
}
func (s *snapshotService) Start(*p2p.Server) error {
return nil
}
func (s *snapshotService) Stop() error {
return nil
}
// SnapshotAPI provides an RPC method to create snapshots of services
type SnapshotAPI struct {
services map[string]node.Service
services map[string]node.Lifecycle
}
func (api SnapshotAPI) Snapshot() (map[string][]byte, error) {

View File

@ -41,26 +41,18 @@ type SimAdapter struct {
pipe func() (net.Conn, net.Conn, error)
mtx sync.RWMutex
nodes map[enode.ID]*SimNode
services map[string]ServiceFunc
lifecycles LifecycleConstructors
}
// NewSimAdapter creates a SimAdapter which is capable of running in-memory
// simulation nodes running any of the given services (the services to run on a
// particular node are passed to the NewNode function in the NodeConfig)
// the adapter uses a net.Pipe for in-memory simulated network connections
func NewSimAdapter(services map[string]ServiceFunc) *SimAdapter {
func NewSimAdapter(services LifecycleConstructors) *SimAdapter {
return &SimAdapter{
pipe: pipes.NetPipe,
nodes: make(map[enode.ID]*SimNode),
services: services,
}
}
func NewTCPAdapter(services map[string]ServiceFunc) *SimAdapter {
return &SimAdapter{
pipe: pipes.TCPPipe,
nodes: make(map[enode.ID]*SimNode),
services: services,
lifecycles: services,
}
}
@ -86,11 +78,11 @@ func (s *SimAdapter) NewNode(config *NodeConfig) (Node, error) {
}
// check the services are valid
if len(config.Services) == 0 {
if len(config.Lifecycles) == 0 {
return nil, errors.New("node must have at least one service")
}
for _, service := range config.Services {
if _, exists := s.services[service]; !exists {
for _, service := range config.Lifecycles {
if _, exists := s.lifecycles[service]; !exists {
return nil, fmt.Errorf("unknown node service %q", service)
}
}
@ -120,7 +112,7 @@ func (s *SimAdapter) NewNode(config *NodeConfig) (Node, error) {
config: config,
node: n,
adapter: s,
running: make(map[string]node.Service),
running: make(map[string]node.Lifecycle),
}
s.nodes[id] = simNode
return simNode, nil
@ -156,11 +148,7 @@ func (s *SimAdapter) DialRPC(id enode.ID) (*rpc.Client, error) {
if !ok {
return nil, fmt.Errorf("unknown node: %s", id)
}
handler, err := node.node.RPCHandler()
if err != nil {
return nil, err
}
return rpc.DialInProc(handler), nil
return node.node.Attach()
}
// GetNode returns the node with the given ID if it exists
@ -180,7 +168,7 @@ type SimNode struct {
config *NodeConfig
adapter *SimAdapter
node *node.Node
running map[string]node.Service
running map[string]node.Lifecycle
client *rpc.Client
registerOnce sync.Once
}
@ -228,7 +216,7 @@ func (sn *SimNode) ServeRPC(conn *websocket.Conn) error {
// simulation_snapshot RPC method
func (sn *SimNode) Snapshots() (map[string][]byte, error) {
sn.lock.RLock()
services := make(map[string]node.Service, len(sn.running))
services := make(map[string]node.Lifecycle, len(sn.running))
for name, service := range sn.running {
services[name] = service
}
@ -253,35 +241,30 @@ func (sn *SimNode) Snapshots() (map[string][]byte, error) {
// Start registers the services and starts the underlying devp2p node
func (sn *SimNode) Start(snapshots map[string][]byte) error {
newService := func(name string) func(ctx *node.ServiceContext) (node.Service, error) {
return func(nodeCtx *node.ServiceContext) (node.Service, error) {
// ensure we only register the services once in the case of the node
// being stopped and then started again
var regErr error
sn.registerOnce.Do(func() {
for _, name := range sn.config.Lifecycles {
ctx := &ServiceContext{
RPCDialer: sn.adapter,
NodeContext: nodeCtx,
Config: sn.config,
}
if snapshots != nil {
ctx.Snapshot = snapshots[name]
}
serviceFunc := sn.adapter.services[name]
service, err := serviceFunc(ctx)
serviceFunc := sn.adapter.lifecycles[name]
service, err := serviceFunc(ctx, sn.node)
if err != nil {
return nil, err
}
sn.running[name] = service
return service, nil
}
}
// ensure we only register the services once in the case of the node
// being stopped and then started again
var regErr error
sn.registerOnce.Do(func() {
for _, name := range sn.config.Services {
if err := sn.node.Register(newService(name)); err != nil {
regErr = err
break
}
// if the service has already been registered, don't register it again.
if _, ok := sn.running[name]; ok {
continue
}
sn.running[name] = service
sn.node.RegisterLifecycle(service)
}
})
if regErr != nil {
@ -293,13 +276,12 @@ func (sn *SimNode) Start(snapshots map[string][]byte) error {
}
// create an in-process RPC client
handler, err := sn.node.RPCHandler()
client, err := sn.node.Attach()
if err != nil {
return err
}
sn.lock.Lock()
sn.client = rpc.DialInProc(handler)
sn.client = client
sn.lock.Unlock()
return nil
@ -313,21 +295,21 @@ func (sn *SimNode) Stop() error {
sn.client = nil
}
sn.lock.Unlock()
return sn.node.Stop()
return sn.node.Close()
}
// Service returns a running service by name
func (sn *SimNode) Service(name string) node.Service {
func (sn *SimNode) Service(name string) node.Lifecycle {
sn.lock.RLock()
defer sn.lock.RUnlock()
return sn.running[name]
}
// Services returns a copy of the underlying services
func (sn *SimNode) Services() []node.Service {
func (sn *SimNode) Services() []node.Lifecycle {
sn.lock.RLock()
defer sn.lock.RUnlock()
services := make([]node.Service, 0, len(sn.running))
services := make([]node.Lifecycle, 0, len(sn.running))
for _, service := range sn.running {
services = append(services, service)
}
@ -335,10 +317,10 @@ func (sn *SimNode) Services() []node.Service {
}
// ServiceMap returns a map by names of the underlying services
func (sn *SimNode) ServiceMap() map[string]node.Service {
func (sn *SimNode) ServiceMap() map[string]node.Lifecycle {
sn.lock.RLock()
defer sn.lock.RUnlock()
services := make(map[string]node.Service, len(sn.running))
services := make(map[string]node.Lifecycle, len(sn.running))
for name, service := range sn.running {
services[name] = service
}

View File

@ -98,11 +98,11 @@ type NodeConfig struct {
// Use an existing database instead of a temporary one if non-empty
DataDir string
// Services are the names of the services which should be run when
// starting the node (for SimNodes it should be the names of services
// contained in SimAdapter.services, for other nodes it should be
// services registered by calling the RegisterService function)
Services []string
// Lifecycles are the names of the service lifecycles which should be run when
// starting the node (for SimNodes it should be the names of service lifecycles
// contained in SimAdapter.lifecycles, for other nodes it should be
// service lifecycles registered by calling the RegisterLifecycle function)
Lifecycles []string
// Properties are the names of the properties this node should hold
// within running services (e.g. "bootnode", "lightnode" or any custom values)
@ -139,7 +139,7 @@ func (n *NodeConfig) MarshalJSON() ([]byte, error) {
confJSON := nodeConfigJSON{
ID: n.ID.String(),
Name: n.Name,
Services: n.Services,
Services: n.Lifecycles,
Properties: n.Properties,
Port: n.Port,
EnableMsgEvents: n.EnableMsgEvents,
@ -177,7 +177,7 @@ func (n *NodeConfig) UnmarshalJSON(data []byte) error {
}
n.Name = confJSON.Name
n.Services = confJSON.Services
n.Lifecycles = confJSON.Services
n.Properties = confJSON.Properties
n.Port = confJSON.Port
n.EnableMsgEvents = confJSON.EnableMsgEvents
@ -235,7 +235,6 @@ func assignTCPPort() (uint16, error) {
type ServiceContext struct {
RPCDialer
NodeContext *node.ServiceContext
Config *NodeConfig
Snapshot []byte
}
@ -247,27 +246,29 @@ type RPCDialer interface {
DialRPC(id enode.ID) (*rpc.Client, error)
}
// Services is a collection of services which can be run in a simulation
type Services map[string]ServiceFunc
// LifecycleConstructor allows a Lifecycle to be constructed during node start-up.
// While the service-specific package usually takes care of Lifecycle creation and registration,
// for testing purposes, it is useful to be able to construct a Lifecycle on spot.
type LifecycleConstructor func(ctx *ServiceContext, stack *node.Node) (node.Lifecycle, error)
// ServiceFunc returns a node.Service which can be used to boot a devp2p node
type ServiceFunc func(ctx *ServiceContext) (node.Service, error)
// LifecycleConstructors stores LifecycleConstructor functions to call during node start-up.
type LifecycleConstructors map[string]LifecycleConstructor
// serviceFuncs is a map of registered services which are used to boot devp2p
// lifecycleConstructorFuncs is a map of registered services which are used to boot devp2p
// nodes
var serviceFuncs = make(Services)
var lifecycleConstructorFuncs = make(LifecycleConstructors)
// RegisterServices registers the given Services which can then be used to
// RegisterLifecycles registers the given Services which can then be used to
// start devp2p nodes using either the Exec or Docker adapters.
//
// It should be called in an init function so that it has the opportunity to
// execute the services before main() is called.
func RegisterServices(services Services) {
for name, f := range services {
if _, exists := serviceFuncs[name]; exists {
func RegisterLifecycles(lifecycles LifecycleConstructors) {
for name, f := range lifecycles {
if _, exists := lifecycleConstructorFuncs[name]; exists {
panic(fmt.Sprintf("node service already exists: %q", name))
}
serviceFuncs[name] = f
lifecycleConstructorFuncs[name] = f
}
// now we have registered the services, run reexec.Init() which will

View File

@ -26,8 +26,8 @@ import (
func newTestNetwork(t *testing.T, nodeCount int) (*Network, []enode.ID) {
t.Helper()
adapter := adapters.NewSimAdapter(adapters.Services{
"noopwoop": func(ctx *adapters.ServiceContext) (node.Service, error) {
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
return NewNoopService(nil), nil
},
})

View File

@ -31,7 +31,6 @@ import (
"github.com/ledgerwatch/turbo-geth/p2p/enode"
"github.com/ledgerwatch/turbo-geth/p2p/simulations"
"github.com/ledgerwatch/turbo-geth/p2p/simulations/adapters"
"github.com/ledgerwatch/turbo-geth/rpc"
)
var adapterType = flag.String("adapter", "sim", `node adapter to use (one of "sim", "exec" or "docker")`)
@ -45,12 +44,14 @@ func main() {
log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(false))))
// register a single ping-pong service
services := map[string]adapters.ServiceFunc{
"ping-pong": func(ctx *adapters.ServiceContext) (node.Service, error) {
return newPingPongService(ctx.Config.ID), nil
services := map[string]adapters.LifecycleConstructor{
"ping-pong": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
pps := newPingPongService(ctx.Config.ID)
stack.RegisterProtocols(pps.Protocols())
return pps, nil
},
}
adapters.RegisterServices(services)
adapters.RegisterLifecycles(services)
// create the NodeAdapter
var adapter adapters.NodeAdapter
@ -110,11 +111,7 @@ func (p *pingPongService) Protocols() []p2p.Protocol {
}}
}
func (p *pingPongService) APIs() []rpc.API {
return nil
}
func (p *pingPongService) Start(server *p2p.Server) error {
func (p *pingPongService) Start() error {
p.log.Info("ping-pong service starting")
return nil
}

View File

@ -63,12 +63,15 @@ type testService struct {
state atomic.Value
}
func newTestService(ctx *adapters.ServiceContext) (node.Service, error) {
func newTestService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
svc := &testService{
id: ctx.Config.ID,
peers: make(map[enode.ID]*testPeer),
}
svc.state.Store(ctx.Snapshot)
stack.RegisterProtocols(svc.Protocols())
stack.RegisterAPIs(svc.APIs())
return svc, nil
}
@ -125,7 +128,7 @@ func (t *testService) APIs() []rpc.API {
}}
}
func (t *testService) Start(server *p2p.Server) error {
func (t *testService) Start() error {
return nil
}
@ -287,7 +290,7 @@ func (t *TestAPI) Events(ctx context.Context) (*rpc.Subscription, error) {
return rpcSub, nil
}
var testServices = adapters.Services{
var testServices = adapters.LifecycleConstructors{
"test": newTestService,
}

View File

@ -110,8 +110,8 @@ func (net *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error)
}
// if no services are configured, use the default service
if len(conf.Services) == 0 {
conf.Services = []string{net.DefaultService}
if len(conf.Lifecycles) == 0 {
conf.Lifecycles = []string{net.DefaultService}
}
// use the NodeAdapter to create the node
@ -913,19 +913,19 @@ func (net *Network) snapshot(addServices []string, removeServices []string) (*Sn
snap.Nodes[i].Snapshots = snapshots
for _, addSvc := range addServices {
haveSvc := false
for _, svc := range snap.Nodes[i].Node.Config.Services {
for _, svc := range snap.Nodes[i].Node.Config.Lifecycles {
if svc == addSvc {
haveSvc = true
break
}
}
if !haveSvc {
snap.Nodes[i].Node.Config.Services = append(snap.Nodes[i].Node.Config.Services, addSvc)
snap.Nodes[i].Node.Config.Lifecycles = append(snap.Nodes[i].Node.Config.Lifecycles, addSvc)
}
}
if len(removeServices) > 0 {
var cleanedServices []string
for _, svc := range snap.Nodes[i].Node.Config.Services {
for _, svc := range snap.Nodes[i].Node.Config.Lifecycles {
haveSvc := false
for _, rmSvc := range removeServices {
if rmSvc == svc {
@ -938,7 +938,7 @@ func (net *Network) snapshot(addServices []string, removeServices []string) (*Sn
}
}
snap.Nodes[i].Node.Config.Services = cleanedServices
snap.Nodes[i].Node.Config.Lifecycles = cleanedServices
}
}
for _, conn := range net.Conns {

View File

@ -41,8 +41,8 @@ func TestSnapshot(t *testing.T) {
// create snapshot from ring network
// this is a minimal service, whose protocol will take exactly one message OR close of connection before quitting
adapter := adapters.NewSimAdapter(adapters.Services{
"noopwoop": func(ctx *adapters.ServiceContext) (node.Service, error) {
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
return NewNoopService(nil), nil
},
})
@ -165,8 +165,8 @@ OUTER:
// PART II
// load snapshot and verify that exactly same connections are formed
adapter = adapters.NewSimAdapter(adapters.Services{
"noopwoop": func(ctx *adapters.ServiceContext) (node.Service, error) {
adapter = adapters.NewSimAdapter(adapters.LifecycleConstructors{
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
return NewNoopService(nil), nil
},
})
@ -256,8 +256,8 @@ OuterTwo:
t.Run("conns after load", func(t *testing.T) {
// Create new network.
n := NewNetwork(
adapters.NewSimAdapter(adapters.Services{
"noopwoop": func(ctx *adapters.ServiceContext) (node.Service, error) {
adapters.NewSimAdapter(adapters.LifecycleConstructors{
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
return NewNoopService(nil), nil
},
}),
@ -288,7 +288,7 @@ OuterTwo:
// with each other and that a snapshot fully represents the desired topology
func TestNetworkSimulation(t *testing.T) {
// create simulation network with 20 testService nodes
adapter := adapters.NewSimAdapter(adapters.Services{
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"test": newTestService,
})
network := NewNetwork(adapter, &NetworkConfig{
@ -437,7 +437,7 @@ func createTestNodesWithProperty(property string, count int, network *Network) (
// It then tests again whilst excluding a node ID from being returned.
// If a node ID is not returned, or more node IDs than expected are returned, the test fails.
func TestGetNodeIDs(t *testing.T) {
adapter := adapters.NewSimAdapter(adapters.Services{
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"test": newTestService,
})
network := NewNetwork(adapter, &NetworkConfig{
@ -486,7 +486,7 @@ func TestGetNodeIDs(t *testing.T) {
// It then tests again whilst excluding a node from being returned.
// If a node is not returned, or more nodes than expected are returned, the test fails.
func TestGetNodes(t *testing.T) {
adapter := adapters.NewSimAdapter(adapters.Services{
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"test": newTestService,
})
network := NewNetwork(adapter, &NetworkConfig{
@ -534,7 +534,7 @@ func TestGetNodes(t *testing.T) {
// TestGetNodesByID creates a set of nodes and attempts to retrieve a subset of them by ID
// If a node is not returned, or more nodes than expected are returned, the test fails.
func TestGetNodesByID(t *testing.T) {
adapter := adapters.NewSimAdapter(adapters.Services{
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"test": newTestService,
})
network := NewNetwork(adapter, &NetworkConfig{
@ -579,7 +579,7 @@ func TestGetNodesByID(t *testing.T) {
// GetNodesByProperty is then checked for correctness by comparing the nodes returned to those initially created.
// If a node with a property is not found, or more nodes than expected are returned, the test fails.
func TestGetNodesByProperty(t *testing.T) {
adapter := adapters.NewSimAdapter(adapters.Services{
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"test": newTestService,
})
network := NewNetwork(adapter, &NetworkConfig{
@ -624,7 +624,7 @@ func TestGetNodesByProperty(t *testing.T) {
// GetNodeIDsByProperty is then checked for correctness by comparing the node IDs returned to those initially created.
// If a node ID with a property is not found, or more nodes IDs than expected are returned, the test fails.
func TestGetNodeIDsByProperty(t *testing.T) {
adapter := adapters.NewSimAdapter(adapters.Services{
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"test": newTestService,
})
network := NewNetwork(adapter, &NetworkConfig{
@ -705,8 +705,8 @@ func benchmarkMinimalServiceTmp(b *testing.B) {
// this is a minimal service, whose protocol will close a channel upon run of protocol
// making it possible to bench the time it takes for the service to start and protocol actually to be run
protoCMap := make(map[enode.ID]map[enode.ID]chan struct{})
adapter := adapters.NewSimAdapter(adapters.Services{
"noopwoop": func(ctx *adapters.ServiceContext) (node.Service, error) {
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
protoCMap[ctx.Config.ID] = make(map[enode.ID]chan struct{})
svc := NewNoopService(protoCMap[ctx.Config.ID])
return svc, nil

View File

@ -66,7 +66,7 @@ func (t *NoopService) APIs() []rpc.API {
return []rpc.API{}
}
func (t *NoopService) Start(server *p2p.Server) error {
func (t *NoopService) Start() error {
return nil
}

View File

@ -1,67 +0,0 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package testing
import (
"fmt"
"sync"
"github.com/ledgerwatch/turbo-geth/log"
"github.com/ledgerwatch/turbo-geth/p2p/enode"
)
type TestPeer interface {
ID() enode.ID
Drop()
}
// TestPeerPool is an example peerPool to demonstrate registration of peer connections
type TestPeerPool struct {
lock sync.Mutex
peers map[enode.ID]TestPeer
}
func NewTestPeerPool() *TestPeerPool {
return &TestPeerPool{peers: make(map[enode.ID]TestPeer)}
}
func (p *TestPeerPool) Add(peer TestPeer) {
p.lock.Lock()
defer p.lock.Unlock()
log.Trace(fmt.Sprintf("pp add peer %v", peer.ID()))
p.peers[peer.ID()] = peer
}
func (p *TestPeerPool) Remove(peer TestPeer) {
p.lock.Lock()
defer p.lock.Unlock()
delete(p.peers, peer.ID())
}
func (p *TestPeerPool) Has(id enode.ID) bool {
p.lock.Lock()
defer p.lock.Unlock()
_, ok := p.peers[id]
return ok
}
func (p *TestPeerPool) Get(id enode.ID) TestPeer {
p.lock.Lock()
defer p.lock.Unlock()
return p.peers[id]
}

View File

@ -1,283 +0,0 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package testing
import (
"errors"
"fmt"
"sync"
"time"
"github.com/ledgerwatch/turbo-geth/log"
"github.com/ledgerwatch/turbo-geth/p2p"
"github.com/ledgerwatch/turbo-geth/p2p/enode"
"github.com/ledgerwatch/turbo-geth/p2p/simulations/adapters"
)
var errTimedOut = errors.New("timed out")
// ProtocolSession is a quasi simulation of a pivot node running
// a service and a number of dummy peers that can send (trigger) or
// receive (expect) messages
type ProtocolSession struct {
Server *p2p.Server
Nodes []*enode.Node
adapter *adapters.SimAdapter
events chan *p2p.PeerEvent
}
// Exchange is the basic units of protocol tests
// the triggers and expects in the arrays are run immediately and asynchronously
// thus one cannot have multiple expects for the SAME peer with DIFFERENT message types
// because it's unpredictable which expect will receive which message
// (with expect #1 and #2, messages might be sent #2 and #1, and both expects will complain about wrong message code)
// an exchange is defined on a session
type Exchange struct {
Label string
Triggers []Trigger
Expects []Expect
Timeout time.Duration
}
// Trigger is part of the exchange, incoming message for the pivot node
// sent by a peer
type Trigger struct {
Msg interface{} // type of message to be sent
Code uint64 // code of message is given
Peer enode.ID // the peer to send the message to
Timeout time.Duration // timeout duration for the sending
}
// Expect is part of an exchange, outgoing message from the pivot node
// received by a peer
type Expect struct {
Msg interface{} // type of message to expect
Code uint64 // code of message is now given
Peer enode.ID // the peer that expects the message
Timeout time.Duration // timeout duration for receiving
}
// Disconnect represents a disconnect event, used and checked by TestDisconnected
type Disconnect struct {
Peer enode.ID // discconnected peer
Error error // disconnect reason
}
// trigger sends messages from peers
func (s *ProtocolSession) trigger(trig Trigger) error {
simNode, ok := s.adapter.GetNode(trig.Peer)
if !ok {
return fmt.Errorf("trigger: peer %v does not exist (1- %v)", trig.Peer, len(s.Nodes))
}
mockNode, ok := simNode.Services()[0].(*mockNode)
if !ok {
return fmt.Errorf("trigger: peer %v is not a mock", trig.Peer)
}
errc := make(chan error)
go func() {
log.Trace(fmt.Sprintf("trigger %v (%v)....", trig.Msg, trig.Code))
errc <- mockNode.Trigger(&trig)
log.Trace(fmt.Sprintf("triggered %v (%v)", trig.Msg, trig.Code))
}()
t := trig.Timeout
if t == time.Duration(0) {
t = 1000 * time.Millisecond
}
select {
case err := <-errc:
return err
case <-time.After(t):
return fmt.Errorf("timout expecting %v to send to peer %v", trig.Msg, trig.Peer)
}
}
// expect checks an expectation of a message sent out by the pivot node
func (s *ProtocolSession) expect(exps []Expect) error {
// construct a map of expectations for each node
peerExpects := make(map[enode.ID][]Expect)
for _, exp := range exps {
if exp.Msg == nil {
return errors.New("no message to expect")
}
peerExpects[exp.Peer] = append(peerExpects[exp.Peer], exp)
}
// construct a map of mockNodes for each node
mockNodes := make(map[enode.ID]*mockNode)
for nodeID := range peerExpects {
simNode, ok := s.adapter.GetNode(nodeID)
if !ok {
return fmt.Errorf("trigger: peer %v does not exist (1- %v)", nodeID, len(s.Nodes))
}
mockNode, ok := simNode.Services()[0].(*mockNode)
if !ok {
return fmt.Errorf("trigger: peer %v is not a mock", nodeID)
}
mockNodes[nodeID] = mockNode
}
// done chanell cancels all created goroutines when function returns
done := make(chan struct{})
defer close(done)
// errc catches the first error from
errc := make(chan error)
wg := &sync.WaitGroup{}
wg.Add(len(mockNodes))
for nodeID, mockNode := range mockNodes {
nodeID := nodeID
mockNode := mockNode
go func() {
defer wg.Done()
// Sum all Expect timeouts to give the maximum
// time for all expectations to finish.
// mockNode.Expect checks all received messages against
// a list of expected messages and timeout for each
// of them can not be checked separately.
var t time.Duration
for _, exp := range peerExpects[nodeID] {
if exp.Timeout == time.Duration(0) {
t += 2000 * time.Millisecond
} else {
t += exp.Timeout
}
}
alarm := time.NewTimer(t)
defer alarm.Stop()
// expectErrc is used to check if error returned
// from mockNode.Expect is not nil and to send it to
// errc only in that case.
// done channel will be closed when function
expectErrc := make(chan error)
go func() {
select {
case expectErrc <- mockNode.Expect(peerExpects[nodeID]...):
case <-done:
case <-alarm.C:
}
}()
select {
case err := <-expectErrc:
if err != nil {
select {
case errc <- err:
case <-done:
case <-alarm.C:
errc <- errTimedOut
}
}
case <-done:
case <-alarm.C:
errc <- errTimedOut
}
}()
}
go func() {
wg.Wait()
// close errc when all goroutines finish to return nill err from errc
close(errc)
}()
return <-errc
}
// TestExchanges tests a series of exchanges against the session
func (s *ProtocolSession) TestExchanges(exchanges ...Exchange) error {
for i, e := range exchanges {
if err := s.testExchange(e); err != nil {
return fmt.Errorf("exchange #%d %q: %v", i, e.Label, err)
}
log.Trace(fmt.Sprintf("exchange #%d %q: run successfully", i, e.Label))
}
return nil
}
// testExchange tests a single Exchange.
// Default timeout value is 2 seconds.
func (s *ProtocolSession) testExchange(e Exchange) error {
errc := make(chan error)
done := make(chan struct{})
defer close(done)
go func() {
for _, trig := range e.Triggers {
err := s.trigger(trig)
if err != nil {
errc <- err
return
}
}
select {
case errc <- s.expect(e.Expects):
case <-done:
}
}()
// time out globally or finish when all expectations satisfied
t := e.Timeout
if t == 0 {
t = 2000 * time.Millisecond
}
alarm := time.NewTimer(t)
defer alarm.Stop()
select {
case err := <-errc:
return err
case <-alarm.C:
return errTimedOut
}
}
// TestDisconnected tests the disconnections given as arguments
// the disconnect structs describe what disconnect error is expected on which peer
func (s *ProtocolSession) TestDisconnected(disconnects ...*Disconnect) error {
expects := make(map[enode.ID]error)
for _, disconnect := range disconnects {
expects[disconnect.Peer] = disconnect.Error
}
timeout := time.After(time.Second)
for len(expects) > 0 {
select {
case event := <-s.events:
if event.Type != p2p.PeerEventTypeDrop {
continue
}
expectErr, ok := expects[event.Peer]
if !ok {
continue
}
if !(expectErr == nil && event.Error == "" || expectErr != nil && expectErr.Error() == event.Error) {
return fmt.Errorf("unexpected error on peer %v. expected '%v', got '%v'", event.Peer, expectErr, event.Error)
}
delete(expects, event.Peer)
case <-timeout:
return fmt.Errorf("timed out waiting for peers to disconnect")
}
}
return nil
}

View File

@ -1,284 +0,0 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
/*
the p2p/testing package provides a unit test scheme to check simple
protocol message exchanges with one pivot node and a number of dummy peers
The pivot test node runs a node.Service, the dummy peers run a mock node
that can be used to send and receive messages
*/
package testing
import (
"bytes"
"crypto/ecdsa"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"github.com/ledgerwatch/turbo-geth/log"
"github.com/ledgerwatch/turbo-geth/node"
"github.com/ledgerwatch/turbo-geth/p2p"
"github.com/ledgerwatch/turbo-geth/p2p/enode"
"github.com/ledgerwatch/turbo-geth/p2p/simulations"
"github.com/ledgerwatch/turbo-geth/p2p/simulations/adapters"
"github.com/ledgerwatch/turbo-geth/rlp"
"github.com/ledgerwatch/turbo-geth/rpc"
)
// ProtocolTester is the tester environment used for unit testing protocol
// message exchanges. It uses p2p/simulations framework
type ProtocolTester struct {
*ProtocolSession
network *simulations.Network
}
// NewProtocolTester constructs a new ProtocolTester
// it takes as argument the pivot node id, the number of dummy peers and the
// protocol run function called on a peer connection by the p2p server
func NewProtocolTester(prvkey *ecdsa.PrivateKey, nodeCount int, run func(*p2p.Peer, p2p.MsgReadWriter) error) *ProtocolTester {
services := adapters.Services{
"test": func(ctx *adapters.ServiceContext) (node.Service, error) {
return &testNode{run}, nil
},
"mock": func(ctx *adapters.ServiceContext) (node.Service, error) {
return newMockNode(), nil
},
}
adapter := adapters.NewSimAdapter(services)
net := simulations.NewNetwork(adapter, &simulations.NetworkConfig{})
nodeConfig := &adapters.NodeConfig{
PrivateKey: prvkey,
EnableMsgEvents: true,
Services: []string{"test"},
}
if _, err := net.NewNodeWithConfig(nodeConfig); err != nil {
panic(err.Error())
}
if err := net.Start(nodeConfig.ID); err != nil {
panic(err.Error())
}
node := net.GetNode(nodeConfig.ID).Node.(*adapters.SimNode)
peers := make([]*adapters.NodeConfig, nodeCount)
nodes := make([]*enode.Node, nodeCount)
for i := 0; i < nodeCount; i++ {
peers[i] = adapters.RandomNodeConfig()
peers[i].Services = []string{"mock"}
if _, err := net.NewNodeWithConfig(peers[i]); err != nil {
panic(fmt.Sprintf("error initializing peer %v: %v", peers[i].ID, err))
}
if err := net.Start(peers[i].ID); err != nil {
panic(fmt.Sprintf("error starting peer %v: %v", peers[i].ID, err))
}
nodes[i] = peers[i].Node()
}
events := make(chan *p2p.PeerEvent, 1000)
node.SubscribeEvents(events)
ps := &ProtocolSession{
Server: node.Server(),
Nodes: nodes,
adapter: adapter,
events: events,
}
self := &ProtocolTester{
ProtocolSession: ps,
network: net,
}
self.Connect(nodeConfig.ID, peers...)
return self
}
// Stop stops the p2p server
func (t *ProtocolTester) Stop() {
t.Server.Stop()
t.network.Shutdown()
}
// Connect brings up the remote peer node and connects it using the
// p2p/simulations network connection with the in memory network adapter
func (t *ProtocolTester) Connect(selfID enode.ID, peers ...*adapters.NodeConfig) {
for _, peer := range peers {
log.Trace(fmt.Sprintf("connect to %v", peer.ID))
if err := t.network.Connect(selfID, peer.ID); err != nil {
panic(fmt.Sprintf("error connecting to peer %v: %v", peer.ID, err))
}
}
}
// testNode wraps a protocol run function and implements the node.Service
// interface
type testNode struct {
run func(*p2p.Peer, p2p.MsgReadWriter) error
}
func (t *testNode) Protocols() []p2p.Protocol {
return []p2p.Protocol{{
Length: 100,
Run: t.run,
}}
}
func (t *testNode) APIs() []rpc.API {
return nil
}
func (t *testNode) Start(server *p2p.Server) error {
return nil
}
func (t *testNode) Stop() error {
return nil
}
// mockNode is a testNode which doesn't actually run a protocol, instead
// exposing channels so that tests can manually trigger and expect certain
// messages
type mockNode struct {
testNode
trigger chan *Trigger
expect chan []Expect
err chan error
stop chan struct{}
stopOnce sync.Once
}
func newMockNode() *mockNode {
mock := &mockNode{
trigger: make(chan *Trigger),
expect: make(chan []Expect),
err: make(chan error),
stop: make(chan struct{}),
}
mock.testNode.run = mock.Run
return mock
}
// Run is a protocol run function which just loops waiting for tests to
// instruct it to either trigger or expect a message from the peer
func (m *mockNode) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error {
for {
select {
case trig := <-m.trigger:
wmsg := Wrap(trig.Msg)
m.err <- p2p.Send(rw, trig.Code, wmsg)
case exps := <-m.expect:
m.err <- expectMsgs(rw, exps)
case <-m.stop:
return nil
}
}
}
func (m *mockNode) Trigger(trig *Trigger) error {
m.trigger <- trig
return <-m.err
}
func (m *mockNode) Expect(exp ...Expect) error {
m.expect <- exp
return <-m.err
}
func (m *mockNode) Stop() error {
m.stopOnce.Do(func() { close(m.stop) })
return nil
}
func expectMsgs(rw p2p.MsgReadWriter, exps []Expect) error {
matched := make([]bool, len(exps))
for {
msg, err := rw.ReadMsg()
if err != nil {
if err == io.EOF {
break
}
return err
}
actualContent, err := ioutil.ReadAll(msg.Payload)
if err != nil {
return err
}
var found bool
for i, exp := range exps {
if exp.Code == msg.Code && bytes.Equal(actualContent, mustEncodeMsg(Wrap(exp.Msg))) {
if matched[i] {
return fmt.Errorf("message #%d received two times", i)
}
matched[i] = true
found = true
break
}
}
if !found {
expected := make([]string, 0)
for i, exp := range exps {
if matched[i] {
continue
}
expected = append(expected, fmt.Sprintf("code %d payload %x", exp.Code, mustEncodeMsg(Wrap(exp.Msg))))
}
return fmt.Errorf("unexpected message code %d payload %x, expected %s", msg.Code, actualContent, strings.Join(expected, " or "))
}
done := true
for _, m := range matched {
if !m {
done = false
break
}
}
if done {
return nil
}
}
for i, m := range matched {
if !m {
return fmt.Errorf("expected message #%d not received", i)
}
}
return nil
}
// mustEncodeMsg uses rlp to encode a message.
// In case of error it panics.
func mustEncodeMsg(msg interface{}) []byte {
contentEnc, err := rlp.EncodeToBytes(msg)
if err != nil {
panic("content encode error: " + err.Error())
}
return contentEnc
}
type WrappedMsg struct {
Context []byte
Size uint32
Payload []byte
}
func Wrap(msg interface{}) interface{} {
data, _ := rlp.EncodeToBytes(msg)
return &WrappedMsg{
Size: uint32(len(data)),
Payload: data,
}
}