2017-05-16 12:09:52 +00:00
|
|
|
package node
|
2016-09-11 11:44:14 +00:00
|
|
|
|
|
|
|
import (
|
2017-05-02 14:35:37 +00:00
|
|
|
"encoding/json"
|
2016-09-11 11:44:14 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2016-12-07 21:07:08 +00:00
|
|
|
"os"
|
2017-01-26 15:48:48 +00:00
|
|
|
"path"
|
2017-03-28 09:04:52 +00:00
|
|
|
"path/filepath"
|
2018-06-13 11:24:04 +00:00
|
|
|
"time"
|
2016-09-11 11:44:14 +00:00
|
|
|
|
2017-05-02 14:35:37 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
2016-12-07 21:07:08 +00:00
|
|
|
"github.com/ethereum/go-ethereum/eth"
|
2017-05-02 14:35:37 +00:00
|
|
|
"github.com/ethereum/go-ethereum/eth/downloader"
|
2016-09-11 11:44:14 +00:00
|
|
|
"github.com/ethereum/go-ethereum/les"
|
2018-03-20 18:35:28 +00:00
|
|
|
"github.com/ethereum/go-ethereum/log"
|
2016-09-11 11:44:14 +00:00
|
|
|
"github.com/ethereum/go-ethereum/node"
|
2017-05-02 14:35:37 +00:00
|
|
|
"github.com/ethereum/go-ethereum/p2p"
|
2017-02-20 09:44:38 +00:00
|
|
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
2018-04-10 06:44:09 +00:00
|
|
|
"github.com/ethereum/go-ethereum/p2p/discv5"
|
2017-03-10 00:16:32 +00:00
|
|
|
"github.com/ethereum/go-ethereum/p2p/nat"
|
2018-03-02 09:25:30 +00:00
|
|
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv6"
|
2018-05-11 19:43:07 +00:00
|
|
|
"github.com/status-im/status-go/mailserver"
|
2018-01-30 11:51:48 +00:00
|
|
|
shhmetrics "github.com/status-im/status-go/metrics/whisper"
|
2018-06-08 11:29:50 +00:00
|
|
|
"github.com/status-im/status-go/params"
|
2018-07-16 07:40:40 +00:00
|
|
|
"github.com/status-im/status-go/services/peer"
|
2018-04-10 10:02:54 +00:00
|
|
|
"github.com/status-im/status-go/services/personal"
|
|
|
|
"github.com/status-im/status-go/services/shhext"
|
2018-05-03 10:36:56 +00:00
|
|
|
"github.com/status-im/status-go/services/status"
|
2018-05-08 10:38:54 +00:00
|
|
|
"github.com/status-im/status-go/timesource"
|
2018-05-02 12:14:08 +00:00
|
|
|
"github.com/syndtr/goleveldb/leveldb"
|
2016-09-11 11:44:14 +00:00
|
|
|
)
|
|
|
|
|
2018-04-16 12:36:09 +00:00
|
|
|
// Errors related to node and services creation.
|
2016-12-07 21:07:08 +00:00
|
|
|
var (
|
2018-07-21 15:32:53 +00:00
|
|
|
ErrNodeMakeFailureFormat = "error creating p2p node: %s"
|
2018-04-10 10:02:54 +00:00
|
|
|
ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service")
|
|
|
|
ErrLightEthRegistrationFailure = errors.New("failed to register the LES service")
|
|
|
|
ErrPersonalServiceRegistrationFailure = errors.New("failed to register the personal api service")
|
2018-05-03 10:36:56 +00:00
|
|
|
ErrStatusServiceRegistrationFailure = errors.New("failed to register the Status service")
|
2018-07-16 07:40:40 +00:00
|
|
|
ErrPeerServiceRegistrationFailure = errors.New("failed to register the Peer service")
|
2016-12-07 21:07:08 +00:00
|
|
|
)
|
2016-09-11 11:44:14 +00:00
|
|
|
|
2018-03-20 18:35:28 +00:00
|
|
|
// All general log messages in this package should be routed through this logger.
|
2018-06-08 11:29:50 +00:00
|
|
|
var logger = log.New("package", "status-go/node")
|
2018-03-20 18:35:28 +00:00
|
|
|
|
2016-12-07 21:07:08 +00:00
|
|
|
// MakeNode create a geth node entity
|
2018-05-02 12:14:08 +00:00
|
|
|
func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
|
2018-04-16 12:36:09 +00:00
|
|
|
// If DataDir is empty, it means we want to create an ephemeral node
|
|
|
|
// keeping data only in memory.
|
|
|
|
if config.DataDir != "" {
|
|
|
|
// make sure data directory exists
|
|
|
|
if err := os.MkdirAll(filepath.Clean(config.DataDir), os.ModePerm); err != nil {
|
|
|
|
return nil, fmt.Errorf("make node: make data directory: %v", err)
|
|
|
|
}
|
2017-05-16 12:09:52 +00:00
|
|
|
|
2018-04-16 12:36:09 +00:00
|
|
|
// make sure keys directory exists
|
|
|
|
if err := os.MkdirAll(filepath.Clean(config.KeyStoreDir), os.ModePerm); err != nil {
|
|
|
|
return nil, fmt.Errorf("make node: make keys directory: %v", err)
|
|
|
|
}
|
2017-04-28 08:49:15 +00:00
|
|
|
}
|
2017-03-28 09:04:52 +00:00
|
|
|
|
2017-05-16 03:24:56 +00:00
|
|
|
stackConfig := defaultEmbeddedNodeConfig(config)
|
2017-05-02 14:35:37 +00:00
|
|
|
|
|
|
|
if len(config.NodeKeyFile) > 0 {
|
2018-03-20 18:35:28 +00:00
|
|
|
logger.Info("Loading private key file", "file", config.NodeKeyFile)
|
2017-05-02 14:35:37 +00:00
|
|
|
pk, err := crypto.LoadECDSA(config.NodeKeyFile)
|
|
|
|
if err != nil {
|
2018-03-20 18:35:28 +00:00
|
|
|
logger.Error("Failed loading private key file", "file", config.NodeKeyFile, "error", err)
|
2017-05-02 14:35:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// override node's private key
|
|
|
|
stackConfig.P2P.PrivateKey = pk
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2017-01-26 00:39:20 +00:00
|
|
|
stack, err := node.New(stackConfig)
|
2016-09-11 11:44:14 +00:00
|
|
|
if err != nil {
|
2018-07-21 15:32:53 +00:00
|
|
|
return nil, fmt.Errorf(ErrNodeMakeFailureFormat, err.Error())
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2018-04-16 12:36:09 +00:00
|
|
|
// start Ethereum service if we are not expected to use an upstream server
|
2017-08-15 10:27:12 +00:00
|
|
|
if !config.UpstreamConfig.Enabled {
|
2018-04-16 12:36:09 +00:00
|
|
|
if err := activateLightEthService(stack, config); err != nil {
|
|
|
|
return nil, fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err)
|
2017-08-15 10:27:12 +00:00
|
|
|
}
|
2018-04-10 10:02:54 +00:00
|
|
|
} else {
|
2018-04-18 11:04:02 +00:00
|
|
|
// `personal_sign` and `personal_ecRecover` methods are important to
|
2018-04-10 10:02:54 +00:00
|
|
|
// keep DApps working.
|
|
|
|
// Usually, they are provided by an ETH or a LES service, but when using
|
|
|
|
// upstream, we don't start any of these, so we need to start our own
|
|
|
|
// implementation.
|
|
|
|
if err := activatePersonalService(stack, config); err != nil {
|
|
|
|
return nil, fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err)
|
|
|
|
}
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2018-04-16 12:36:09 +00:00
|
|
|
// start Whisper service.
|
2018-05-02 12:14:08 +00:00
|
|
|
if err := activateShhService(stack, config, db); err != nil {
|
2017-05-16 12:09:52 +00:00
|
|
|
return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2018-05-03 10:36:56 +00:00
|
|
|
// start status service.
|
|
|
|
if err := activateStatusService(stack, config); err != nil {
|
|
|
|
return nil, fmt.Errorf("%v: %v", ErrStatusServiceRegistrationFailure, err)
|
|
|
|
}
|
|
|
|
|
2018-07-16 07:40:40 +00:00
|
|
|
// start peer service
|
|
|
|
if err := activatePeerService(stack); err != nil {
|
|
|
|
return nil, fmt.Errorf("%v: %v", ErrPeerServiceRegistrationFailure, err)
|
|
|
|
}
|
|
|
|
|
2017-05-16 12:09:52 +00:00
|
|
|
return stack, nil
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 03:24:56 +00:00
|
|
|
// defaultEmbeddedNodeConfig returns default stack configuration for mobile client node
|
|
|
|
func defaultEmbeddedNodeConfig(config *params.NodeConfig) *node.Config {
|
2017-07-13 06:54:10 +00:00
|
|
|
nc := &node.Config{
|
2017-05-16 03:24:56 +00:00
|
|
|
DataDir: config.DataDir,
|
|
|
|
KeyStoreDir: config.KeyStoreDir,
|
|
|
|
UseLightweightKDF: true,
|
2017-05-16 12:09:52 +00:00
|
|
|
NoUSB: true,
|
2017-05-16 03:24:56 +00:00
|
|
|
Name: config.Name,
|
|
|
|
Version: config.Version,
|
|
|
|
P2P: p2p.Config{
|
2018-05-10 11:45:51 +00:00
|
|
|
NoDiscovery: true, // we always use only v5 server
|
2018-04-10 06:44:09 +00:00
|
|
|
ListenAddr: config.ListenAddr,
|
|
|
|
NAT: nat.Any(),
|
|
|
|
MaxPeers: config.MaxPeers,
|
|
|
|
MaxPendingPeers: config.MaxPendingPeers,
|
2017-05-16 03:24:56 +00:00
|
|
|
},
|
2018-02-27 10:39:30 +00:00
|
|
|
IPCPath: makeIPCPath(config),
|
2018-04-16 12:36:09 +00:00
|
|
|
HTTPCors: nil,
|
2018-04-12 16:17:10 +00:00
|
|
|
HTTPModules: config.FormatAPIModules(),
|
2018-02-27 10:39:30 +00:00
|
|
|
HTTPVirtualHosts: []string{"localhost"},
|
2017-05-16 03:24:56 +00:00
|
|
|
}
|
2017-07-13 06:54:10 +00:00
|
|
|
|
|
|
|
if config.RPCEnabled {
|
|
|
|
nc.HTTPHost = config.HTTPHost
|
|
|
|
nc.HTTPPort = config.HTTPPort
|
|
|
|
}
|
|
|
|
|
2018-04-16 12:36:09 +00:00
|
|
|
if config.ClusterConfig != nil && config.ClusterConfig.Enabled {
|
2018-04-10 06:44:09 +00:00
|
|
|
nc.P2P.BootstrapNodesV5 = parseNodesV5(config.ClusterConfig.BootNodes)
|
2018-04-20 12:23:18 +00:00
|
|
|
nc.P2P.StaticNodes = parseNodes(config.ClusterConfig.StaticNodes)
|
2017-12-07 12:07:45 +00:00
|
|
|
}
|
2017-07-13 06:54:10 +00:00
|
|
|
return nc
|
2017-05-16 03:24:56 +00:00
|
|
|
}
|
|
|
|
|
2018-04-16 12:36:09 +00:00
|
|
|
// activateLightEthService configures and registers the eth.Ethereum service with a given node.
|
|
|
|
func activateLightEthService(stack *node.Node, config *params.NodeConfig) error {
|
|
|
|
if config.LightEthConfig == nil || !config.LightEthConfig.Enabled {
|
2018-03-20 18:35:28 +00:00
|
|
|
logger.Info("LES protocol is disabled")
|
2017-03-28 09:04:52 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-02 14:35:37 +00:00
|
|
|
var genesis *core.Genesis
|
|
|
|
if config.LightEthConfig.Genesis != "" {
|
|
|
|
genesis = new(core.Genesis)
|
|
|
|
if err := json.Unmarshal([]byte(config.LightEthConfig.Genesis), genesis); err != nil {
|
|
|
|
return fmt.Errorf("invalid genesis spec: %v", err)
|
|
|
|
}
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2017-05-02 14:35:37 +00:00
|
|
|
ethConf := eth.DefaultConfig
|
|
|
|
ethConf.Genesis = genesis
|
|
|
|
ethConf.SyncMode = downloader.LightSync
|
2017-05-03 14:24:48 +00:00
|
|
|
ethConf.NetworkId = config.NetworkID
|
2017-05-02 14:35:37 +00:00
|
|
|
ethConf.DatabaseCache = config.LightEthConfig.DatabaseCache
|
2018-04-16 12:36:09 +00:00
|
|
|
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
2018-01-15 20:26:41 +00:00
|
|
|
return les.New(ctx, ðConf)
|
2018-04-16 12:36:09 +00:00
|
|
|
})
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2018-04-10 10:02:54 +00:00
|
|
|
func activatePersonalService(stack *node.Node, config *params.NodeConfig) error {
|
|
|
|
return stack.Register(func(*node.ServiceContext) (node.Service, error) {
|
|
|
|
svc := personal.New(stack.AccountManager())
|
|
|
|
return svc, nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-05-03 10:36:56 +00:00
|
|
|
func activateStatusService(stack *node.Node, config *params.NodeConfig) error {
|
|
|
|
if !config.StatusServiceEnabled {
|
|
|
|
logger.Info("Status service api is disabled")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
|
|
|
var whisper *whisper.Whisper
|
|
|
|
if err := ctx.Service(&whisper); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
svc := status.New(whisper)
|
|
|
|
return svc, nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-07-16 07:40:40 +00:00
|
|
|
func activatePeerService(stack *node.Node) error {
|
|
|
|
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
|
|
|
svc := peer.New()
|
|
|
|
return svc, nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-07-04 09:30:57 +00:00
|
|
|
func registerMailServer(whisperService *whisper.Whisper, config *params.WhisperConfig) (err error) {
|
|
|
|
// if the Password is already set, do not override it
|
|
|
|
if config.MailServerPassword == "" && config.MailServerPasswordFile != "" {
|
|
|
|
err = config.ReadMailServerPasswordFile()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// similarly, do not override already configured AsymKey
|
|
|
|
if config.MailServerAsymKey == nil && config.MailServerAsymKeyFile != "" {
|
|
|
|
err = config.ReadMailServerAsymKeyFile()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var mailServer mailserver.WMailServer
|
|
|
|
whisperService.RegisterServer(&mailServer)
|
|
|
|
|
|
|
|
return mailServer.Init(whisperService, config)
|
|
|
|
}
|
|
|
|
|
2016-12-07 21:07:08 +00:00
|
|
|
// activateShhService configures Whisper and adds it to the given node.
|
2018-05-02 12:14:08 +00:00
|
|
|
func activateShhService(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) (err error) {
|
2018-04-16 12:36:09 +00:00
|
|
|
if config.WhisperConfig == nil || !config.WhisperConfig.Enabled {
|
2018-03-20 18:35:28 +00:00
|
|
|
logger.Info("SHH protocol is disabled")
|
2017-03-28 09:04:52 +00:00
|
|
|
return nil
|
|
|
|
}
|
2018-06-13 11:24:04 +00:00
|
|
|
if config.WhisperConfig.EnableNTPSync {
|
2018-06-12 16:50:25 +00:00
|
|
|
if err = stack.Register(func(*node.ServiceContext) (node.Service, error) {
|
2018-06-13 11:24:04 +00:00
|
|
|
return timesource.Default(), nil
|
|
|
|
}); err != nil {
|
2018-06-12 16:50:25 +00:00
|
|
|
return
|
2018-06-13 11:24:04 +00:00
|
|
|
}
|
2018-05-08 10:38:54 +00:00
|
|
|
}
|
2018-04-12 16:17:10 +00:00
|
|
|
|
2018-05-08 10:38:54 +00:00
|
|
|
err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
2018-02-19 14:53:40 +00:00
|
|
|
whisperServiceConfig := &whisper.Config{
|
|
|
|
MaxMessageSize: whisper.DefaultMaxMessageSize,
|
|
|
|
MinimumAcceptedPOW: 0.001,
|
2018-06-13 11:24:04 +00:00
|
|
|
TimeSource: time.Now,
|
2018-02-19 14:53:40 +00:00
|
|
|
}
|
2018-06-13 11:24:04 +00:00
|
|
|
|
|
|
|
if config.WhisperConfig.EnableNTPSync {
|
|
|
|
if whisperServiceConfig.TimeSource, err = whisperTimeSource(ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-12 16:17:10 +00:00
|
|
|
whisperService := whisper.New(whisperServiceConfig)
|
2017-04-12 18:26:54 +00:00
|
|
|
|
2018-01-30 11:51:48 +00:00
|
|
|
// enable metrics
|
|
|
|
whisperService.RegisterEnvelopeTracer(&shhmetrics.EnvelopeTracer{})
|
|
|
|
|
2017-04-12 18:26:54 +00:00
|
|
|
// enable mail service
|
2018-04-12 16:17:10 +00:00
|
|
|
if config.WhisperConfig.EnableMailServer {
|
2018-07-04 09:30:57 +00:00
|
|
|
if err := registerMailServer(whisperService, config.WhisperConfig); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to register MailServer: %v", err)
|
2018-05-11 19:43:07 +00:00
|
|
|
}
|
2017-04-12 18:26:54 +00:00
|
|
|
}
|
|
|
|
|
2018-04-12 16:17:10 +00:00
|
|
|
if config.WhisperConfig.LightClient {
|
2018-03-02 09:25:30 +00:00
|
|
|
emptyBloomFilter := make([]byte, 64)
|
|
|
|
if err := whisperService.SetBloomFilter(emptyBloomFilter); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-12 18:26:54 +00:00
|
|
|
return whisperService, nil
|
2018-04-12 16:17:10 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
2018-04-12 16:17:10 +00:00
|
|
|
|
2018-04-11 15:41:51 +00:00
|
|
|
// TODO(dshulyak) add a config option to enable it by default, but disable if app is started from statusd
|
2018-04-26 05:56:19 +00:00
|
|
|
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
2018-04-12 16:17:10 +00:00
|
|
|
var whisper *whisper.Whisper
|
|
|
|
if err := ctx.Service(&whisper); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-06-25 13:27:17 +00:00
|
|
|
svc := shhext.New(whisper, shhext.EnvelopeSignalHandler{}, db, config.DebugAPIEnabled)
|
2018-04-11 15:41:51 +00:00
|
|
|
return svc, nil
|
|
|
|
})
|
2016-09-11 11:44:14 +00:00
|
|
|
}
|
|
|
|
|
2017-01-26 02:46:37 +00:00
|
|
|
// makeIPCPath returns IPC-RPC filename
|
2017-03-15 21:03:01 +00:00
|
|
|
func makeIPCPath(config *params.NodeConfig) string {
|
|
|
|
if !config.IPCEnabled {
|
2017-01-26 02:46:37 +00:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2017-03-15 21:03:01 +00:00
|
|
|
return path.Join(config.DataDir, config.IPCFile)
|
2017-01-26 02:46:37 +00:00
|
|
|
}
|
|
|
|
|
2018-03-20 14:05:21 +00:00
|
|
|
// parseNodes creates list of discover.Node out of enode strings.
|
|
|
|
func parseNodes(enodes []string) []*discover.Node {
|
2018-06-04 13:47:13 +00:00
|
|
|
var nodes []*discover.Node
|
|
|
|
for _, enode := range enodes {
|
|
|
|
parsedPeer, err := discover.ParseNode(enode)
|
|
|
|
if err == nil {
|
|
|
|
nodes = append(nodes, parsedPeer)
|
|
|
|
} else {
|
|
|
|
logger.Error("Failed to parse enode", "enode", enode, "err", err)
|
|
|
|
}
|
|
|
|
|
2017-02-20 09:44:38 +00:00
|
|
|
}
|
2018-03-20 14:05:21 +00:00
|
|
|
return nodes
|
2017-02-20 09:44:38 +00:00
|
|
|
}
|
2018-04-10 06:44:09 +00:00
|
|
|
|
|
|
|
// parseNodesV5 creates list of discv5.Node out of enode strings.
|
|
|
|
func parseNodesV5(enodes []string) []*discv5.Node {
|
2018-06-04 13:47:13 +00:00
|
|
|
var nodes []*discv5.Node
|
|
|
|
for _, enode := range enodes {
|
|
|
|
parsedPeer, err := discv5.ParseNode(enode)
|
|
|
|
|
|
|
|
if err == nil {
|
|
|
|
nodes = append(nodes, parsedPeer)
|
|
|
|
} else {
|
|
|
|
logger.Error("Failed to parse enode", "enode", enode, "err", err)
|
|
|
|
}
|
2018-04-10 06:44:09 +00:00
|
|
|
}
|
|
|
|
return nodes
|
|
|
|
}
|
2018-06-13 11:24:04 +00:00
|
|
|
|
2018-07-25 14:48:02 +00:00
|
|
|
func parseNodesToNodeID(enodes []string) []discover.NodeID {
|
|
|
|
nodeIDs := make([]discover.NodeID, 0, len(enodes))
|
|
|
|
for _, node := range parseNodes(enodes) {
|
|
|
|
nodeIDs = append(nodeIDs, node.ID)
|
|
|
|
}
|
|
|
|
return nodeIDs
|
|
|
|
}
|
|
|
|
|
2018-06-13 11:24:04 +00:00
|
|
|
// whisperTimeSource get timeSource to be used by whisper
|
|
|
|
func whisperTimeSource(ctx *node.ServiceContext) (func() time.Time, error) {
|
|
|
|
var timeSource *timesource.NTPTimeSource
|
|
|
|
if err := ctx.Service(&timeSource); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return timeSource.Now, nil
|
|
|
|
}
|