diff --git a/cli/executor_test.go b/cli/executor_test.go index 978568b09..a61b7f0a2 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "fmt" "io" "math" "strings" @@ -137,6 +138,7 @@ func newTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockch } serverConfig := network.NewServerConfig(cfg) + serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test") netSrv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), zap.NewNop()) require.NoError(t, err) cons, err := consensus.NewService(consensus.Config{ diff --git a/go.mod b/go.mod index aaceb2fa0..3fcdf5c56 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nspcc-dev/neo-go require ( github.com/btcsuite/btcd v0.22.0-beta github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e + github.com/coreos/go-semver v0.3.0 github.com/davecgh/go-spew v1.1.1 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/golang-lru v0.5.4 diff --git a/go.sum b/go.sum index 31641fef4..949627592 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/pkg/config/config.go b/pkg/config/config.go index 73d7f9ff8..bbaaf2cd6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,7 +9,14 @@ import ( "gopkg.in/yaml.v2" ) -const userAgentFormat = "/NEO-GO:%s/" +const ( + // UserAgentWrapper is a string that user agent string should be wrapped into. + UserAgentWrapper = "/" + // UserAgentPrefix is a prefix used to generate user agent string. + UserAgentPrefix = "NEO-GO:" + // UserAgentFormat is a formatted string used to generate user agent string. + UserAgentFormat = UserAgentWrapper + UserAgentPrefix + "%s" + UserAgentWrapper +) // Version the version of the node, set at build time. var Version string @@ -23,7 +30,7 @@ type Config struct { // GenerateUserAgent creates user agent string based on build time environment. func (c Config) GenerateUserAgent() string { - return fmt.Sprintf(userAgentFormat, Version) + return fmt.Sprintf(UserAgentFormat, Version) } // Load attempts to load the config from the given diff --git a/pkg/rpc/response/result/version.go b/pkg/rpc/response/result/version.go index 2dfa7122c..b2823a810 100644 --- a/pkg/rpc/response/result/version.go +++ b/pkg/rpc/response/result/version.go @@ -1,7 +1,14 @@ package result import ( + "encoding/json" + "fmt" + "strings" + + "github.com/coreos/go-semver/semver" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" ) type ( @@ -10,19 +17,47 @@ type ( Version struct { // Magic contains network magic. // Deprecated: use Protocol.StateRootInHeader instead - Magic netmode.Magic `json:"network"` - TCPPort uint16 `json:"tcpport"` - WSPort uint16 `json:"wsport,omitempty"` - Nonce uint32 `json:"nonce"` - UserAgent string `json:"useragent"` - Protocol Protocol `json:"protocol"` + Magic netmode.Magic + TCPPort uint16 + WSPort uint16 + Nonce uint32 + UserAgent string + Protocol Protocol // StateRootInHeader is true if state root is contained in block header. // Deprecated: use Protocol.StateRootInHeader instead - StateRootInHeader bool `json:"staterootinheader,omitempty"` + StateRootInHeader bool } // Protocol represents network-dependent parameters. Protocol struct { + AddressVersion byte + Network netmode.Magic + MillisecondsPerBlock int + MaxTraceableBlocks uint32 + MaxValidUntilBlockIncrement uint32 + MaxTransactionsPerBlock uint16 + MemoryPoolMaxTransactions int + ValidatorsCount byte + InitialGasDistribution fixedn.Fixed8 + // StateRootInHeader is true if state root is contained in block header. + StateRootInHeader bool + } +) + +type ( + // versionMarshallerAux is an auxiliary struct used for Version JSON marshalling. + versionMarshallerAux struct { + Magic netmode.Magic `json:"network"` + TCPPort uint16 `json:"tcpport"` + WSPort uint16 `json:"wsport,omitempty"` + Nonce uint32 `json:"nonce"` + UserAgent string `json:"useragent"` + Protocol protocolMarshallerAux `json:"protocol"` + StateRootInHeader bool `json:"staterootinheader,omitempty"` + } + + // protocolMarshallerAux is an auxiliary struct used for Protocol JSON marshalling. + protocolMarshallerAux struct { AddressVersion byte `json:"addressversion"` Network netmode.Magic `json:"network"` MillisecondsPerBlock int `json:"msperblock"` @@ -32,7 +67,116 @@ type ( MemoryPoolMaxTransactions int `json:"memorypoolmaxtransactions"` ValidatorsCount byte `json:"validatorscount"` InitialGasDistribution int64 `json:"initialgasdistribution"` - // StateRootInHeader is true if state root is contained in block header. - StateRootInHeader bool `json:"staterootinheader,omitempty"` + StateRootInHeader bool `json:"staterootinheader,omitempty"` + } + + // versionUnmarshallerAux is an auxiliary struct used for Version JSON unmarshalling. + versionUnmarshallerAux struct { + Magic netmode.Magic `json:"network"` + TCPPort uint16 `json:"tcpport"` + WSPort uint16 `json:"wsport,omitempty"` + Nonce uint32 `json:"nonce"` + UserAgent string `json:"useragent"` + Protocol protocolUnmarshallerAux `json:"protocol"` + StateRootInHeader bool `json:"staterootinheader,omitempty"` + } + + // protocolUnmarshallerAux is an auxiliary struct used for Protocol JSON unmarshalling. + protocolUnmarshallerAux struct { + AddressVersion byte `json:"addressversion"` + Network netmode.Magic `json:"network"` + MillisecondsPerBlock int `json:"msperblock"` + MaxTraceableBlocks uint32 `json:"maxtraceableblocks"` + MaxValidUntilBlockIncrement uint32 `json:"maxvaliduntilblockincrement"` + MaxTransactionsPerBlock uint16 `json:"maxtransactionsperblock"` + MemoryPoolMaxTransactions int `json:"memorypoolmaxtransactions"` + ValidatorsCount byte `json:"validatorscount"` + InitialGasDistribution json.RawMessage `json:"initialgasdistribution"` + StateRootInHeader bool `json:"staterootinheader,omitempty"` } ) + +// latestNonBreakingVersion is a latest NeoGo revision that keeps older RPC +// clients compatibility with newer RPC servers (https://github.com/nspcc-dev/neo-go/pull/2435). +var latestNonBreakingVersion = *semver.New("0.98.2") + +// MarshalJSON implements the json marshaller interface. +func (v *Version) MarshalJSON() ([]byte, error) { + aux := versionMarshallerAux{ + Magic: v.Magic, + TCPPort: v.TCPPort, + WSPort: v.WSPort, + Nonce: v.Nonce, + UserAgent: v.UserAgent, + Protocol: protocolMarshallerAux{ + AddressVersion: v.Protocol.AddressVersion, + Network: v.Protocol.Network, + MillisecondsPerBlock: v.Protocol.MillisecondsPerBlock, + MaxTraceableBlocks: v.Protocol.MaxTraceableBlocks, + MaxValidUntilBlockIncrement: v.Protocol.MaxValidUntilBlockIncrement, + MaxTransactionsPerBlock: v.Protocol.MaxTransactionsPerBlock, + MemoryPoolMaxTransactions: v.Protocol.MemoryPoolMaxTransactions, + ValidatorsCount: v.Protocol.ValidatorsCount, + InitialGasDistribution: int64(v.Protocol.InitialGasDistribution), + StateRootInHeader: v.Protocol.StateRootInHeader, + }, + StateRootInHeader: v.StateRootInHeader, + } + return json.Marshal(aux) +} + +// UnmarshalJSON implements the json unmarshaller interface. +func (v *Version) UnmarshalJSON(data []byte) error { + var aux versionUnmarshallerAux + err := json.Unmarshal(data, &aux) + if err != nil { + return err + } + v.Magic = aux.Magic + v.TCPPort = aux.TCPPort + v.WSPort = aux.WSPort + v.Nonce = aux.Nonce + v.UserAgent = aux.UserAgent + v.Protocol.AddressVersion = aux.Protocol.AddressVersion + v.Protocol.Network = aux.Protocol.Network + v.Protocol.MillisecondsPerBlock = aux.Protocol.MillisecondsPerBlock + v.Protocol.MaxTraceableBlocks = aux.Protocol.MaxTraceableBlocks + v.Protocol.MaxValidUntilBlockIncrement = aux.Protocol.MaxValidUntilBlockIncrement + v.Protocol.MaxTransactionsPerBlock = aux.Protocol.MaxTransactionsPerBlock + v.Protocol.MemoryPoolMaxTransactions = aux.Protocol.MemoryPoolMaxTransactions + v.Protocol.ValidatorsCount = aux.Protocol.ValidatorsCount + v.Protocol.StateRootInHeader = aux.Protocol.StateRootInHeader + v.StateRootInHeader = aux.StateRootInHeader + if len(aux.Protocol.InitialGasDistribution) == 0 { + return nil + } + + if strings.HasPrefix(v.UserAgent, config.UserAgentWrapper+config.UserAgentPrefix) { + ver, err := userAgentToVersion(v.UserAgent) + if err == nil && ver.Compare(latestNonBreakingVersion) <= 0 { + err := json.Unmarshal(aux.Protocol.InitialGasDistribution, &v.Protocol.InitialGasDistribution) + if err != nil { + return fmt.Errorf("failed to unmarshal InitialGASDistribution into fixed8: %w", err) + } + return nil + } + } + var val int64 + err = json.Unmarshal(aux.Protocol.InitialGasDistribution, &val) + if err != nil { + return fmt.Errorf("failed to unmarshal InitialGASDistribution into int64: %w", err) + } + v.Protocol.InitialGasDistribution = fixedn.Fixed8(val) + + return nil +} + +func userAgentToVersion(userAgent string) (*semver.Version, error) { + verStr := strings.Trim(userAgent, config.UserAgentWrapper) + verStr = strings.TrimPrefix(verStr, config.UserAgentPrefix) + ver, err := semver.NewVersion(verStr) + if err != nil { + return nil, fmt.Errorf("can't retrieve neo-go version from UserAgent: %w", err) + } + return ver, nil +} diff --git a/pkg/rpc/response/result/version_test.go b/pkg/rpc/response/result/version_test.go new file mode 100644 index 000000000..1c6ddfe10 --- /dev/null +++ b/pkg/rpc/response/result/version_test.go @@ -0,0 +1,143 @@ +package result + +import ( + "encoding/json" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" + "github.com/stretchr/testify/require" +) + +func TestVersion_MarshalUnmarshalJSON(t *testing.T) { + responseFromGoOld := `{ + "network": 860833102, + "nonce": 1677922561, + "protocol": { + "addressversion": 53, + "initialgasdistribution": "52000000", + "maxtraceableblocks": 2102400, + "maxtransactionsperblock": 512, + "maxvaliduntilblockincrement": 5760, + "memorypoolmaxtransactions": 50000, + "msperblock": 15000, + "network": 860833102, + "validatorscount": 7 + }, + "tcpport": 10333, + "useragent": "/NEO-GO:0.98.2/", + "wsport": 10334 + }` + responseFromGoNew := `{ + "network": 860833102, + "nonce": 1677922561, + "protocol": { + "addressversion": 53, + "initialgasdistribution": 5200000000000000, + "maxtraceableblocks": 2102400, + "maxtransactionsperblock": 512, + "maxvaliduntilblockincrement": 5760, + "memorypoolmaxtransactions": 50000, + "msperblock": 15000, + "network": 860833102, + "validatorscount": 7 + }, + "tcpport": 10333, + "useragent": "/NEO-GO:0.98.3/", + "wsport": 10334 + }` + responseFromSharp := `{ + "nonce": 1677922561, + "protocol": { + "addressversion": 53, + "initialgasdistribution": 5200000000000000, + "maxtraceableblocks": 2102400, + "maxtransactionsperblock": 512, + "maxvaliduntilblockincrement": 5760, + "memorypoolmaxtransactions": 50000, + "msperblock": 15000, + "network": 860833102, + "validatorscount": 7 + }, + "tcpport": 10333, + "useragent": "/Neo:3.1.0/", + "wsport": 10334 + }` + v := &Version{ + Magic: 860833102, + TCPPort: 10333, + WSPort: 10334, + Nonce: 1677922561, + UserAgent: "/NEO-GO:0.98.3/", + Protocol: Protocol{ + AddressVersion: 53, + Network: 860833102, + MillisecondsPerBlock: 15000, + MaxTraceableBlocks: 2102400, + MaxValidUntilBlockIncrement: 5760, + MaxTransactionsPerBlock: 512, + MemoryPoolMaxTransactions: 50000, + ValidatorsCount: 7, + // Unmarshalled InitialGasDistribution should always be a valid Fixed8 for both old and new clients. + InitialGasDistribution: fixedn.Fixed8FromInt64(52000000), + StateRootInHeader: false, + }, + StateRootInHeader: false, + } + t.Run("MarshalJSON", func(t *testing.T) { + actual, err := json.Marshal(v) + require.NoError(t, err) + require.JSONEq(t, responseFromGoNew, string(actual)) + }) + t.Run("UnmarshalJSON", func(t *testing.T) { + t.Run("Go node response", func(t *testing.T) { + t.Run("old RPC server", func(t *testing.T) { + actual := &Version{} + require.NoError(t, json.Unmarshal([]byte(responseFromGoOld), actual)) + expected := new(Version) + *expected = *v + expected.UserAgent = "/NEO-GO:0.98.2/" + require.Equal(t, expected, actual) + }) + t.Run("new RPC server", func(t *testing.T) { + actual := &Version{} + require.NoError(t, json.Unmarshal([]byte(responseFromGoNew), actual)) + require.Equal(t, v, actual) + }) + }) + t.Run("Sharp node response", func(t *testing.T) { + actual := &Version{} + require.NoError(t, json.Unmarshal([]byte(responseFromSharp), actual)) + expected := new(Version) + *expected = *v + expected.UserAgent = "/Neo:3.1.0/" + expected.Magic = 0 // No magic in C#. + require.Equal(t, expected, actual) + }) + }) +} + +func TestVersionFromUserAgent(t *testing.T) { + type testCase struct { + success bool + cmpWithBreaking int + } + var testcases = map[string]testCase{ + "/Neo:3.1.0/": {success: false}, + "/NEO-GO:0.98.4": {success: true, cmpWithBreaking: 1}, + "/NEO-GO:0.98.4-pre-12344/": {success: true, cmpWithBreaking: 1}, + "/NEO-GO:0.98.3/": {success: true, cmpWithBreaking: 1}, + "/NEO-GO:0.98.3-pre-123/": {success: true, cmpWithBreaking: 1}, + "/NEO-GO:0.98.2/": {success: true, cmpWithBreaking: 0}, + "/NEO-GO:0.98.2-pre-12345/": {success: true, cmpWithBreaking: -1}, + "/NEO-GO:123456": {success: false}, + } + for str, tc := range testcases { + ver, err := userAgentToVersion(str) + if tc.success { + require.NoError(t, err) + require.Equal(t, ver.Compare(latestNonBreakingVersion), tc.cmpWithBreaking) + } else { + require.Error(t, err) + } + } +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index d0f3062a2..d08758173 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -536,7 +536,7 @@ func (s *Server) getVersion(_ request.Params) (interface{}, *response.Error) { } cfg := s.chain.GetConfig() - return result.Version{ + return &result.Version{ Magic: s.network, TCPPort: port, Nonce: s.coreServer.ID(), @@ -551,7 +551,7 @@ func (s *Server) getVersion(_ request.Params) (interface{}, *response.Error) { MaxTransactionsPerBlock: cfg.MaxTransactionsPerBlock, MemoryPoolMaxTransactions: cfg.MemPoolSize, ValidatorsCount: byte(cfg.GetNumOfCNs(s.chain.BlockHeight())), - InitialGasDistribution: int64(cfg.InitialGASSupply), + InitialGasDistribution: cfg.InitialGASSupply, StateRootInHeader: cfg.StateRootInHeader, }, }, nil diff --git a/pkg/rpc/server/server_helper_test.go b/pkg/rpc/server/server_helper_test.go index 4ea76cc46..56bd4aadb 100644 --- a/pkg/rpc/server/server_helper_test.go +++ b/pkg/rpc/server/server_helper_test.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "math/big" "net/http" "net/http/httptest" @@ -101,6 +102,7 @@ func initClearServerWithServices(t testing.TB, needOracle bool, needNotary bool) chain, orc, cfg, logger := getUnitTestChain(t, needOracle, needNotary) serverConfig := network.NewServerConfig(cfg) + serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test") serverConfig.Port = 0 server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger) require.NoError(t, err) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 0b28ab710..868e19eb4 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -19,6 +19,7 @@ import ( "github.com/gorilla/websocket" "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/fee" @@ -849,7 +850,7 @@ var rpcTestCases = map[string][]rpcTestCase{ check: func(t *testing.T, e *executor, ver interface{}) { resp, ok := ver.(*result.Version) require.True(t, ok) - require.Equal(t, "/NEO-GO:/", resp.UserAgent) + require.Equal(t, "/NEO-GO:0.98.3-test/", resp.UserAgent) cfg := e.chain.GetConfig() require.EqualValues(t, address.NEO3Prefix, resp.Protocol.AddressVersion) @@ -2605,6 +2606,7 @@ func BenchmarkHandleIn(b *testing.B) { chain, orc, cfg, logger := getUnitTestChain(b, false, false) serverConfig := network.NewServerConfig(cfg) + serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test") serverConfig.LogLevel = zapcore.FatalLevel server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger) require.NoError(b, err)