diff --git a/CHANGELOG.md b/CHANGELOG.md index 463884b564..807591a143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,8 @@ Changelog for NeoFS Node - Graveyard from metabase (#3744) ### Updated -- `github.com/nspcc-dev/neofs-contract` module to `v0.25.2-0.20251219150129-498a820b9d6b` (#3670, #3746) +- `github.com/nspcc-dev/neo-go` module to `v0.114.1-0.20251222145711-e174185e133e` (#3742) +- `github.com/nspcc-dev/neofs-contract` module to `v0.25.2-0.20251223154102-15c76da7ea19` (#3670, #3746, #3742) - `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.16.0.20251203135706-86667929fbb8` (#3711) - `github.com/nspcc-dev/locode-db` module to `v0.8.2` (#3729) diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go index adf42f3413..5fb9cd7c13 100644 --- a/cmd/neofs-node/config.go +++ b/cmd/neofs-node/config.go @@ -39,6 +39,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/services/replicator" trustcontroller "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/controller" truststorage "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/storage" + "github.com/nspcc-dev/neofs-node/pkg/services/sidechain" "github.com/nspcc-dev/neofs-node/pkg/timers" "github.com/nspcc-dev/neofs-node/pkg/util" "github.com/nspcc-dev/neofs-node/pkg/util/state" @@ -172,6 +173,7 @@ type shared struct { control *controlSvc.Server metaService *meta.Meta + sidechain *sidechain.SideChain containerPayments *paymentChecker } diff --git a/cmd/neofs-node/config/meta/meta.go b/cmd/neofs-node/config/meta/meta.go index a3a0a3049c..df1574bb6c 100644 --- a/cmd/neofs-node/config/meta/meta.go +++ b/cmd/neofs-node/config/meta/meta.go @@ -1,6 +1,39 @@ package metaconfig +import ( + "time" + + "github.com/nspcc-dev/neofs-node/pkg/innerring/config" +) + // Meta contains configuration for Meta service. type Meta struct { - Path string `mapstructure:"path"` + // List of nodes' addresses to communicate with over Neo P2P protocol in + // 'host:port' format. + // + // Optional: by default, node runs as standalone. + SeedNodes []string `mapstructure:"seed_nodes"` + + // Storage configuration. Must be set using one of constructors like BoltDB. + // + // Required. + Storage config.Storage `mapstructure:"storage"` + + // Maximum time period (approximate) between two adjacent blocks, + // if used enables dynamic block time (contrary to TimePerBlock + // targeting for every block). + // + // Optional: not set by default. Must not be negative, must be + // bigger than TimePerBlock. + MaxTimePerBlock time.Duration `mapstructure:"max_time_per_block"` + + // Neo RPC service configuration. + // + // Optional: see RPC defaults. + RPC config.RPC `mapstructure:"rpc"` + + // P2P settings. + // + // Required. + P2P config.P2P `mapstructure:"p2p"` } diff --git a/cmd/neofs-node/main.go b/cmd/neofs-node/main.go index c7f060815f..e39d82d32c 100644 --- a/cmd/neofs-node/main.go +++ b/cmd/neofs-node/main.go @@ -138,6 +138,7 @@ func initApp(c *cfg) { initAndLog(c, "session", initSessionService) initAndLog(c, "reputation", initReputationService) initAndLog(c, "meta", initMeta) + initAndLog(c, "meta sidechain", initMeta_new) initAndLog(c, "object", initObjectService) initAndLog(c, "morph notifications", listenMorphNotifications) diff --git a/cmd/neofs-node/meta.go b/cmd/neofs-node/meta.go index ddd014d604..ce1cf30ddf 100644 --- a/cmd/neofs-node/meta.go +++ b/cmd/neofs-node/meta.go @@ -10,52 +10,50 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/core/container" "github.com/nspcc-dev/neofs-node/pkg/core/netmap" cntClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/container" - "github.com/nspcc-dev/neofs-node/pkg/services/meta" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" containerSDK "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" netmapsdk "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - "go.uber.org/zap" "golang.org/x/sync/errgroup" ) func initMeta(c *cfg) { - if c.cfgMorph.client == nil { - initMorphComponents(c) - } - - c.cfgMeta.network = &neofsNetwork{ - key: c.binPublicKey, - cnrClient: c.cCli, - containers: c.cnrSrc, - network: c.netMapSource, - header: c.cfgObject.getSvc, - } - - var err error - p := meta.Parameters{ - Logger: c.log.With(zap.String("service", "meta data")), - Network: c.cfgMeta.network, - Timeout: c.appCfg.FSChain.DialTimeout, - NeoEnpoints: c.appCfg.FSChain.Endpoints, - ContainerHash: c.containerSH, - NetmapHash: c.netmapSH, - RootPath: c.appCfg.Meta.Path, - } - if p.RootPath == "" { - p.RootPath = "metadata" - } - c.metaService, err = meta.New(p) - fatalOnErr(err) - - c.workers = append(c.workers, newWorkerFromFunc(func(ctx context.Context) { - err = c.metaService.Run(ctx) - if err != nil { - c.internalErr <- fmt.Errorf("meta data service error: %w", err) - } - })) + //if c.cfgMorph.client == nil { + // initMorphComponents(c) + //} + // + //c.cfgMeta.network = &neofsNetwork{ + // key: c.binPublicKey, + // cnrClient: c.cCli, + // containers: c.cnrSrc, + // network: c.netMapSource, + // header: c.cfgObject.getSvc, + //} + // + //var err error + //p := meta.Parameters{ + // Logger: c.log.With(zap.String("service", "metadata chain")), + // Network: c.cfgMeta.network, + // Timeout: c.appCfg.FSChain.DialTimeout, + // NeoEnpoints: c.appCfg.FSChain.Endpoints, + // ContainerHash: c.containerSH, + // NetmapHash: c.netmapSH, + // RootPath: c.appCfg.Meta.Path, + //} + //if p.RootPath == "" { + // p.RootPath = "metadata" + //} + //c.metaService, err = meta.New(p) + //fatalOnErr(err) + // + //c.workers = append(c.workers, newWorkerFromFunc(func(ctx context.Context) { + // err = c.metaService.Run(ctx) + // if err != nil { + // c.internalErr <- fmt.Errorf("meta data service error: %w", err) + // } + //})) } type neofsNetwork struct { diff --git a/cmd/neofs-node/meta_new.go b/cmd/neofs-node/meta_new.go new file mode 100644 index 0000000000..665ce641f0 --- /dev/null +++ b/cmd/neofs-node/meta_new.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" + "github.com/nspcc-dev/neofs-node/pkg/services/sidechain" + "go.uber.org/zap" +) + +func initMeta_new(c *cfg) { + l := c.log.With(zap.String("component", "metadata chain")) + + v, err := c.cfgMorph.client.GetVersion() + fatalOnErr(err) + + fsChainProtocol := v.Protocol + standByCommittee := make([]string, 0, len(v.Protocol.StandbyCommittee)) + for _, c := range v.Protocol.StandbyCommittee { + standByCommittee = append(standByCommittee, c.StringCompressed()) + } + + p2pCfg := c.appCfg.Meta.P2P + + var chainCfg = config.Config{ + ProtocolConfiguration: config.ProtocolConfiguration{ + CommitteeHistory: nil, + Genesis: config.Genesis{ + MaxTraceableBlocks: fsChainProtocol.MaxTraceableBlocks, + MaxValidUntilBlockIncrement: fsChainProtocol.MaxValidUntilBlockIncrement, + Roles: map[noderoles.Role]keys.PublicKeys{ + noderoles.P2PNotary: v.Protocol.StandbyCommittee, + noderoles.NeoFSAlphabet: v.Protocol.StandbyCommittee, + }, + TimePerBlock: time.Duration(fsChainProtocol.MillisecondsPerBlock) * time.Millisecond, + }, + Magic: fsChainProtocol.Network + 1, + InitialGASSupply: fsChainProtocol.InitialGasDistribution, + P2PNotaryRequestPayloadPoolSize: 1000, + MaxTraceableBlocks: fsChainProtocol.MaxTraceableBlocks, + MaxValidUntilBlockIncrement: fsChainProtocol.MaxValidUntilBlockIncrement, + P2PSigExtensions: true, + SeedList: c.appCfg.Meta.SeedNodes, + StandbyCommittee: standByCommittee, + StateRootInHeader: false, + MaxTimePerBlock: c.appCfg.Meta.MaxTimePerBlock, + ValidatorsCount: uint32(len(standByCommittee)), + ValidatorsHistory: nil, + VerifyTransactions: true, + }, + ApplicationConfiguration: config.ApplicationConfiguration{ + P2P: config.P2P{ + Addresses: p2pCfg.Listen, + AttemptConnPeers: int(p2pCfg.Peers.Attempts), + DialTimeout: p2pCfg.DialTimeout, + MaxPeers: int(p2pCfg.Peers.Max), + MinPeers: int(p2pCfg.Peers.Min), + PingInterval: p2pCfg.Ping.Interval, + PingTimeout: p2pCfg.Ping.Timeout, + ProtoTickInterval: p2pCfg.ProtoTickInterval, + }, + Oracle: config.OracleConfiguration{}, + P2PNotary: config.P2PNotary{}, + StateRoot: config.StateRoot{}, + NeoFSBlockFetcher: config.NeoFSBlockFetcher{}, + NeoFSStateFetcher: config.NeoFSStateFetcher{}, + }, + } + + var cfgDB dbconfig.DBConfiguration + cfgDB.Type = c.appCfg.Meta.Storage.Type + switch c.appCfg.Meta.Storage.Type { + case dbconfig.BoltDB: + cfgDB.BoltDBOptions.FilePath = c.appCfg.Meta.Storage.Path + case dbconfig.LevelDB: + cfgDB.LevelDBOptions.DataDirectoryPath = c.appCfg.Meta.Storage.Path + default: + panic(fmt.Sprintf("unsupported metadata storage type: %s", c.appCfg.Meta.Storage.Type)) + } + chainCfg.ApplicationConfiguration.DBConfiguration = cfgDB + + if len(c.appCfg.Meta.RPC.Listen) > 0 { + var ( + rpcConfig config.RPC + rpcCfgRead = c.appCfg.Meta.RPC + ) + + rpcConfig.BasicService = config.BasicService{ + Enabled: true, + Addresses: c.appCfg.Meta.RPC.Listen, + } + rpcConfig.MaxGasInvoke = fixedn.Fixed8FromInt64(int64(rpcCfgRead.MaxGasInvoke)) + rpcConfig.MaxIteratorResultItems = 100 + rpcConfig.MaxWebSocketClients = int(rpcCfgRead.MaxWebSocketClients) + rpcConfig.SessionEnabled = true + rpcConfig.SessionExpansionEnabled = true + rpcConfig.SessionPoolSize = int(rpcCfgRead.SessionPoolSize) + rpcConfig.StartWhenSynchronized = true + if tlsCfg := rpcCfgRead.TLS; tlsCfg.Enabled { + rpcConfig.TLSConfig.Enabled = true + rpcConfig.TLSConfig.Addresses = tlsCfg.Listen + rpcConfig.TLSConfig.CertFile = tlsCfg.CertFile + rpcConfig.TLSConfig.KeyFile = tlsCfg.KeyFile + } + + chainCfg.ApplicationConfiguration.RPC = rpcConfig + } + + applySidechainDefaults(&chainCfg) + + err = chainCfg.ProtocolConfiguration.Validate() + fatalOnErr(err) + + ch, err := sidechain.New(chainCfg, l, c.internalErr) + fatalOnErr(err) + + c.sidechain = ch + + c.workers = append(c.workers, &workerFromFunc{ + fn: func(ctx context.Context) { + err := ch.Run(ctx) + if err != nil { + c.internalErr <- err + } + }, + }) +} + +func applySidechainDefaults(cfg *config.Config) { + if cfg.ApplicationConfiguration.P2P.MaxPeers == 0 { + cfg.ApplicationConfiguration.P2P.MaxPeers = 100 + } + if cfg.ApplicationConfiguration.P2P.AttemptConnPeers == 0 { + cfg.ApplicationConfiguration.P2P.AttemptConnPeers = cfg.ApplicationConfiguration.P2P.MinPeers + 10 + } + if cfg.ApplicationConfiguration.P2P.DialTimeout == 0 { + cfg.ApplicationConfiguration.P2P.DialTimeout = time.Minute + } + if cfg.ApplicationConfiguration.P2P.ProtoTickInterval == 0 { + cfg.ApplicationConfiguration.P2P.ProtoTickInterval = 2 * time.Second + } + if cfg.ProtocolConfiguration.MaxTraceableBlocks == 0 { + cfg.ProtocolConfiguration.MaxTraceableBlocks = 17280 + } + if cfg.ProtocolConfiguration.MaxValidUntilBlockIncrement == 0 { + cfg.ProtocolConfiguration.MaxValidUntilBlockIncrement = 8640 + } + if cfg.ApplicationConfiguration.P2P.PingInterval == 0 { + cfg.ApplicationConfiguration.P2P.PingInterval = 30 * time.Second + } + if cfg.ApplicationConfiguration.P2P.PingTimeout == 0 { + cfg.ApplicationConfiguration.P2P.PingTimeout = time.Minute + } + if cfg.ApplicationConfiguration.RPC.MaxWebSocketClients == 0 { + cfg.ApplicationConfiguration.RPC.MaxWebSocketClients = 64 + } + if cfg.ApplicationConfiguration.RPC.SessionPoolSize == 0 { + cfg.ApplicationConfiguration.RPC.SessionPoolSize = 20 + } + if cfg.ApplicationConfiguration.RPC.MaxGasInvoke == 0 { + cfg.ApplicationConfiguration.RPC.MaxGasInvoke = 100 + } +} diff --git a/go.mod b/go.mod index 755d14b34f..bc43479d53 100644 --- a/go.mod +++ b/go.mod @@ -19,9 +19,9 @@ require ( github.com/nspcc-dev/bbolt v0.0.0-20250911202005-807225ebb0c8 github.com/nspcc-dev/hrw/v2 v2.0.4 github.com/nspcc-dev/locode-db v0.8.2 - github.com/nspcc-dev/neo-go v0.114.0 + github.com/nspcc-dev/neo-go v0.114.1-0.20251222145711-e174185e133e github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea - github.com/nspcc-dev/neofs-contract v0.25.2-0.20251219150129-498a820b9d6b + github.com/nspcc-dev/neofs-contract v0.25.2-0.20251223154102-15c76da7ea19 github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251203135706-86667929fbb8 github.com/nspcc-dev/tzhash v1.8.3 github.com/panjf2000/ants/v2 v2.11.3 @@ -70,7 +70,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nspcc-dev/dbft v0.4.0 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20250911084817-6fb4472993d1 // indirect - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 // indirect + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251217090505-857f951d81a9 // indirect github.com/nspcc-dev/rfc6979 v0.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect @@ -93,7 +93,9 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/go.sum b/go.sum index c69a1c65ef..270186dd65 100644 --- a/go.sum +++ b/go.sum @@ -191,14 +191,14 @@ github.com/nspcc-dev/hrw/v2 v2.0.4 h1:o3Zh/2aF+IgGpvt414f46Ya20WG9u9vWxVd16ErFI8 github.com/nspcc-dev/hrw/v2 v2.0.4/go.mod h1:dUjOx27zTTvoPmT5EG25vSSWL2tKS7ndAa2TPTiZwFo= github.com/nspcc-dev/locode-db v0.8.2 h1:+9+1Z7ppG+ISDLHzMND7PZ8+R4H3d04doVRyNevOpz0= github.com/nspcc-dev/locode-db v0.8.2/go.mod h1:PtAASXSG4D4Oz0js9elzTyTr8GLpOJO20qFL881Nims= -github.com/nspcc-dev/neo-go v0.114.0 h1:JxyLGlQGtzrfWvhdrUa35BGzBaadwPtLdNL5ehfOF2k= -github.com/nspcc-dev/neo-go v0.114.0/go.mod h1:visra3tXvGBgBfhMizRGEB+bUI5a/zoeqr5WQRKXFGQ= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 h1:9jH0IXFw8rjBgBVWSJbWeEHf7XDjANBnmEas49rdAH8= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go v0.114.1-0.20251222145711-e174185e133e h1:mdGTZLXefDo3zIm42z+XFBbSK3cWU6rYxlyfq1Ir0B8= +github.com/nspcc-dev/neo-go v0.114.1-0.20251222145711-e174185e133e/go.mod h1:2klaZUCv0Ut+6d4GAO3w9NbtS1bOAX0Sqc10CvWjhHI= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251217090505-857f951d81a9 h1:5+Ue5+i72uJVfHoq1+6mc6KlpriaqQaO96LtaDEsTfg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251217090505-857f951d81a9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK0EMGLvunXcFyq7fBURS/CsN4MH+4nlYiqn6pTwWAU= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y= -github.com/nspcc-dev/neofs-contract v0.25.2-0.20251219150129-498a820b9d6b h1:E8yWtvW2DrCEsCKFfvVRrfYu88DezqL4Hh/GPcSJXQI= -github.com/nspcc-dev/neofs-contract v0.25.2-0.20251219150129-498a820b9d6b/go.mod h1:CYX51uP2pNBCK7Q0ygD1LNsoFSHbB2F5luaBrluFkUo= +github.com/nspcc-dev/neofs-contract v0.25.2-0.20251223154102-15c76da7ea19 h1:SHs5o+wWqbdWbqQOlrONKSXhf8pDYdW+FnW04HTQvLg= +github.com/nspcc-dev/neofs-contract v0.25.2-0.20251223154102-15c76da7ea19/go.mod h1:9ziQViIqszec1SRE+mJX8gbAJmXEAY1g0ZE3TGVZYTc= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251203135706-86667929fbb8 h1:oasL8SD11yOmW0a/GqMmm4/0sb86hMOYMi7HC20PIb0= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251203135706-86667929fbb8/go.mod h1:IrM1JG/klBtecZEApIf8USgLonNcarv32R1O0dj4kQI= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= diff --git a/pkg/innerring/config/config.go b/pkg/innerring/config/config.go index 0c7ff38e85..2992c66cae 100644 --- a/pkg/innerring/config/config.go +++ b/pkg/innerring/config/config.go @@ -106,8 +106,8 @@ type Settlement struct { // Experimental configures experimental features. type Experimental struct { - ChainMetaData bool `mapstructure:"chain_meta_data"` - AllowEC bool `mapstructure:"allow_ec"` + ChainMetaData MetaChain `mapstructure:"chain_meta_data"` + AllowEC bool `mapstructure:"allow_ec"` } // Mainnet configures mainnet chain settings. diff --git a/pkg/innerring/config/metachain.go b/pkg/innerring/config/metachain.go new file mode 100644 index 0000000000..e0dd5096af --- /dev/null +++ b/pkg/innerring/config/metachain.go @@ -0,0 +1,38 @@ +package config + +import "time" + +// TODO +type MetaChain struct { + // Swith experimental meta-on-chain support. + Enabled bool `mapstructure:"enabled"` + + // List of nodes' addresses to communicate with over Neo P2P protocol in + // 'host:port' format. + // + // Optional: by default, node runs as standalone. + SeedNodes []string `mapstructure:"seed_nodes"` + + // Storage configuration. Must be set using one of constructors like BoltDB. + // + // Required. + Storage Storage `mapstructure:"storage"` + + // Maximum time period (approximate) between two adjacent blocks, + // if used enables dynamic block time (contrary to TimePerBlock + // targeting for every block). + // + // Optional: not set by default. Must not be negative, must be + // bigger than TimePerBlock. + MaxTimePerBlock time.Duration `mapstructure:"max_time_per_block"` + + // Neo RPC service configuration. + // + // Optional: see RPC defaults. + RPC RPC `mapstructure:"rpc"` + + // P2P settings. + // + // Required. + P2P P2P `mapstructure:"p2p"` +} diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index 541f24304f..bfa93a3584 100644 --- a/pkg/innerring/innerring.go +++ b/pkg/innerring/innerring.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "sync/atomic" + "time" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -16,6 +17,7 @@ import ( "github.com/nspcc-dev/neofs-node/misc" "github.com/nspcc-dev/neofs-node/pkg/innerring/config" "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/blockchain" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain" "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/alphabet" "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/balance" "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/container" @@ -55,7 +57,8 @@ type ( Server struct { log *zap.Logger - bc *blockchain.Blockchain + bc *blockchain.Blockchain + metaChain *blockchain.Blockchain // event producers fsChainListener event.Listener @@ -92,7 +95,7 @@ type ( netmapProcessor *netmap.Processor containerProcessor *container.Processor - workers []func(context.Context) + workers []func(context.Context) error // Set of local resources that must be // initialized at the very beginning of @@ -784,7 +787,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *config.Config, errChan chan< AlphabetState: server, ContainerClient: cnrClient, NetworkState: server.netmapClient, - MetaEnabled: cfg.Experimental.ChainMetaData, + MetaEnabled: cfg.Experimental.ChainMetaData.Enabled, AllowEC: cfg.Experimental.AllowEC, }) if err != nil { @@ -887,6 +890,40 @@ func New(ctx context.Context, log *zap.Logger, cfg *config.Config, errChan chan< initTimers(server, cfg, settlementProcessor) + if cfg.Experimental.ChainMetaData.Enabled { + v, err := server.fsChainClient.GetVersion() + if err != nil { + return nil, fmt.Errorf("fetchin FS chain version: %w", err) + } + fsChainProtocol := v.Protocol + metaChainCfg := config.Consensus{ + Storage: cfg.Experimental.ChainMetaData.Storage, + SeedNodes: cfg.Experimental.ChainMetaData.SeedNodes, + RPC: cfg.Experimental.ChainMetaData.RPC, + P2P: cfg.Experimental.ChainMetaData.P2P, + MaxTimePerBlock: cfg.Experimental.ChainMetaData.MaxTimePerBlock, + + Magic: uint32(fsChainProtocol.Network) + 1, + Committee: fsChainProtocol.StandbyCommittee, + TimePerBlock: time.Duration(fsChainProtocol.MillisecondsPerBlock) * time.Millisecond, + MaxTraceableBlocks: fsChainProtocol.MaxTraceableBlocks, + MaxValidUntilBlockIncrement: fsChainProtocol.MaxValidUntilBlockIncrement, + + Hardforks: config.Hardforks{}, + ValidatorsHistory: config.ValidatorsHistory{}, + SetRolesInGenesis: true, + KeepOnlyLatestState: false, + RemoveUntraceableBlocks: false, + P2PNotaryRequestPayloadPoolSize: 1000, // default for blockchain.New() + } + + server.metaChain, err = metachain.NewMetaChain(&metaChainCfg, &cfg.Wallet, errChan, log.With(zap.String("component", "metadata chain"))) + if err != nil { + return nil, fmt.Errorf("init meta sidechain blockchain: %w", err) + } + server.workers = append(server.workers, server.metaChain.Run) + } + return server, nil } diff --git a/pkg/innerring/internal/blockchain/blockchain.go b/pkg/innerring/internal/blockchain/blockchain.go index 140e4f258a..bd02dbd270 100644 --- a/pkg/innerring/internal/blockchain/blockchain.go +++ b/pkg/innerring/internal/blockchain/blockchain.go @@ -11,6 +11,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/consensus" "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" @@ -48,7 +49,7 @@ type Blockchain struct { // New returns new Blockchain configured by the specified Config. New panics if // any required Config field is zero or unset. Resulting Blockchain is ready to // run. Launched Blockchain should be finally stopped. -func New(cfg *config.Consensus, wallet *config.Wallet, errChan chan<- error, log *zap.Logger) (res *Blockchain, err error) { +func New(cfg *config.Consensus, wallet *config.Wallet, errChan chan<- error, log *zap.Logger, customNatives ...func(cfg neogoconfig.ProtocolConfiguration) []interop.Contract) (res *Blockchain, err error) { switch { case cfg.Storage.Type == "": panic("uninitialized storage config") @@ -259,7 +260,7 @@ func New(cfg *config.Consensus, wallet *config.Wallet, errChan chan<- error, log } }() - bc, err := core.NewBlockchain(bcStorage, cfgBase.Blockchain(), log) + bc, err := core.NewBlockchain(bcStorage, cfgBase.Blockchain(), log, customNatives...) if err != nil { return nil, fmt.Errorf("init core blockchain component: %w", err) } diff --git a/pkg/innerring/internal/metachain/chain.go b/pkg/innerring/internal/metachain/chain.go new file mode 100644 index 0000000000..7cd847f2a2 --- /dev/null +++ b/pkg/innerring/internal/metachain/chain.go @@ -0,0 +1,14 @@ +package metachain + +import ( + "github.com/nspcc-dev/neofs-node/pkg/innerring/config" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/blockchain" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/contracts" + "go.uber.org/zap" +) + +// NewMetaChain returns side chain with redefined/custom native contracts. +// See [contracts.NewCustomNatives] for details. +func NewMetaChain(cfg *config.Consensus, wallet *config.Wallet, errChan chan<- error, log *zap.Logger) (*blockchain.Blockchain, error) { + return blockchain.New(cfg, wallet, errChan, log, contracts.NewCustomNatives) +} diff --git a/pkg/innerring/internal/metachain/contracts/contracts.go b/pkg/innerring/internal/metachain/contracts/contracts.go new file mode 100644 index 0000000000..710746526a --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/contracts.go @@ -0,0 +1,45 @@ +package contracts + +import ( + "fmt" + + neogoconfig "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/contracts/gas" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/contracts/meta" +) + +// NewCustomNatives returns custom list of native contracts for metadata +// side chain. Returned contracts: +// - NEO +// - Management +// - Ledger +// - Policy +// - Designate +// - Notary +// - redefined GAS (see [gas.NewGAS] for details) +// - new native metadata contract (see [meta.NewMetadata] for details). +func NewCustomNatives(cfg neogoconfig.ProtocolConfiguration) []interop.Contract { + var ( + defaultContracts = native.NewDefaultContracts(cfg) + newContracts = make([]interop.Contract, 0) + + neoContract native.INEO + ) + for _, contract := range defaultContracts { + switch contract.(type) { + case *native.NEO: + neoContract = contract.(native.INEO) + newContracts = append(newContracts, neoContract) + case *native.GAS: + newContracts = append(newContracts, gas.NewGAS(int64(cfg.InitialGASSupply))) + case *native.Management, *native.Ledger, *native.Policy, *native.Designate, *native.Notary: + newContracts = append(newContracts, contract) + case *native.Std, *native.Crypto, *native.Oracle, *native.Treasury: + default: + panic(fmt.Sprintf("unexpected native contract found: %T", contract)) + } + } + return append(newContracts, meta.NewMetadata(neoContract)) +} diff --git a/pkg/innerring/internal/metachain/contracts/gas/gas.go b/pkg/innerring/internal/metachain/contracts/gas/gas.go new file mode 100644 index 0000000000..936618f299 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/gas/gas.go @@ -0,0 +1,104 @@ +package gas + +import ( + "errors" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativeids" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// DefaultBalance is a balance of every account in redefined [GAS] native +// contract +const DefaultBalance = 100 + +// GAS represents GAS custom native contract. It always returns [DefaultBalance] as a +// balance, has no-op `Burn`, `Mint`, `Transfer` operations. +type GAS struct { + nep17TokenNative + initialSupply int64 +} + +// NewGAS returns [GAS] custom native contract. +func NewGAS(init int64) *GAS { + g := &GAS{ + initialSupply: init, + } + defer g.BuildHFSpecificMD(g.ActiveIn()) + + nep17 := newNEP17Native(nativenames.Gas, nativeids.GasToken, nil) + nep17.symbol = "GAS" + nep17.decimals = 8 + nep17.factor = native.GASFactor + + g.nep17TokenNative = *nep17 + + return g +} + +// Initialize initializes a GAS contract. +func (g *GAS) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != g.ActiveIn() { + return nil + } + + if err := g.nep17TokenNative.Initialize(ic); err != nil { + return err + } + _, totalSupply := g.getTotalSupply(ic.DAO) + if totalSupply.Sign() != 0 { + return errors.New("already initialized") + } + h, err := getStandbyValidatorsHash(ic) + if err != nil { + return err + } + g.Mint(ic, h, big.NewInt(g.initialSupply), false) + return nil +} + +// InitializeCache implements the Contract interface. +func (g *GAS) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + return nil +} + +// OnPersist implements the Contract interface. +func (g *GAS) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements the Contract interface. +func (g *GAS) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn implements the Contract interface. +func (g *GAS) ActiveIn() *config.Hardfork { + return nil +} + +// BalanceOf returns native GAS token balance for the acc. +func (g *GAS) BalanceOf(d *dao.Simple, acc util.Uint160) *big.Int { + return g.balanceOfInternal(d, acc) +} + +func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) { + cfg := ic.Chain.GetConfig() + committee, err := keys.NewPublicKeysFromStrings(cfg.StandbyCommittee) + if err != nil { + return util.Uint160{}, err + } + s, err := smartcontract.CreateDefaultMultiSigRedeemScript(committee[:cfg.GetNumOfCNs(0)]) + if err != nil { + return util.Uint160{}, err + } + return hash.Hash160(s), nil +} diff --git a/pkg/innerring/internal/metachain/contracts/gas/gas_test.go b/pkg/innerring/internal/metachain/contracts/gas/gas_test.go new file mode 100644 index 0000000000..f10e0523c2 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/gas/gas_test.go @@ -0,0 +1,44 @@ +package gas_test + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/contracts" +) + +func newGasClient(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker) { + ch, validators, committee := chain.NewMultiWithOptions(t, &chain.Options{ + NewNatives: contracts.NewCustomNatives, + }) + e := neotest.NewExecutor(t, ch, validators, committee) + + return e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)), e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas)) +} + +const defaultBalance = 100 + +func TestGAS(t *testing.T) { + gasValidatorsI, gasCommitteeI := newGasClient(t) + hardcodedBalance := stackitem.NewBigInteger(big.NewInt(defaultBalance * native.GASFactor)) + + t.Run("committee balance", func(t *testing.T) { + gasCommitteeI.Invoke(t, hardcodedBalance, "balanceOf", gasCommitteeI.Hash) + }) + + t.Run("new account balance", func(t *testing.T) { + s := gasValidatorsI.NewAccount(t, defaultBalance*native.GASFactor+1) + gasCommitteeI.WithSigners(s).Invoke(t, hardcodedBalance, "balanceOf", s.ScriptHash()) + }) + + t.Run("transfer does not change balance", func(t *testing.T) { + newAcc := gasValidatorsI.NewAccount(t, defaultBalance*native.GASFactor+1) + gasCommitteeI.Invoke(t, stackitem.Bool(true), "transfer", gasCommitteeI.Hash, newAcc.ScriptHash(), 1, stackitem.Null{}) + gasCommitteeI.Invoke(t, hardcodedBalance, "balanceOf", newAcc.ScriptHash()) + }) +} diff --git a/pkg/innerring/internal/metachain/contracts/gas/nep17.go b/pkg/innerring/internal/metachain/contracts/gas/nep17.go new file mode 100644 index 0000000000..942cad4a22 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/gas/nep17.go @@ -0,0 +1,215 @@ +package gas + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// nep17TokenNative represents a NEP-17 token contract. +type nep17TokenNative struct { + interop.ContractMD + symbol string + decimals int64 + factor int64 +} + +// totalSupplyKey is the key used to store totalSupply value. +var totalSupplyKey = []byte{11} + +func (c *nep17TokenNative) Metadata() *interop.ContractMD { + return &c.ContractMD +} + +func newNEP17Native(name string, id int32, onManifestConstruction func(m *manifest.Manifest, hf config.Hardfork)) *nep17TokenNative { + n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name, id, func(m *manifest.Manifest, hf config.Hardfork) { + m.SupportedStandards = []string{manifest.NEP17StandardName} + if onManifestConstruction != nil { + onManifestConstruction(m, hf) + } + })} + + desc := NewDescriptor("symbol", smartcontract.StringType) + md := NewMethodAndPrice(n.Symbol, 0, callflag.NoneFlag) + n.AddMethod(md, desc) + + desc = NewDescriptor("decimals", smartcontract.IntegerType) + md = NewMethodAndPrice(n.Decimals, 0, callflag.NoneFlag) + n.AddMethod(md, desc) + + desc = NewDescriptor("totalSupply", smartcontract.IntegerType) + md = NewMethodAndPrice(n.TotalSupply, 1<<15, callflag.ReadStates) + n.AddMethod(md, desc) + + desc = NewDescriptor("balanceOf", smartcontract.IntegerType, + manifest.NewParameter("account", smartcontract.Hash160Type)) + md = NewMethodAndPrice(n.balanceOf, 1<<15, callflag.ReadStates) + n.AddMethod(md, desc) + + transferParams := []manifest.Parameter{ + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("to", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + } + desc = NewDescriptor("transfer", smartcontract.BoolType, + append(transferParams, manifest.NewParameter("data", smartcontract.AnyType))..., + ) + md = NewMethodAndPrice(n.Transfer, 1<<17, callflag.States|callflag.AllowCall|callflag.AllowNotify) + md.StorageFee = 50 + n.AddMethod(md, desc) + + eDesc := NewEventDescriptor("Transfer", transferParams...) + eMD := NewEvent(eDesc) + n.AddEvent(eMD) + + return n +} + +func (c *nep17TokenNative) Initialize(_ *interop.Context) error { + return nil +} + +func (c *nep17TokenNative) Symbol(_ *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewByteArray([]byte(c.symbol)) +} + +func (c *nep17TokenNative) Decimals(_ *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(c.decimals)) +} + +func (c *nep17TokenNative) TotalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + _, supply := c.getTotalSupply(ic.DAO) + return stackitem.NewBigInteger(supply) +} + +func (c *nep17TokenNative) getTotalSupply(d *dao.Simple) (state.StorageItem, *big.Int) { + si := d.GetStorageItem(c.ID, totalSupplyKey) + if si == nil { + si = []byte{} + } + return si, bigint.FromBytes(si) +} + +func (c *nep17TokenNative) Transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return stackitem.NewBool(true) +} + +func addrToStackItem(u *util.Uint160) stackitem.Item { + if u == nil { + return stackitem.Null{} + } + return stackitem.NewByteArray(u.BytesBE()) +} + +func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int, + data stackitem.Item, callOnPayment bool, postCalls ...func()) { +} + +func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) error { + return ic.AddNotification(c.Hash, "Transfer", stackitem.NewArray([]stackitem.Item{ + addrToStackItem(from), + addrToStackItem(to), + stackitem.NewBigInteger(amount), + })) +} + +// TransferInternal transfers NEO across accounts. +func (c *nep17TokenNative) TransferInternal(ic *interop.Context, from, to util.Uint160, amount *big.Int, data stackitem.Item) error { + return nil +} + +// balanceOf is the only difference with default native GAS implementation: +// it always returns fixed number of tokens. +func (c *nep17TokenNative) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(DefaultBalance * native.GASFactor)) +} + +func (c *nep17TokenNative) balanceOfInternal(d *dao.Simple, h util.Uint160) *big.Int { + return big.NewInt(DefaultBalance * native.GASFactor) +} + +func (c *nep17TokenNative) Mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) { + return +} + +func (c *nep17TokenNative) Burn(ic *interop.Context, h util.Uint160, amount *big.Int) { + return +} + +func NewDescriptor(name string, ret smartcontract.ParamType, ps ...manifest.Parameter) *manifest.Method { + if len(ps) == 0 { + ps = []manifest.Parameter{} + } + return &manifest.Method{ + Name: name, + Parameters: ps, + ReturnType: ret, + } +} + +// NewMethodAndPrice builds method with the provided descriptor and ActiveFrom/ActiveTill hardfork +// values consequently specified via activations. [config.HFDefault] specified as ActiveFrom is treated +// as active starting from the genesis block. +func NewMethodAndPrice(f interop.Method, cpuFee int64, flags callflag.CallFlag, activations ...config.Hardfork) *interop.MethodAndPrice { + md := &interop.MethodAndPrice{ + HFSpecificMethodAndPrice: interop.HFSpecificMethodAndPrice{ + Func: f, + CPUFee: cpuFee, + RequiredFlags: flags, + }, + } + if len(activations) > 0 { + if activations[0] != config.HFDefault { + md.ActiveFrom = &activations[0] + } + } + if len(activations) > 1 { + md.ActiveTill = &activations[1] + } + return md +} + +func NewEventDescriptor(name string, ps ...manifest.Parameter) *manifest.Event { + if len(ps) == 0 { + ps = []manifest.Parameter{} + } + return &manifest.Event{ + Name: name, + Parameters: ps, + } +} + +// NewEvent builds event with the provided descriptor and ActiveFrom/ActiveTill hardfork +// values consequently specified via activations. +func NewEvent(desc *manifest.Event, activations ...config.Hardfork) interop.Event { + md := interop.Event{ + HFSpecificEvent: interop.HFSpecificEvent{ + MD: desc, + }, + } + if len(activations) > 0 { + md.ActiveFrom = &activations[0] + } + if len(activations) > 1 { + md.ActiveTill = &activations[1] + } + return md +} + +// makeUint160Key creates a key from the account script hash. +func makeUint160Key(prefix byte, h util.Uint160) []byte { + k := make([]byte, util.Uint160Size+1) + k[0] = prefix + copy(k[1:], h.BytesBE()) + return k +} diff --git a/pkg/innerring/internal/metachain/contracts/meta/const.go b/pkg/innerring/internal/metachain/contracts/meta/const.go new file mode 100644 index 0000000000..5389f38f88 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/meta/const.go @@ -0,0 +1,21 @@ +package meta + +import ( + "math" +) + +const ( + // Metadata contract identifiers. + MetaDataContractID = math.MinInt32 + MetaDataContractName = "MetaData" + + // event names + putObjectEvent = "ObjectPut" + + // storage prefixes + metaContainersPrefix = 0x01 + containerPlacementPrefix = 0x02 + + // limits + maxREPsClauses = 255 +) diff --git a/pkg/innerring/internal/metachain/contracts/meta/meta.go b/pkg/innerring/internal/metachain/contracts/meta/meta.go new file mode 100644 index 0000000000..fbab683119 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/meta/meta.go @@ -0,0 +1,85 @@ +package meta + +import ( + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" +) + +var ( + _ interop.Contract = (*MetaData)(nil) +) + +// MetaData is a native contract for processing NeoFS objects meta data. +type MetaData struct { + interop.ContractMD + + neo native.INEO +} + +func (m *MetaData) Initialize(_ *interop.Context, _ *config.Hardfork, _ *interop.HFSpecificContractMD) error { + return nil +} + +func (m *MetaData) ActiveIn() *config.Hardfork { + return nil +} + +func (m *MetaData) InitializeCache(_ interop.IsHardforkEnabled, _ uint32, _ *dao.Simple) error { + return nil +} + +func (m *MetaData) Metadata() *interop.ContractMD { + return &m.ContractMD +} + +func (m *MetaData) OnPersist(_ *interop.Context) error { + return nil +} + +func (m *MetaData) PostPersist(_ *interop.Context) error { + return nil +} + +// NewMetadata returns native [MetaData] native contract. +func NewMetadata(neo native.INEO) *MetaData { + m := &MetaData{neo: neo} + defer m.BuildHFSpecificMD(m.ActiveIn()) + m.ContractMD = *interop.NewContractMD(MetaDataContractName, MetaDataContractID) + + desc := native.NewDescriptor("submitObjectPut", smartcontract.VoidType, + manifest.NewParameter("metaInformation", smartcontract.ByteArrayType), + manifest.NewParameter("signatures", smartcontract.ArrayType)) + md := native.NewMethodAndPrice(m.submitObjectPut, 1<<15, callflag.States|callflag.AllowNotify) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("registerMetaContainer", smartcontract.VoidType, + manifest.NewParameter("cID", smartcontract.Hash256Type)) + md = native.NewMethodAndPrice(m.registerMetaContainer, 1<<15, callflag.WriteStates) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("unregisterMetaContainer", smartcontract.VoidType, + manifest.NewParameter("cID", smartcontract.Hash256Type)) + md = native.NewMethodAndPrice(m.unregisterMetaContainer, 1<<15, callflag.WriteStates) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("updateContainerList", smartcontract.VoidType, + manifest.NewParameter("cID", smartcontract.Hash256Type), + manifest.NewParameter("vectors", smartcontract.ArrayType)) + md = native.NewMethodAndPrice(m.updateContainerList, 1<<15, callflag.WriteStates) + m.AddMethod(md, desc) + + eDesc := native.NewEventDescriptor(putObjectEvent, + manifest.NewParameter("ContainerID", smartcontract.Hash256Type), + manifest.NewParameter("ObjectID", smartcontract.Hash256Type), + manifest.NewParameter("Meta", smartcontract.MapType), + ) + eMD := native.NewEvent(eDesc) + m.AddEvent(eMD) + + return m +} diff --git a/pkg/innerring/internal/metachain/contracts/meta/meta_test.go b/pkg/innerring/internal/metachain/contracts/meta/meta_test.go new file mode 100644 index 0000000000..c70d5b1528 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/meta/meta_test.go @@ -0,0 +1,302 @@ +package meta_test + +import ( + "fmt" + "math" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/contracts" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/contracts/meta" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + "github.com/stretchr/testify/require" +) + +const ( + metaContainersPrefix = 0x01 + containerPlacementPrefix = 0x02 +) + +func newMetaClient(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker) { + ch, validators, committee := chain.NewMultiWithOptions(t, &chain.Options{ + NewNatives: contracts.NewCustomNatives, + }) + e := neotest.NewExecutor(t, ch, validators, committee) + + return e.ValidatorInvoker(e.NativeHash(t, meta.MetaDataContractName)), e.CommitteeInvoker(e.NativeHash(t, meta.MetaDataContractName)) +} + +func TestMetaDataContract_Containers(t *testing.T) { + metaValidatorsI, metaCommitteeI := newMetaClient(t) + cID := cidtest.ID() + key := append([]byte{metaContainersPrefix}, cID[:]...) + + metaValidatorsI.WithSigners(metaValidatorsI.NewAccount(t)).InvokeFail(t, native.ErrInvalidWitness.Error(), "registerMetaContainer", cID[:]) + + coldVal := metaValidatorsI.Chain.GetStorageItem(meta.MetaDataContractID, key) + require.Nil(t, coldVal) + + metaCommitteeI.Invoke(t, stackitem.Null{}, "registerMetaContainer", cID[:]) + val := metaCommitteeI.Chain.GetStorageItem(meta.MetaDataContractID, key) + require.Equal(t, []byte{}, []byte(val)) + + metaCommitteeI.Invoke(t, stackitem.Null{}, "unregisterMetaContainer", cID[:]) + val = metaCommitteeI.Chain.GetStorageItem(meta.MetaDataContractID, key) + require.Nil(t, val) +} + +func TestMetaDataContract_Netmap(t *testing.T) { + metaValidatorsI, metaCommitteeI := newMetaClient(t) + cID := cidtest.ID() + + metaValidatorsI.WithSigners(metaValidatorsI.NewAccount(t)).InvokeFail(t, native.ErrInvalidWitness.Error(), "registerMetaContainer", cID[:]) + metaCommitteeI.Invoke(t, stackitem.Null{}, "registerMetaContainer", cID[:]) + key := append([]byte{containerPlacementPrefix}, cID[:]...) + + coldVal := metaValidatorsI.Chain.GetStorageItem(meta.MetaDataContractID, key) + require.Nil(t, coldVal) + + var newPlacement meta.Placement + for i := range 5 { + var nodes keys.PublicKeys + for range 5 { + k, err := keys.NewPrivateKey() + require.NoError(t, err) + nodes = append(nodes, k.PublicKey()) + } + + newPlacement = append(newPlacement, meta.PlacementVector{ + REP: uint8(i), + Nodes: nodes, + }) + } + newPlacementI, err := newPlacement.ToStackItem() + require.NoError(t, err) + + metaCommitteeI.Invoke(t, stackitem.Null{}, "updateContainerList", cID[:], &newPlacement) + val := metaCommitteeI.Chain.GetStorageItem(meta.MetaDataContractID, key) + it, err := stackitem.Deserialize(val) + require.NoError(t, err) + requirePlacementsEqual(t, newPlacementI, it) +} + +func requirePlacementsEqual(t *testing.T, a, b stackitem.Item) { + p1, p2 := a.Value().([]stackitem.Item), b.Value().([]stackitem.Item) + require.Equal(t, len(p1), len(p2)) + + for i := range p1 { + v1 := p1[i].Value().([]stackitem.Item) + v2 := p2[i].Value().([]stackitem.Item) + + r1, err := v1[0].TryInteger() + require.NoError(t, err) + r2, err := v2[0].TryInteger() + require.NoError(t, err) + + require.Zero(t, r1.Cmp(r2)) + + n1 := v1[1].Value().([]stackitem.Item) + n2 := v2[1].Value().([]stackitem.Item) + + for j := range n1 { + node1, err := n1[j].TryBytes() + require.NoError(t, err) + node2, err := n2[j].TryBytes() + require.NoError(t, err) + + require.Equal(t, node1, node2) + } + } +} + +func TestMetaDataContract_Objects(t *testing.T) { + _, metaCommitteeI := newMetaClient(t) + cID := cidtest.ID() + const ( + numOfVectors = 5 + nodesInVector = 5 + ) + + var nodes [][]*keys.PrivateKey + for range numOfVectors { + vector := make([]*keys.PrivateKey, 0, nodesInVector) + for range nodesInVector { + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + vector = append(vector, k) + } + nodes = append(nodes, vector) + } + updateContainerList(t, metaCommitteeI, cID, nodes) + + t.Run("meta disabled", func(t *testing.T) { + metaInfo, err := stackitem.Serialize(stackitem.NewMapWithValue( + []stackitem.MapElement{{Key: stackitem.Make("cid"), Value: stackitem.Make(cID[:])}})) + require.NoError(t, err) + + metaCommitteeI.InvokeFail(t, "container does not support chained metadata", "submitObjectPut", metaInfo, signMeta([]byte{}, nodes)) + }) + + t.Run("meta enabled", func(t *testing.T) { + oID := oidtest.ID() + + metaCommitteeI.Invoke(t, stackitem.Null{}, "registerMetaContainer", cID[:]) + val := metaCommitteeI.Chain.GetStorageItem(meta.MetaDataContractID, append([]byte{metaContainersPrefix}, cID[:]...)) + require.Equal(t, []byte{}, []byte(val)) + + t.Run("not enough signatures", func(t *testing.T) { + m := testMeta(cID[:], oID[:]) + rawMeta, err := stackitem.Serialize(m) + require.NoError(t, err) + sigs := signMeta(rawMeta, nodes) + + badSigI, badSigJ := 2, 3 + sigs[badSigI].([]any)[badSigJ] = make([]byte, keys.SignatureLen) + + metaCommitteeI.InvokeFail(t, fmt.Sprintf("%d sig vector does not contain correct number of signatures, %d found, REP: %d", badSigI, nodesInVector-1, nodesInVector), "submitObjectPut", rawMeta, sigs) + }) + + t.Run("correct meta data", func(t *testing.T) { + m := testMeta(cID[:], oID[:]) + rawMeta, err := stackitem.Serialize(m) + require.NoError(t, err) + + sigs := signMeta(rawMeta, nodes) + + hash := metaCommitteeI.Invoke(t, stackitem.Null{}, "submitObjectPut", rawMeta, sigs) + res := metaCommitteeI.GetTxExecResult(t, hash) + require.Len(t, res.Events, 1) + require.Equal(t, "ObjectPut", res.Events[0].Name) + notificationArgs := res.Events[0].Item.Value().([]stackitem.Item) + require.Len(t, notificationArgs, 3) + require.Equal(t, cID[:], notificationArgs[0].Value().([]byte)) + require.Equal(t, oID[:], notificationArgs[1].Value().([]byte)) + + metaValuesExp := m.Value().([]stackitem.MapElement) + metaValuesGot := notificationArgs[2].Value().([]stackitem.MapElement) + require.Equal(t, metaValuesExp, metaValuesGot) + }) + + t.Run("additional testing values", func(t *testing.T) { + // meta-on-chain feature is in progress, it may or may not include additional + // values passed through the contract, therefore, it should be allowed to + // accept unknown map KV pairs + + m := testMeta(cID[:], oID[:]) + m.Add(stackitem.Make("test"), stackitem.Make("test")) + rawMeta, err := stackitem.Serialize(m) + require.NoError(t, err) + + sigs := signMeta(rawMeta, nodes) + + hash := metaCommitteeI.Invoke(t, stackitem.Null{}, "submitObjectPut", rawMeta, sigs) + res := metaCommitteeI.GetTxExecResult(t, hash) + require.Len(t, res.Events, 1) + require.Equal(t, "ObjectPut", res.Events[0].Name) + notificationArgs := res.Events[0].Item.Value().([]stackitem.Item) + require.Len(t, notificationArgs, 3) + require.Equal(t, cID[:], notificationArgs[0].Value().([]byte)) + require.Equal(t, oID[:], notificationArgs[1].Value().([]byte)) + + metaValuesExp := m.Value().([]stackitem.MapElement) + metaValuesGot := notificationArgs[2].Value().([]stackitem.MapElement) + require.Equal(t, metaValuesExp, metaValuesGot) + }) + + t.Run("missing required values", func(t *testing.T) { + testFunc := func(key string) { + m := testMeta(cID[:], oID[:]) + m.Drop(m.Index(stackitem.Make(key))) + raw, err := stackitem.Serialize(m) + require.NoError(t, err) + + sigs := signMeta(raw, nodes) + + metaCommitteeI.InvokeFail(t, fmt.Sprintf("missing required '%s' key in map", key), "submitObjectPut", raw, sigs) + } + + testFunc("oid") + testFunc("size") + testFunc("validUntil") + testFunc("network") + }) + + t.Run("incorrect values", func(t *testing.T) { + testFunc := func(key string, newVal any) { + m := testMeta(cID[:], oID[:]) + m.Add(stackitem.Make(key), stackitem.Make(newVal)) + raw, err := stackitem.Serialize(m) + require.NoError(t, err) + + sigs := signMeta(raw, nodes) + + metaCommitteeI.InvokeFail(t, "incorrect", "submitObjectPut", raw, sigs) + } + + testFunc("oid", []byte{1}) + testFunc("validUntil", 1) // tested chain will have some blocks for sure + testFunc("network", netmode.UnitTestNet+1) + testFunc("type", math.MaxInt64) + testFunc("firstPart", []byte{1}) + testFunc("previousPart", []byte{1}) + testFunc("deleted", []any{[]byte{1}}) + testFunc("locked", []any{[]byte{1}}) + }) + }) +} + +func testMeta(cid, oid []byte) *stackitem.Map { + deleted := oidtest.ID() + locked := oidtest.ID() + + return stackitem.NewMapWithValue( + []stackitem.MapElement{ + {Key: stackitem.Make("network"), Value: stackitem.Make(netmode.UnitTestNet)}, + {Key: stackitem.Make("cid"), Value: stackitem.Make(cid)}, + {Key: stackitem.Make("oid"), Value: stackitem.Make(oid)}, + {Key: stackitem.Make("firstPart"), Value: stackitem.Make(oid)}, + {Key: stackitem.Make("size"), Value: stackitem.Make(123)}, + {Key: stackitem.Make("deleted"), Value: stackitem.Make([]any{deleted[:]})}, + {Key: stackitem.Make("locked"), Value: stackitem.Make([]any{locked[:]})}, + {Key: stackitem.Make("validUntil"), Value: stackitem.Make(math.MaxInt)}, + }) +} + +func updateContainerList(t *testing.T, metaI *neotest.ContractInvoker, cID cid.ID, nodes [][]*keys.PrivateKey) { + var newPlacement meta.Placement + for _, v := range nodes { + var vectorPublic keys.PublicKeys + for _, n := range v { + vectorPublic = append(vectorPublic, n.PublicKey()) + } + + newPlacement = append(newPlacement, meta.PlacementVector{ + REP: uint8(len(v)), + Nodes: vectorPublic, + }) + } + + metaI.Invoke(t, stackitem.Null{}, "updateContainerList", cID[:], &newPlacement) +} + +func signMeta(meta []byte, nodes [][]*keys.PrivateKey) []any { + var sigs []any + for _, v := range nodes { + sigsVector := make([]any, 0, len(v)) + for _, n := range v { + sigsVector = append(sigsVector, n.Sign(meta)) + } + + sigs = append(sigs, sigsVector) + } + + return sigs +} diff --git a/pkg/innerring/internal/metachain/contracts/meta/method_containers.go b/pkg/innerring/internal/metachain/contracts/meta/method_containers.go new file mode 100644 index 0000000000..3b2e4bded0 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/meta/method_containers.go @@ -0,0 +1,57 @@ +package meta + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neofs-contract/common" +) + +func (m *MetaData) unregisterMetaContainer(ic *interop.Context, args []stackitem.Item) stackitem.Item { + const argsNumber = 1 + if len(args) != argsNumber { + panic(fmt.Errorf("unexpected number of args: %d expected, %d given", argsNumber, len(args))) + } + cID, ok := args[0].Value().([]byte) + if !ok { + panic(fmt.Errorf("unexpected argument value: %T expected, %T given", cID, args[0].Value())) + } + if len(cID) != smartcontract.Hash256Len { + panic(fmt.Errorf("unexpected container ID length: %d expected, %d given", smartcontract.Hash256Len, len(cID))) + } + + ok = m.neo.CheckCommittee(ic) + if !ok { + panic(common.ErrAlphabetWitnessFailed) + } + + ic.DAO.DeleteStorageItem(m.ID, append([]byte{metaContainersPrefix}, cID...)) + + return stackitem.Null{} +} + +func (m *MetaData) registerMetaContainer(ic *interop.Context, args []stackitem.Item) stackitem.Item { + const argsNumber = 1 + if len(args) != argsNumber { + panic(fmt.Errorf("unexpected number of args: %d expected, %d given", argsNumber, len(args))) + } + cID, ok := args[0].Value().([]byte) + if !ok { + panic(fmt.Errorf("unexpected argument value: %T expected, %T given", cID, args[0].Value())) + } + if len(cID) != smartcontract.Hash256Len { + panic(fmt.Errorf("unexpected container ID length: %d expected, %d given", smartcontract.Hash256Len, len(cID))) + } + + ok = m.neo.CheckCommittee(ic) + if !ok { + panic(common.ErrAlphabetWitnessFailed) + } + + ic.DAO.PutStorageItem(m.ID, append([]byte{metaContainersPrefix}, cID...), state.StorageItem{}) + + return stackitem.Null{} +} diff --git a/pkg/innerring/internal/metachain/contracts/meta/method_netmap.go b/pkg/innerring/internal/metachain/contracts/meta/method_netmap.go new file mode 100644 index 0000000000..b6aabd76f2 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/meta/method_netmap.go @@ -0,0 +1,120 @@ +package meta + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neofs-contract/common" +) + +// Placement is a placeholder for container's storage nodes list. +type Placement []PlacementVector + +// PlacementVector is a single placement vector in NeoFS [Placement]. +type PlacementVector struct { + REP uint8 + Nodes keys.PublicKeys +} + +func (p Placement) ToStackItem() (stackitem.Item, error) { + res := make([]stackitem.Item, 0, len(p)) + for _, v := range p { + keysRaw := make([]any, 0, len(v.Nodes)) + for i := range v.Nodes { + keysRaw = append(keysRaw, v.Nodes[i].Bytes()) + } + + res = append(res, stackitem.NewArray([]stackitem.Item{ + stackitem.Make(v.REP), + stackitem.Make(keysRaw), + })) + } + + return stackitem.NewArray(res), nil +} + +func (p *Placement) FromStackItem(it stackitem.Item) error { + arr, ok := it.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + + *p = make(Placement, 0, len(arr)) + for i := range arr { + vectorRaw, ok := arr[i].Value().([]stackitem.Item) + if !ok { + return fmt.Errorf("%d vector not an array", i) + } + if len(vectorRaw) != 2 { + return fmt.Errorf("%d vector length has unexpected number of fields: %d expected; %d given", i, 2, len(vectorRaw)) + } + + repB, err := vectorRaw[0].TryInteger() + if err != nil { + return fmt.Errorf("%d vector has incorrect REP: %w", i, err) + } + rep := repB.Int64() + if rep > maxREPsClauses { + return fmt.Errorf("%d vector exceeds maximum number of REP: max %d expetected, %d given", i, maxREPsClauses, rep) + } + + keysRaw, ok := vectorRaw[1].Value().([]stackitem.Item) + if !ok { + return fmt.Errorf("%d vector's keys field is not an array: %w", i, err) + } + pKeys := make(keys.PublicKeys, 0, len(keysRaw)) + for j := range keysRaw { + kRaw, err := keysRaw[j].TryBytes() + if err != nil { + return fmt.Errorf("incorrect %d key of %d vector: %w", j, i, err) + } + var k keys.PublicKey + err = k.DecodeBytes(kRaw) + if err != nil { + return fmt.Errorf("%d key of %d vector is not a key: %w", j, i, err) + } + + pKeys = append(pKeys, &k) + } + + *p = append(*p, PlacementVector{REP: uint8(rep), Nodes: pKeys}) + } + return nil +} + +func (m *MetaData) updateContainerList(ic *interop.Context, args []stackitem.Item) stackitem.Item { + const argsNumber = 2 + if len(args) != argsNumber { + panic(fmt.Errorf("unexpected number of args: %d expected, %d given", argsNumber, len(args))) + } + cID, ok := args[0].Value().([]byte) + if !ok { + panic(fmt.Errorf("unexpected argument value: %T expected, %T given", cID, args[0].Value())) + } + if len(cID) != smartcontract.Hash256Len { + panic(fmt.Errorf("unexpected container ID length: %d expected, %d given", smartcontract.Hash256Len, len(cID))) + } + var newPlacement Placement + err := newPlacement.FromStackItem(args[1]) + if err != nil { + panic(fmt.Errorf("incorrect placement list: %w", err)) + } + + ok = m.neo.CheckCommittee(ic) + if !ok { + panic(common.ErrAlphabetWitnessFailed) + } + + placementRaw, err := stackitem.Serialize(args[1]) + if err != nil { + panic(fmt.Errorf("cannot serialize placement: %w", err)) + } + + ic.DAO.PutStorageItem(m.ID, append([]byte{containerPlacementPrefix}, cID...), placementRaw) + + return stackitem.Null{} +} diff --git a/pkg/innerring/internal/metachain/contracts/meta/method_objects.go b/pkg/innerring/internal/metachain/contracts/meta/method_objects.go new file mode 100644 index 0000000000..d8e7ea4549 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts/meta/method_objects.go @@ -0,0 +1,207 @@ +package meta + +import ( + "fmt" + "slices" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +func (m *MetaData) submitObjectPut(ic *interop.Context, args []stackitem.Item) stackitem.Item { + const argsNumber = 2 + if len(args) != argsNumber { + panic(fmt.Errorf("unexpected number of args: %d expected, %d given", argsNumber, len(args))) + } + metaInfoRaw, ok := args[0].Value().([]byte) + if !ok { + panic(fmt.Errorf("unexpected first argument value: %T expected, %T given", metaInfoRaw, args[0].Value())) + } + metaInfoSI, err := stackitem.Deserialize(metaInfoRaw) + if err != nil { + panic(fmt.Errorf("cannot deserialize meta information from byte array: %w", err)) + } + metaInfo, ok := metaInfoSI.Value().([]stackitem.MapElement) + if !ok { + panic(fmt.Errorf("unexpected deserialized meta information value: expected %T, %T, given", metaInfo, metaInfoSI.Value())) + } + cID, err := requiredInMap(metaInfo, "cid").TryBytes() + if err != nil || len(cID) != smartcontract.Hash256Len { + panic("invalid container ID") + } + if ic.DAO.GetStorageItem(m.ID, append([]byte{metaContainersPrefix}, cID...)) == nil { + panic("container does not support chained metadata") + } + + sigsVectorsRaw, ok := args[1].Value().([]stackitem.Item) + if !ok { + panic(fmt.Errorf("unexpected second argument value: %T expected, %T given", sigsVectorsRaw, args[1].Value())) + } + var sigVectors = make([][][]byte, 0, len(sigsVectorsRaw)) + for i := range sigsVectorsRaw { + vectorRaw, ok := sigsVectorsRaw[i].Value().([]stackitem.Item) + if !ok { + panic(fmt.Errorf("unexpected %d signatures vector value: %T expected, %T given", i, vectorRaw, sigsVectorsRaw[i].Value())) + } + vector := make([][]byte, 0, len(vectorRaw)) + for j := range vectorRaw { + sig, ok := vectorRaw[j].Value().([]byte) + if !ok { + panic(fmt.Errorf("unexpected %d signature value in %d signatures vector: %T expected, %T given", j, i, sig, sigsVectorsRaw[j].Value())) + } + vector = append(vector, sig) + } + sigVectors = append(sigVectors, vector) + } + + cnrListRaw := ic.DAO.GetStorageItem(m.ID, append([]byte{containerPlacementPrefix}, cID...)) + placementI, err := stackitem.Deserialize(cnrListRaw) + if err != nil { + panic(fmt.Errorf("cannot deserialize container placement list: %w", err)) + } + var placement Placement + err = placement.FromStackItem(placementI) + if err != nil { + panic(fmt.Errorf("cannot retreive placement vector from stack item: %w", err)) + } + + if len(sigVectors) != len(placement) { + panic(fmt.Errorf("unexpected number of signature vectors: %d signatures, %d placement vectors found", len(sigVectors), len(placement))) + } + + metaHash := hash.Sha256(metaInfoRaw).BytesBE() +nodesLoop: + for i := range sigVectors { + var foundSigs int + rep := int(placement[i].REP) + if len(sigVectors[i]) < rep { + panic(fmt.Errorf("%d signature vector contains %d signatures but REP is %d", i, len(sigVectors[i]), rep)) + } + + for _, sig := range sigVectors[i] { + for _, node := range placement[i].Nodes { + if node.Verify(sig, metaHash) { + foundSigs++ + break + } + } + } + + if foundSigs == rep { + continue nodesLoop + } else { + panic(fmt.Errorf("%d sig vector does not contain correct number of signatures, %d found, REP: %d", i, foundSigs, rep)) + } + } + + // required + + oID, err := requiredInMap(metaInfo, "oid").TryBytes() + if err != nil || len(oID) != smartcontract.Hash256Len { + panic("incorrect object ID") + } + _, err = requiredInMap(metaInfo, "size").TryInteger() + if err != nil { + panic("incorrect object size") + } + vub, err := requiredInMap(metaInfo, "validUntil").TryInteger() + if err != nil { + panic("incorrect vub") + } + if vub.Int64() <= int64(ic.BlockHeight()) { + panic("incorrect vub: exceeded") + } + magic, err := requiredInMap(metaInfo, "network").TryInteger() + if err != nil || magic.Int64() != int64(ic.Network) { + panic("incorrect network magic") + } + + // optional + + if v, ok := getFromMap(metaInfo, "type"); ok { + typ, err := v.TryInteger() + if err != nil { + panic("incorrect object type") + } + switch typ.Int64() { + case 0, 1, 2, 3, 4: // regular, tombstone, storage group, lock, link + default: + panic(fmt.Errorf("incorrect object type: %d", typ.Int64())) + } + } + if v, ok := getFromMap(metaInfo, "firstPart"); ok { + firstPart, err := v.TryBytes() + if err != nil || len(firstPart) != smartcontract.Hash256Len { + panic("incorrect first part object ID") + } + } + if v, ok := getFromMap(metaInfo, "previousPart"); ok { + previousPart, err := v.TryBytes() + if err != nil || len(previousPart) != smartcontract.Hash256Len { + panic("incorrect previous part object ID") + } + } + if v, ok := getFromMap(metaInfo, "locked"); ok { + locked, ok := v.Value().([]stackitem.Item) + if !ok { + panic("incorrect locked objects array") + } + for i, lRaw := range locked { + l, err := lRaw.TryBytes() + if err != nil { + panic(fmt.Errorf("incorrect %d locked object: %w", i, err)) + } + if len(l) != smartcontract.Hash256Len { + panic(fmt.Errorf("incorrect locked object length: %d", len(l))) + } + } + } + if v, ok := getFromMap(metaInfo, "deleted"); ok { + deleted, ok := v.Value().([]stackitem.Item) + if !ok { + panic("incorrect deleted objects array") + } + for i, dRaw := range deleted { + d, err := dRaw.TryBytes() + if err != nil { + panic(fmt.Errorf("incorrect %d deleted object: %w", i, err)) + } + if len(d) != smartcontract.Hash256Len { + panic(fmt.Errorf("incorrect deleted object length: %d", len(d))) + } + } + } + + err = ic.AddNotification(m.Hash, putObjectEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(cID), + stackitem.NewByteArray(oID), + stackitem.NewMapWithValue(metaInfo)})) + if err != nil { + panic(err) + } + + return stackitem.Null{} +} + +func requiredInMap(m []stackitem.MapElement, key string) stackitem.Item { + v, ok := getFromMap(m, key) + if !ok { + panic("missing required '" + key + "' key in map") + } + + return v +} + +func getFromMap(m []stackitem.MapElement, key string) (stackitem.Item, bool) { + k := stackitem.Make(key) + i := slices.IndexFunc(m, func(e stackitem.MapElement) bool { + return e.Key.Equals(k) + }) + if i == -1 { + return nil, false + } + + return m[i].Value, true +} diff --git a/pkg/morph/client/notary.go b/pkg/morph/client/notary.go index a867b332d8..ae4168a21d 100644 --- a/pkg/morph/client/notary.go +++ b/pkg/morph/client/notary.go @@ -24,8 +24,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/scparser" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neofs-node/pkg/util/rand" @@ -430,7 +430,7 @@ func (c *Client) NotarySignAndInvokeTX(mainTx *transaction.Transaction, await bo if len(script) == 0 { acc = notary.FakeContractAccount(mainTx.Signers[2].Account) } else { - pubBytes, ok := vm.ParseSignatureContract(script) + pubBytes, ok := scparser.ParseSignatureContract(script) if ok { pub, err := keys.NewPublicKeyFromBytes(pubBytes, elliptic.P256()) if err != nil { @@ -438,7 +438,7 @@ func (c *Client) NotarySignAndInvokeTX(mainTx *transaction.Transaction, await bo } acc = notary.FakeSimpleAccount(pub) } else { - m, pubsBytes, ok := vm.ParseMultiSigContract(script) + m, pubsBytes, ok := scparser.ParseMultiSigContract(script) if !ok { return errors.New("failed to parse verification script of signer #2: unknown witness type") } diff --git a/pkg/services/sidechain/sidechain.go b/pkg/services/sidechain/sidechain.go new file mode 100644 index 0000000000..704832bab1 --- /dev/null +++ b/pkg/services/sidechain/sidechain.go @@ -0,0 +1,119 @@ +package sidechain + +import ( + "context" + "fmt" + "time" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/network" + "github.com/nspcc-dev/neo-go/pkg/services/rpcsrv" + "go.uber.org/zap" +) + +// TODO +type SideChain struct { + logger *zap.Logger + storage storage.Store + core *core.Blockchain + netServer *network.Server + rpcServer *rpcsrv.Server + + chErr chan error +} + +// TODO +func New(cfg config.Config, log *zap.Logger, errCh chan error) (*SideChain, error) { + store, err := storage.NewStore(cfg.ApplicationConfiguration.DBConfiguration) + if err != nil { + return &SideChain{}, fmt.Errorf("could not initialize storage: %w", err) + } + defer func() { + if err != nil { + closeErr := store.Close() + if closeErr != nil { + err = fmt.Errorf("%w; also failed to close blockchain storage: %w", err, closeErr) + } + } + }() + + chain, err := core.NewBlockchain(store, cfg.Blockchain(), log.With(zap.String("subcomponent", "core chain"))) + if err != nil { + return &SideChain{}, fmt.Errorf("initializing meta block chain: %w", err) + } + + cfgServer, err := network.NewServerConfig(cfg) + if err != nil { + return nil, fmt.Errorf("compose NeoGo server config from the base one: %w", err) + } + + netServer, err := network.NewServer(cfgServer, chain, chain.GetStateSyncModule(), log.With(zap.String("subcomponent", "network server"))) + if err != nil { + return nil, fmt.Errorf("init NeoGo network server: %w", err) + } + + chErr := make(chan error) + go func() { + for { + err, ok := <-chErr + if !ok { + return + } + errCh <- err + } + }() + + rpcServer := rpcsrv.New(chain, cfg.ApplicationConfiguration.RPC, netServer, nil, log.With(zap.String("subcomponent", "rpc server")), chErr) + netServer.AddService(rpcServer) + + return &SideChain{ + logger: log, + storage: store, + core: chain, + netServer: netServer, + rpcServer: rpcServer, + chErr: chErr, + }, nil +} + +// TODO +func (s *SideChain) Run(ctx context.Context) error { + var err error + defer func() { + // note that we can't rely on the fact that the method never returns an error + // since this may not be forever + if err != nil { + closeErr := s.storage.Close() + if closeErr != nil { + err = fmt.Errorf("%w; also failed to close blockchain storage: %w", err, closeErr) + } + } + }() + + go s.core.Run() + go s.netServer.Start() + + t := time.NewTicker(s.core.GetConfig().ProtocolConfiguration.Genesis.TimePerBlock) + + for { + s.logger.Info("waiting for synchronization with the blockchain network...") + select { + case <-ctx.Done(): + return fmt.Errorf("await state sync: %w", context.Cause(ctx)) + case <-t.C: + if s.netServer.IsInSync() { + s.logger.Info("blockchain state successfully synchronized") + return nil + } + } + } +} + +// TODO +func (s *SideChain) Stop() { + s.netServer.Shutdown() + s.core.Close() + close(s.chErr) +}