From 8068d8020081d4962b0cb196016202ba5e03d72a Mon Sep 17 00:00:00 2001 From: Daniel Van Der Maden Date: Fri, 14 Aug 2020 00:01:13 -0700 Subject: [PATCH] Node API Refactor - pt3 (Stage 3.1 of Node API Overhaul) (#3297) * [rosetta] Add server with example block & network services * Update go.mod for rosetta SDK Signed-off-by: Daniel Van Der Maden * [internal/configs] Add RosettaServer to node config Signed-off-by: Daniel Van Der Maden * [cmd] Add rosetta port flag Signed-off-by: Daniel Van Der Maden * [node] Add rosetta server start & rename api.go Signed-off-by: Daniel Van Der Maden * [rosetta] Change server start to use nodeconfig of rosetta Signed-off-by: Daniel Van Der Maden * [cmd] Cleanup debugging prints Signed-off-by: Daniel Van Der Maden * [rosetta] Fix stdout print & document placeholders Signed-off-by: Daniel Van Der Maden * [rosetta] Fix lint & make StartSevers more consistent Signed-off-by: Daniel Van Der Maden * [cmd] Disable rosetta by default Signed-off-by: Daniel Van Der Maden * [test] Make explorer deploy rosetta server Signed-off-by: Daniel Van Der Maden * [rosetta] Use direct http server for start * Make go.mod changes minimal Signed-off-by: Daniel Van Der Maden * [rosetta] Fix fmt Signed-off-by: Daniel Van Der Maden * [rosetta] Fix fmt of go.mod Signed-off-by: Daniel Van Der Maden * [rosetta] Use port 9700 instead of 10000 Signed-off-by: Daniel Van Der Maden * [cmd] Bump config version Signed-off-by: Daniel Van Der Maden * [cmd] Add v1.0.0 config backwards compatibility test * Included update message if old config is loaded Signed-off-by: Daniel Van Der Maden * [test] Do not broadcast invalid tx on localnet Signed-off-by: Daniel Van Der Maden * [cmd] Correct for invalid port when loading old config Signed-off-by: Daniel Van Der Maden * [cmd] Make rosetta variable names consistent Signed-off-by: Daniel Van Der Maden --- cmd/harmony/config.go | 13 ++- cmd/harmony/config_test.go | 80 ++++++++++++++ cmd/harmony/default.go | 10 +- cmd/harmony/flags.go | 28 ++++- cmd/harmony/flags_test.go | 52 ++++++--- cmd/harmony/main.go | 18 ++++ go.mod | 7 +- internal/configs/node/config.go | 26 +++-- internal/configs/node/network.go | 14 ++- node/{rpc.go => api.go} | 7 ++ rosetta/common/config.go | 75 +++++++++++++ rosetta/rosetta.go | 87 +++++++++++++++ rosetta/services/block_service.go | 158 ++++++++++++++++++++++++++++ rosetta/services/network_service.go | 100 ++++++++++++++++++ test/deploy.sh | 4 +- 15 files changed, 643 insertions(+), 36 deletions(-) rename node/{rpc.go => api.go} (91%) create mode 100644 rosetta/common/config.go create mode 100644 rosetta/rosetta.go create mode 100644 rosetta/services/block_service.go create mode 100644 rosetta/services/network_service.go diff --git a/cmd/harmony/config.go b/cmd/harmony/config.go index ccb9c266b..cb381f573 100644 --- a/cmd/harmony/config.go +++ b/cmd/harmony/config.go @@ -97,9 +97,11 @@ type logContext struct { } type httpConfig struct { - Enabled bool - IP string - Port int + Enabled bool + IP string + Port int + RosettaEnabled bool + RosettaPort int } type wsConfig struct { @@ -235,6 +237,11 @@ func loadHarmonyConfig(file string) (harmonyConfig, error) { if err := toml.Unmarshal(b, &config); err != nil { return harmonyConfig{}, err } + + // Correct for old config version load (port 0 is invalid anyways) + if config.HTTP.RosettaPort == 0 { + config.HTTP.RosettaPort = defaultConfig.HTTP.RosettaPort + } return config, nil } diff --git a/cmd/harmony/config_test.go b/cmd/harmony/config_test.go index b0f8e5f47..40a0e88e8 100644 --- a/cmd/harmony/config_test.go +++ b/cmd/harmony/config_test.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io/ioutil" "os" "path/filepath" "reflect" @@ -28,6 +29,85 @@ func init() { } } +func TestV1_0_0Config(t *testing.T) { + testConfig := `Version = "1.0.0" + +[BLSKeys] + KMSConfigFile = "" + KMSConfigSrcType = "shared" + KMSEnabled = true + KeyDir = "./.hmy/blskeys" + KeyFiles = [] + MaxKeys = 10 + PassEnabled = true + PassFile = "" + PassSrcType = "auto" + SavePassphrase = false + +[General] + DataDir = "./" + IsArchival = false + NoStaking = false + NodeType = "validator" + ShardID = -1 + +[HTTP] + Enabled = true + IP = "127.0.0.1" + Port = 9500 + +[Log] + FileName = "harmony.log" + Folder = "./latest" + RotateSize = 100 + Verbosity = 3 + +[Network] + BootNodes = ["/ip4/100.26.90.187/tcp/9874/p2p/Qmdfjtk6hPoyrH1zVD9PEH4zfWLo38dP2mDvvKXfh3tnEv","/ip4/54.213.43.194/tcp/9874/p2p/QmZJJx6AdaoEkGLrYG4JeLCKeCKDjnFz2wfHNHxAqFSGA9","/ip4/13.113.101.219/tcp/12019/p2p/QmQayinFSgMMw5cSpDUiD9pQ2WeP6WNmGxpZ6ou3mdVFJX","/ip4/99.81.170.167/tcp/12019/p2p/QmRVbTpEYup8dSaURZfF6ByrMTSKa4UyUzJhSjahFzRqNj"] + DNSPort = 9000 + DNSZone = "t.hmny.io" + LegacySyncing = false + NetworkType = "mainnet" + +[P2P] + KeyFile = "./.hmykey" + Port = 9000 + +[Pprof] + Enabled = false + ListenAddr = "127.0.0.1:6060" + +[TxPool] + BlacklistFile = "./.hmy/blacklist.txt" + +[WS] + Enabled = true + IP = "127.0.0.1" + Port = 9800` + testDir := filepath.Join(testBaseDir, t.Name()) + os.RemoveAll(testDir) + os.MkdirAll(testDir, 0777) + file := filepath.Join(testDir, "test.config") + err := ioutil.WriteFile(file, []byte(testConfig), 0644) + if err != nil { + t.Fatal(err) + } + config, err := loadHarmonyConfig(file) + if err != nil { + t.Fatal(err) + } + if config.HTTP.RosettaEnabled { + t.Errorf("Expected rosetta http server to be disabled when loading old config") + } + if config.Version != "1.0.0" { + t.Errorf("Expected config version: 1.0.0, not %v", config.Version) + } + config.Version = defaultConfig.Version // Shortcut for testing, value checked above + if !reflect.DeepEqual(config, defaultConfig) { + t.Errorf("Unexpected config \n\t%+v \n\t%+v", config, defaultConfig) + } +} + func TestPersistConfig(t *testing.T) { testDir := filepath.Join(testBaseDir, t.Name()) os.RemoveAll(testDir) diff --git a/cmd/harmony/default.go b/cmd/harmony/default.go index 95d76c1ab..3741a20c5 100644 --- a/cmd/harmony/default.go +++ b/cmd/harmony/default.go @@ -2,7 +2,7 @@ package main import nodeconfig "github.com/harmony-one/harmony/internal/configs/node" -const tomlConfigVersion = "1.0.0" +const tomlConfigVersion = "1.0.1" const ( defNetworkType = nodeconfig.Mainnet @@ -23,9 +23,11 @@ var defaultConfig = harmonyConfig{ KeyFile: "./.hmykey", }, HTTP: httpConfig{ - Enabled: true, - IP: "127.0.0.1", - Port: nodeconfig.DefaultRPCPort, + Enabled: true, + RosettaEnabled: false, + IP: "127.0.0.1", + Port: nodeconfig.DefaultRPCPort, + RosettaPort: nodeconfig.DefaultRosettaPort, }, WS: wsConfig{ Enabled: true, diff --git a/cmd/harmony/flags.go b/cmd/harmony/flags.go index 8bd39dd1d..e028106c4 100644 --- a/cmd/harmony/flags.go +++ b/cmd/harmony/flags.go @@ -45,8 +45,10 @@ var ( httpFlags = []cli.Flag{ httpEnabledFlag, + httpRosettaEnabledFlag, httpIPFlag, httpPortFlag, + httpRosettaPortFlag, } wsFlags = []cli.Flag{ @@ -396,10 +398,20 @@ var ( Usage: "rpc port to listen for HTTP requests", DefValue: defaultConfig.HTTP.Port, } + httpRosettaEnabledFlag = cli.BoolFlag{ + Name: "http.rosetta", + Usage: "enable HTTP / Rosetta requests", + DefValue: defaultConfig.HTTP.RosettaEnabled, + } + httpRosettaPortFlag = cli.IntFlag{ + Name: "http.rosetta.port", + Usage: "rosetta port to listen for HTTP requests", + DefValue: defaultConfig.HTTP.RosettaPort, + } ) func applyHTTPFlags(cmd *cobra.Command, config *harmonyConfig) { - var isRPCSpecified bool + var isRPCSpecified, isRosettaSpecified bool if cli.IsFlagChanged(cmd, httpIPFlag) { config.HTTP.IP = cli.GetStringFlagValue(cmd, httpIPFlag) @@ -411,6 +423,17 @@ func applyHTTPFlags(cmd *cobra.Command, config *harmonyConfig) { isRPCSpecified = true } + if cli.IsFlagChanged(cmd, httpRosettaPortFlag) { + config.HTTP.RosettaPort = cli.GetIntFlagValue(cmd, httpRosettaPortFlag) + isRosettaSpecified = true + } + + if cli.IsFlagChanged(cmd, httpRosettaEnabledFlag) { + config.HTTP.RosettaEnabled = cli.GetBoolFlagValue(cmd, httpRosettaEnabledFlag) + } else if isRosettaSpecified { + config.HTTP.RosettaEnabled = true + } + if cli.IsFlagChanged(cmd, httpEnabledFlag) { config.HTTP.Enabled = cli.GetBoolFlagValue(cmd, httpEnabledFlag) } else if isRPCSpecified { @@ -1028,7 +1051,8 @@ func applyLegacyMiscFlags(cmd *cobra.Command, config *harmonyConfig) { if cli.IsFlagChanged(cmd, legacyPortFlag) { legacyPort := cli.GetIntFlagValue(cmd, legacyPortFlag) config.P2P.Port = legacyPort - config.HTTP.Port = nodeconfig.GetHTTPPortFromBase(legacyPort) + config.HTTP.Port = nodeconfig.GetRPCHTTPPortFromBase(legacyPort) + config.HTTP.RosettaPort = nodeconfig.GetRosettaHTTPPortFromBase(legacyPort) config.WS.Port = nodeconfig.GetWSPortFromBase(legacyPort) } diff --git a/cmd/harmony/flags_test.go b/cmd/harmony/flags_test.go index e46ace61a..6a76c2b2e 100644 --- a/cmd/harmony/flags_test.go +++ b/cmd/harmony/flags_test.go @@ -54,9 +54,11 @@ func TestHarmonyFlags(t *testing.T) { KeyFile: defaultConfig.P2P.KeyFile, }, HTTP: httpConfig{ - Enabled: true, - IP: "127.0.0.1", - Port: 9500, + Enabled: true, + IP: "127.0.0.1", + Port: 9500, + RosettaEnabled: false, + RosettaPort: 9700, }, WS: wsConfig{ Enabled: true, @@ -324,25 +326,51 @@ func TestRPCFlags(t *testing.T) { { args: []string{"--http=false"}, expConfig: httpConfig{ - Enabled: false, - IP: defaultConfig.HTTP.IP, - Port: defaultConfig.HTTP.Port, + Enabled: false, + RosettaEnabled: false, + IP: defaultConfig.HTTP.IP, + Port: defaultConfig.HTTP.Port, + RosettaPort: defaultConfig.HTTP.RosettaPort, }, }, { args: []string{"--http.ip", "8.8.8.8", "--http.port", "9001"}, expConfig: httpConfig{ - Enabled: true, - IP: "8.8.8.8", - Port: 9001, + Enabled: true, + RosettaEnabled: false, + IP: "8.8.8.8", + Port: 9001, + RosettaPort: defaultConfig.HTTP.RosettaPort, + }, + }, + { + args: []string{"--http.ip", "8.8.8.8", "--http.port", "9001", "--http.rosetta.port", "10001"}, + expConfig: httpConfig{ + Enabled: true, + RosettaEnabled: true, + IP: "8.8.8.8", + Port: 9001, + RosettaPort: 10001, + }, + }, + { + args: []string{"--http.ip", "8.8.8.8", "--http.rosetta.port", "10001"}, + expConfig: httpConfig{ + Enabled: true, + RosettaEnabled: true, + IP: "8.8.8.8", + Port: defaultConfig.HTTP.Port, + RosettaPort: 10001, }, }, { args: []string{"--ip", "8.8.8.8", "--port", "9001", "--public_rpc"}, expConfig: httpConfig{ - Enabled: true, - IP: publicListenIP, - Port: 9501, + Enabled: true, + RosettaEnabled: false, + IP: publicListenIP, + Port: 9501, + RosettaPort: 9701, }, }, } diff --git a/cmd/harmony/main.go b/cmd/harmony/main.go index 170b45174..1cd6affc4 100644 --- a/cmd/harmony/main.go +++ b/cmd/harmony/main.go @@ -148,6 +148,11 @@ func getHarmonyConfig(cmd *cobra.Command) (harmonyConfig, error) { if err != nil { return harmonyConfig{}, err } + if config.Version != defaultConfig.Version { + fmt.Printf("Loaded config version %s which is not latest (%s).\n", + config.Version, defaultConfig.Version) + fmt.Println("Update saved config with `./harmony dumpconfig [config_file]`") + } applyRootFlags(cmd, &config) @@ -279,6 +284,13 @@ func setupNodeAndRun(hc harmonyConfig) { currentNode.SupportBeaconSyncing() } + // Parse rosetta config + nodeConfig.RosettaServer = nodeconfig.RosettaServerConfig{ + HTTPEnabled: hc.HTTP.RosettaEnabled, + HTTPIp: hc.HTTP.IP, + HTTPPort: hc.HTTP.RosettaPort, + } + if hc.Revert != nil && hc.Revert.RevertBefore != 0 && hc.Revert.RevertTo != 0 { chain := currentNode.Blockchain() if hc.Revert.RevertBeacon { @@ -327,6 +339,12 @@ func setupNodeAndRun(hc harmonyConfig) { Msg("StartRPC failed") } + if err := currentNode.StartRosetta(); err != nil { + utils.Logger().Warn(). + Err(err). + Msg("Start Rosetta failed") + } + if err := currentNode.BootstrapConsensus(); err != nil { fmt.Println("could not bootstrap consensus", err.Error()) os.Exit(-1) diff --git a/go.mod b/go.mod index 6dfa3a4df..b7a757b99 100644 --- a/go.mod +++ b/go.mod @@ -10,18 +10,19 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/cespare/cp v1.1.1 + github.com/coinbase/rosetta-sdk-go v0.3.4 github.com/davecgh/go-spew v1.1.1 github.com/davidlazar/go-crypto v0.0.0-20190912175916-7055855a373f // indirect github.com/deckarep/golang-set v1.7.1 github.com/edsrzf/mmap-go v1.0.0 // indirect - github.com/ethereum/go-ethereum v1.8.27 + github.com/ethereum/go-ethereum v1.9.18 github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a // indirect github.com/garslo/gogen v0.0.0-20170307003452-d6ebae628c7c // indirect github.com/golang/mock v1.4.0 github.com/golang/protobuf v1.4.0 github.com/golangci/golangci-lint v1.22.2 github.com/gorilla/handlers v1.4.0 // indirect - github.com/gorilla/mux v1.7.2 + github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 github.com/harmony-ek/gencodec v0.0.0-20190215044613-e6740dbdd846 github.com/harmony-one/abool v1.0.1 @@ -78,3 +79,5 @@ require ( gopkg.in/urfave/cli.v1 v1.20.0 // indirect gopkg.in/yaml.v2 v2.2.7 ) + +replace github.com/ethereum/go-ethereum => github.com/ethereum/go-ethereum v1.8.27 diff --git a/internal/configs/node/config.go b/internal/configs/node/config.go index a3d04595c..87d6a5f1d 100644 --- a/internal/configs/node/config.go +++ b/internal/configs/node/config.go @@ -68,15 +68,16 @@ var peerID peer.ID // PeerID of the node // ConfigType is the structure of all node related configuration variables type ConfigType struct { // The three groupID design, please refer to https://github.com/harmony-one/harmony/blob/master/node/node.md#libp2p-integration - beacon GroupID // the beacon group ID - group GroupID // the group ID of the shard (note: for beacon chain node, the beacon and shard group are the same) - client GroupID // the client group ID of the shard - isClient bool // whether this node is a client node, such as wallet - ShardID uint32 // ShardID of this node; TODO ek – revisit when resharding - role Role // Role of the node - Port string // Port of the node. - IP string // IP of the node. - RPCServer RPCServerConfig // RPC server port and ip + beacon GroupID // the beacon group ID + group GroupID // the group ID of the shard (note: for beacon chain node, the beacon and shard group are the same) + client GroupID // the client group ID of the shard + isClient bool // whether this node is a client node, such as wallet + ShardID uint32 // ShardID of this node; TODO ek – revisit when resharding + role Role // Role of the node + Port string // Port of the node. + IP string // IP of the node. + RPCServer RPCServerConfig // RPC server port and ip + RosettaServer RosettaServerConfig // rosetta server port and ip StringRole string P2PPriKey p2p_crypto.PrivKey ConsensusPriKey multibls.PrivateKeys @@ -102,6 +103,13 @@ type RPCServerConfig struct { WSPort int } +// RosettaServerConfig is the config for the rosetta server +type RosettaServerConfig struct { + HTTPEnabled bool + HTTPIp string + HTTPPort int +} + // configs is a list of node configuration. // It has at least one configuration. // The first one is the default, global node configuration diff --git a/internal/configs/node/network.go b/internal/configs/node/network.go index 47cb3754a..3ccc52940 100644 --- a/internal/configs/node/network.go +++ b/internal/configs/node/network.go @@ -47,6 +47,8 @@ const ( DefaultDNSPort = 9000 // DefaultRPCPort is the default rpc port. The actual port used is 9000+500 DefaultRPCPort = 9500 + // DefaultRosettaPort is the default rosetta port. The actual port used is 9000+700 + DefaultRosettaPort = 9700 // DefaultWSPort is the default port for web socket endpoint. The actual port used is DefaultWSPort = 9800 ) @@ -55,6 +57,9 @@ const ( // rpcHTTPPortOffset is the port offset for RPC HTTP requests rpcHTTPPortOffset = 500 + // rpcHTTPPortOffset is the port offset for rosetta HTTP requests + rosettaHTTPPortOffset = 700 + // rpcWSPortOffSet is the port offset for RPC websocket requests rpcWSPortOffSet = 800 ) @@ -100,11 +105,16 @@ func GetDefaultDNSPort(NetworkType) int { return DefaultDNSPort } -// GetHTTPPortFromBase return the HTTP port from base port -func GetHTTPPortFromBase(basePort int) int { +// GetRPCHTTPPortFromBase return the rpc HTTP port from base port +func GetRPCHTTPPortFromBase(basePort int) int { return basePort + rpcHTTPPortOffset } +// GetRosettaHTTPPortFromBase return the rosetta HTTP port from base port +func GetRosettaHTTPPortFromBase(basePort int) int { + return basePort + rosettaHTTPPortOffset +} + // GetWSPortFromBase return the Websocket port from the base port func GetWSPortFromBase(basePort int) int { return basePort + rpcWSPortOffSet diff --git a/node/rpc.go b/node/api.go similarity index 91% rename from node/rpc.go rename to node/api.go index cd2cc6407..d20a8251f 100644 --- a/node/rpc.go +++ b/node/api.go @@ -4,6 +4,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/hmy" + "github.com/harmony-one/harmony/rosetta" hmy_rpc "github.com/harmony-one/harmony/rpc" "github.com/harmony-one/harmony/rpc/filters" "github.com/libp2p/go-libp2p-core/peer" @@ -79,6 +80,12 @@ func (node *Node) StopRPC() error { return hmy_rpc.StopServers() } +// StartRosetta start rosetta service +func (node *Node) StartRosetta() error { + harmony := hmy.New(node, node.TxPool, node.CxPool, node.Consensus.ShardID) + return rosetta.StartServers(harmony, node.NodeConfig.RosettaServer) +} + // APIs return the collection of local RPC services. // NOTE, some of these services probably need to be moved to somewhere else. func (node *Node) APIs(harmony *hmy.Harmony) []rpc.API { diff --git a/rosetta/common/config.go b/rosetta/common/config.go new file mode 100644 index 000000000..ebcc9ce80 --- /dev/null +++ b/rosetta/common/config.go @@ -0,0 +1,75 @@ +package config + +import ( + "fmt" + "time" + + "github.com/coinbase/rosetta-sdk-go/types" + shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" + "github.com/harmony-one/harmony/rpc" + "github.com/harmony-one/harmony/shard" + staking "github.com/harmony-one/harmony/staking/types" +) + +const ( + // Blockchain .. + Blockchain = "Harmony" + + // Symbol .. + Symbol = "ONE" + + // Decimals .. + Decimals = 18 + + // CurveType .. + CurveType = types.Secp256k1 +) + +var ( + // TransactionTypes .. + TransactionTypes = []string{ + "Transfer", + "CrossShardTransfer", + staking.DirectiveCreateValidator.String(), + staking.DirectiveEditValidator.String(), + staking.DirectiveDelegate.String(), + staking.DirectiveUndelegate.String(), + staking.DirectiveCollectRewards.String(), + } + + // ReadTimeout .. + ReadTimeout = 30 * time.Second + + // WriteTimeout .. + WriteTimeout = 30 * time.Second + + // IdleTimeout .. + IdleTimeout = 120 * time.Second +) + +// ShardMetadata for the network identifier +type ShardMetadata struct { + IsBeacon bool `json:"isBeacon"` +} + +// GetNetwork fetches the networking identifier for the given shard +func GetNetwork(shardID uint32) *types.NetworkIdentifier { + metadata, _ := rpc.NewStructuredResponse(ShardMetadata{ + IsBeacon: shardID == shard.BeaconChainShardID, + }) + return &types.NetworkIdentifier{ + Blockchain: Blockchain, + Network: getNetworkName(), + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: fmt.Sprintf("shard %d", shardID), + Metadata: metadata, + }, + } +} + +func getNetworkName() string { + if shard.Schedule.GetNetworkID() == shardingconfig.MainNet { + return "Mainnet" + } + return "Testnet" +} diff --git a/rosetta/rosetta.go b/rosetta/rosetta.go new file mode 100644 index 000000000..d374861a1 --- /dev/null +++ b/rosetta/rosetta.go @@ -0,0 +1,87 @@ +package rosetta + +import ( + "fmt" + "net" + "net/http" + "time" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/server" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/harmony-one/harmony/hmy" + nodeconfig "github.com/harmony-one/harmony/internal/configs/node" + "github.com/harmony-one/harmony/internal/utils" + common "github.com/harmony-one/harmony/rosetta/common" + "github.com/harmony-one/harmony/rosetta/services" +) + +// StartServers starts the rosetta http server +func StartServers(hmy *hmy.Harmony, config nodeconfig.RosettaServerConfig) error { + if !config.HTTPEnabled { + utils.Logger().Info().Msg("Rosetta http server disabled...") + return nil + } + network := common.GetNetwork(hmy.ShardID) + + serverAsserter, err := asserter.NewServer( + common.TransactionTypes, + nodeconfig.GetDefaultConfig().Role() == nodeconfig.ExplorerNode, + []*types.NetworkIdentifier{network}, + ) + if err != nil { + return err + } + + router := server.CorsMiddleware(loggerMiddleware(getRouter(network, serverAsserter, hmy))) + utils.Logger().Info(). + Int("port", config.HTTPPort). + Str("ip", config.HTTPIp). + Msg("Starting Rosetta server") + + endpoint := fmt.Sprintf("%s:%d", config.HTTPIp, config.HTTPPort) + var ( + listener net.Listener + ) + if listener, err = net.Listen("tcp", endpoint); err != nil { + return err + } + go newHTTPServer(router).Serve(listener) + return nil +} + +func newHTTPServer(handler http.Handler) *http.Server { + return &http.Server{ + Handler: handler, + ReadTimeout: common.ReadTimeout, + WriteTimeout: common.WriteTimeout, + IdleTimeout: common.IdleTimeout, + } +} + +func getRouter( + network *types.NetworkIdentifier, + asserter *asserter.Asserter, + hmy *hmy.Harmony, +) http.Handler { + return server.NewRouter( + server.NewNetworkAPIController(services.NewNetworkAPIService(network, hmy), asserter), + server.NewBlockAPIController(services.NewBlockAPIService(network, hmy), asserter), + ) +} + +func loggerMiddleware(router http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + router.ServeHTTP(w, r) + msg := fmt.Sprintf( + "Rosetta: %s %s %s", + r.Method, + r.RequestURI, + time.Since(start), + ) + utils.Logger().Info().Msg(msg) + // Print to stdout for quick check of rosetta activity + fmt.Printf("%s %s\n", time.Now().Format("2006-01-02 15:04:05"), msg) + }) +} diff --git a/rosetta/services/block_service.go b/rosetta/services/block_service.go new file mode 100644 index 000000000..852d95d66 --- /dev/null +++ b/rosetta/services/block_service.go @@ -0,0 +1,158 @@ +package services + +import ( + "context" + "fmt" + "time" + + "github.com/coinbase/rosetta-sdk-go/server" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/harmony-one/harmony/hmy" +) + +// BlockAPIService implements the server.BlockAPIServicer interface. +type BlockAPIService struct { + hmy *hmy.Harmony + network *types.NetworkIdentifier +} + +// NewBlockAPIService creates a new instance of a BlockAPIService. +func NewBlockAPIService( + network *types.NetworkIdentifier, hmy *hmy.Harmony, +) server.BlockAPIServicer { + return &BlockAPIService{ + hmy: hmy, + network: network, + } +} + +// Block implements the /block endpoint (placeholder) +// FIXME: remove placeholder & implement block endpoint +func (s *BlockAPIService) Block( + ctx context.Context, + request *types.BlockRequest, +) (*types.BlockResponse, *types.Error) { + if *request.BlockIdentifier.Index != 1000 { + previousBlockIndex := *request.BlockIdentifier.Index - 1 + if previousBlockIndex < 0 { + previousBlockIndex = 0 + } + + return &types.BlockResponse{ + Block: &types.Block{ + BlockIdentifier: &types.BlockIdentifier{ + Index: *request.BlockIdentifier.Index, + Hash: fmt.Sprintf("block %d", *request.BlockIdentifier.Index), + }, + ParentBlockIdentifier: &types.BlockIdentifier{ + Index: previousBlockIndex, + Hash: fmt.Sprintf("block %d", previousBlockIndex), + }, + Timestamp: time.Now().UnixNano() / 1000000, + Transactions: []*types.Transaction{}, + }, + }, nil + } + + return &types.BlockResponse{ + Block: &types.Block{ + BlockIdentifier: &types.BlockIdentifier{ + Index: 1000, + Hash: "block 1000", + }, + ParentBlockIdentifier: &types.BlockIdentifier{ + Index: 999, + Hash: "block 999", + }, + Timestamp: 1586483189000, + Transactions: []*types.Transaction{ + { + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "transaction 0", + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "Transfer", + Status: "Success", + Account: &types.AccountIdentifier{ + Address: "account 0", + }, + Amount: &types.Amount{ + Value: "-1000", + Currency: &types.Currency{ + Symbol: "ROS", + Decimals: 2, + }, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: 0, + }, + }, + Type: "Transfer", + Status: "Reverted", + Account: &types.AccountIdentifier{ + Address: "account 1", + }, + Amount: &types.Amount{ + Value: "1000", + Currency: &types.Currency{ + Symbol: "ROS", + Decimals: 2, + }, + }, + }, + }, + }, + }, + }, + OtherTransactions: []*types.TransactionIdentifier{ + { + Hash: "transaction 1", + }, + }, + }, nil +} + +// BlockTransaction implements the /block/transaction endpoint (placeholder) +// FIXME: remove placeholder & implement block endpoint +func (s *BlockAPIService) BlockTransaction( + ctx context.Context, + request *types.BlockTransactionRequest, +) (*types.BlockTransactionResponse, *types.Error) { + return &types.BlockTransactionResponse{ + Transaction: &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "transaction 1", + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "Reward", + Status: "Success", + Account: &types.AccountIdentifier{ + Address: "account 2", + }, + Amount: &types.Amount{ + Value: "1000", + Currency: &types.Currency{ + Symbol: "ROS", + Decimals: 2, + }, + }, + }, + }, + }, + }, nil +} diff --git a/rosetta/services/network_service.go b/rosetta/services/network_service.go new file mode 100644 index 000000000..1b9f12cf9 --- /dev/null +++ b/rosetta/services/network_service.go @@ -0,0 +1,100 @@ +package services + +import ( + "context" + + "github.com/coinbase/rosetta-sdk-go/server" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/harmony-one/harmony/hmy" +) + +// NetworkAPIService implements the server.NetworkAPIServicer interface. +type NetworkAPIService struct { + hmy *hmy.Harmony + network *types.NetworkIdentifier +} + +// NewNetworkAPIService creates a new instance of a NetworkAPIService. +func NewNetworkAPIService( + network *types.NetworkIdentifier, hmy *hmy.Harmony, +) server.NetworkAPIServicer { + return &NetworkAPIService{ + hmy: hmy, + network: network, + } +} + +// NetworkList implements the /network/list endpoint (placeholder) +// FIXME: remove placeholder & implement block endpoint +func (s *NetworkAPIService) NetworkList( + ctx context.Context, + request *types.MetadataRequest, +) (*types.NetworkListResponse, *types.Error) { + return &types.NetworkListResponse{ + NetworkIdentifiers: []*types.NetworkIdentifier{ + s.network, + }, + }, nil +} + +// NetworkStatus implements the /network/status endpoint (placeholder) +// FIXME: remove placeholder & implement block endpoint +func (s *NetworkAPIService) NetworkStatus( + ctx context.Context, + request *types.NetworkRequest, +) (*types.NetworkStatusResponse, *types.Error) { + return &types.NetworkStatusResponse{ + CurrentBlockIdentifier: &types.BlockIdentifier{ + Index: 1000, + Hash: "block 1000", + }, + CurrentBlockTimestamp: int64(1586483189000), + GenesisBlockIdentifier: &types.BlockIdentifier{ + Index: 0, + Hash: "block 0", + }, + Peers: []*types.Peer{ + { + PeerID: "peer 1", + }, + }, + }, nil +} + +// NetworkOptions implements the /network/options endpoint (placeholder) +// FIXME: remove placeholder & implement block endpoint +func (s *NetworkAPIService) NetworkOptions( + ctx context.Context, + request *types.NetworkRequest, +) (*types.NetworkOptionsResponse, *types.Error) { + return &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: "1.4.0", + NodeVersion: "0.0.1", + }, + Allow: &types.Allow{ + OperationStatuses: []*types.OperationStatus{ + { + Status: "Success", + Successful: true, + }, + { + Status: "Reverted", + Successful: false, + }, + }, + OperationTypes: []string{ + "Transfer", + "Reward", + }, + Errors: []*types.Error{ + { + Code: 1, + Message: "not implemented", + Retriable: false, + }, + }, + }, + }, nil +} diff --git a/test/deploy.sh b/test/deploy.sh index f0a1a93f4..071189906 100755 --- a/test/deploy.sh +++ b/test/deploy.sh @@ -82,7 +82,7 @@ function launch_localnet() { # Read config for i-th node form config file IFS=' ' read -r ip port mode bls_key shard <<<"${line}" - args=("${base_args[@]}" --ip "${ip}" --port "${port}" --key "/tmp/${ip}-${port}.key" --db_dir "${ROOT}/db-${ip}-${port}" "--broadcast_invalid_tx=true") + args=("${base_args[@]}" --ip "${ip}" --port "${port}" --key "/tmp/${ip}-${port}.key" --db_dir "${ROOT}/db-${ip}-${port}" "--broadcast_invalid_tx=false") if [[ -z "$ip" || -z "$port" ]]; then echo "skip empty node" continue @@ -103,7 +103,7 @@ function launch_localnet() { # Setup flags for i-th node based on config case "${mode}" in explorer) - args=("${args[@]}" "--node_type=explorer" "--shard_id=${shard}") + args=("${args[@]}" "--node_type=explorer" "--shard_id=${shard}" "--http.rosetta=true") ;; archival) args=("${args[@]}" --is_archival --run.legacy)