rpc: support InitialGasDistribution response from old Neo-Go nodes

https://github.com/nspcc-dev/neo-go/pull/2435 breaks compatibility
between newer RPC clients and older RPC servers with the following
error:
```
failed to get network magic: json: cannot unmarshal string into Go struct field Protocol.protocol.initialgasdistribution of type int64
```

This behaviour is expected, but we can't allow this radical change.
Thus, the following solution is implemented:
1. RPC server responds with proper non-stringified
   InitialGasDistribution value. The value represents an integral
   of fixed8 multiplied by the decimals.
2. RPC client is able to distinguish older and newer responses. For
   older one the stringified value without decimals part is
   expected. For newer responses the int64 value with decimal part
   is expected.

The cludge will be present in the code for a while until nodes of
version <=0.98.3 become completely absolete.
This commit is contained in:
Anna Shaleva 2022-04-26 12:32:06 +03:00
parent c042c5bb63
commit 9862b40f2c
9 changed files with 316 additions and 14 deletions

View file

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"math" "math"
"strings" "strings"
@ -137,6 +138,7 @@ func newTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockch
} }
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test")
netSrv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), zap.NewNop()) netSrv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), zap.NewNop())
require.NoError(t, err) require.NoError(t, err)
cons, err := consensus.NewService(consensus.Config{ cons, err := consensus.NewService(consensus.Config{

1
go.mod
View file

@ -3,6 +3,7 @@ module github.com/nspcc-dev/neo-go
require ( require (
github.com/btcsuite/btcd v0.22.0-beta github.com/btcsuite/btcd v0.22.0-beta
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e 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/davecgh/go-spew v1.1.1
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/golang-lru v0.5.4

2
go.sum
View file

@ -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-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/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/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 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/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= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=

View file

@ -9,7 +9,14 @@ import (
"gopkg.in/yaml.v2" "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. // Version the version of the node, set at build time.
var Version string var Version string
@ -23,7 +30,7 @@ type Config struct {
// GenerateUserAgent creates user agent string based on build time environment. // GenerateUserAgent creates user agent string based on build time environment.
func (c Config) GenerateUserAgent() string { func (c Config) GenerateUserAgent() string {
return fmt.Sprintf(userAgentFormat, Version) return fmt.Sprintf(UserAgentFormat, Version)
} }
// Load attempts to load the config from the given // Load attempts to load the config from the given

View file

@ -1,6 +1,12 @@
package result package result
import ( 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/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
) )
@ -11,19 +17,47 @@ type (
Version struct { Version struct {
// Magic contains network magic. // Magic contains network magic.
// Deprecated: use Protocol.StateRootInHeader instead // Deprecated: use Protocol.StateRootInHeader instead
Magic netmode.Magic `json:"network"` Magic netmode.Magic
TCPPort uint16 `json:"tcpport"` TCPPort uint16
WSPort uint16 `json:"wsport,omitempty"` WSPort uint16
Nonce uint32 `json:"nonce"` Nonce uint32
UserAgent string `json:"useragent"` UserAgent string
Protocol Protocol `json:"protocol"` Protocol Protocol
// StateRootInHeader is true if state root is contained in block header. // StateRootInHeader is true if state root is contained in block header.
// Deprecated: use Protocol.StateRootInHeader instead // Deprecated: use Protocol.StateRootInHeader instead
StateRootInHeader bool `json:"staterootinheader,omitempty"` StateRootInHeader bool
} }
// Protocol represents network-dependent parameters. // Protocol represents network-dependent parameters.
Protocol struct { 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"` AddressVersion byte `json:"addressversion"`
Network netmode.Magic `json:"network"` Network netmode.Magic `json:"network"`
MillisecondsPerBlock int `json:"msperblock"` MillisecondsPerBlock int `json:"msperblock"`
@ -32,8 +66,117 @@ type (
MaxTransactionsPerBlock uint16 `json:"maxtransactionsperblock"` MaxTransactionsPerBlock uint16 `json:"maxtransactionsperblock"`
MemoryPoolMaxTransactions int `json:"memorypoolmaxtransactions"` MemoryPoolMaxTransactions int `json:"memorypoolmaxtransactions"`
ValidatorsCount byte `json:"validatorscount"` ValidatorsCount byte `json:"validatorscount"`
InitialGasDistribution fixedn.Fixed8 `json:"initialgasdistribution"` 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
}

View file

@ -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)
}
}
}

View file

@ -536,7 +536,7 @@ func (s *Server) getVersion(_ request.Params) (interface{}, *response.Error) {
} }
cfg := s.chain.GetConfig() cfg := s.chain.GetConfig()
return result.Version{ return &result.Version{
Magic: s.network, Magic: s.network,
TCPPort: port, TCPPort: port,
Nonce: s.coreServer.ID(), Nonce: s.coreServer.ID(),

View file

@ -1,6 +1,7 @@
package server package server
import ( import (
"fmt"
"math/big" "math/big"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -101,6 +102,7 @@ func initClearServerWithServices(t testing.TB, needOracle bool, needNotary bool)
chain, orc, cfg, logger := getUnitTestChain(t, needOracle, needNotary) chain, orc, cfg, logger := getUnitTestChain(t, needOracle, needNotary)
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test")
serverConfig.Port = 0 serverConfig.Port = 0
server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger) server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger)
require.NoError(t, err) require.NoError(t, err)

View file

@ -19,6 +19,7 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/internal/testserdes" "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"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "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{}) { check: func(t *testing.T, e *executor, ver interface{}) {
resp, ok := ver.(*result.Version) resp, ok := ver.(*result.Version)
require.True(t, ok) 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() cfg := e.chain.GetConfig()
require.EqualValues(t, address.NEO3Prefix, resp.Protocol.AddressVersion) 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) chain, orc, cfg, logger := getUnitTestChain(b, false, false)
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test")
serverConfig.LogLevel = zapcore.FatalLevel serverConfig.LogLevel = zapcore.FatalLevel
server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger) server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger)
require.NoError(b, err) require.NoError(b, err)