diff --git a/go.mod b/go.mod index 46f96bf5c7..d42564f833 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ 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.116.0 + github.com/nspcc-dev/neo-go v0.116.1-0.20260130082647-9c7004d3311f github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea github.com/nspcc-dev/neofs-contract v0.26.0 github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20260127152410-12dbac67e506 @@ -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 b4eb393468..f81a2fd5ac 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,8 @@ 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.116.0 h1:s6z0TDCCH9E/0XVb28e0MYXWzENHhApGsiifLPlstTs= -github.com/nspcc-dev/neo-go v0.116.0/go.mod h1:RDOBkZ+EGtr/NRFItY1oLx7zEIKKqFZKjKupEnMj6q8= +github.com/nspcc-dev/neo-go v0.116.1-0.20260130082647-9c7004d3311f h1:IOEvEfypBaC3Am2tHpqoxc+KwN7CpYGlKykJy/67k74= +github.com/nspcc-dev/neo-go v0.116.1-0.20260130082647-9c7004d3311f/go.mod h1:RDOBkZ+EGtr/NRFItY1oLx7zEIKKqFZKjKupEnMj6q8= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20260121113504-979d1f4aada1 h1:k2PZRCJ82ZSNa398+U6lty6Z0NZOurL72wnEn6ulgos= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20260121113504-979d1f4aada1/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK0EMGLvunXcFyq7fBURS/CsN4MH+4nlYiqn6pTwWAU= 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..05247c55a8 --- /dev/null +++ b/pkg/innerring/internal/metachain/chain.go @@ -0,0 +1,13 @@ +package metachain + +import ( + "github.com/nspcc-dev/neofs-node/pkg/innerring/config" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/blockchain" + "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, NewCustomNatives) +} diff --git a/pkg/innerring/internal/metachain/contracts.go b/pkg/innerring/internal/metachain/contracts.go new file mode 100644 index 0000000000..52f59fcbb5 --- /dev/null +++ b/pkg/innerring/internal/metachain/contracts.go @@ -0,0 +1,55 @@ +package metachain + +import ( + 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/gas" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/meta" +) + +// NewCustomNatives returns custom list of native contracts for metadata +// side chain. Returned contracts: +// - Management +// - Ledger +// - NEO +// - redefined GAS (see [gas.NewGAS] for details) +// - Policy +// - Designate +// - Notary +// - new native metadata contract (see [meta.NewMetadata] for details). +func NewCustomNatives(cfg neogoconfig.ProtocolConfiguration) []interop.Contract { + mgmt := native.NewManagement() + ledger := native.NewLedger() + + g := gas.NewGAS() + n := native.NewNEO(cfg) + p := native.NewPolicy() + + n.GAS = g + n.Policy = p + + mgmt.NEO = n + mgmt.Policy = p + ledger.Policy = p + + desig := native.NewDesignate(cfg.Genesis.Roles) + desig.NEO = n + + notary := native.NewNotary() + notary.Policy = p + notary.GAS = g + notary.NEO = n + notary.Desig = desig + + return []interop.Contract{ + mgmt, + ledger, + n, + g, + p, + desig, + notary, + meta.NewMetadata(n), + } +} diff --git a/pkg/innerring/internal/metachain/gas/gas.go b/pkg/innerring/internal/metachain/gas/gas.go new file mode 100644 index 0000000000..0fe020d8da --- /dev/null +++ b/pkg/innerring/internal/metachain/gas/gas.go @@ -0,0 +1,142 @@ +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/native/nativeids" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "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" +) + +// DefaultBalance is a balance of every account in redefined [GAS] native +// contract. +const DefaultBalance = 100 + +var _ = (native.IGAS)(&GAS{}) + +func (g *GAS) Metadata() *interop.ContractMD { + return &g.ContractMD +} + +// GAS represents GAS custom native contract. It always returns [DefaultBalance] as a +// balance, has no-op `Burn`, `Mint`, `Transfer` operations. +type GAS struct { + interop.ContractMD + symbol string + decimals int64 + factor int64 +} + +// NewGAS returns [GAS] custom native contract. +func NewGAS() *GAS { + g := &GAS{} + defer g.BuildHFSpecificMD(g.ActiveIn()) + + g.ContractMD = *interop.NewContractMD(nativenames.Gas, nativeids.GasToken, func(m *manifest.Manifest, hf config.Hardfork) { + m.SupportedStandards = []string{manifest.NEP17StandardName} + }) + g.symbol = "GAS" + g.decimals = 8 + g.factor = native.GASFactor + + desc := native.NewDescriptor("symbol", smartcontract.StringType) + md := native.NewMethodAndPrice(g.Symbol, 0, callflag.NoneFlag) + g.AddMethod(md, desc) + + desc = native.NewDescriptor("decimals", smartcontract.IntegerType) + md = native.NewMethodAndPrice(g.Decimals, 0, callflag.NoneFlag) + g.AddMethod(md, desc) + + desc = native.NewDescriptor("totalSupply", smartcontract.IntegerType) + md = native.NewMethodAndPrice(g.TotalSupply, 1<<15, callflag.ReadStates) + g.AddMethod(md, desc) + + desc = native.NewDescriptor("balanceOf", smartcontract.IntegerType, + manifest.NewParameter("account", smartcontract.Hash160Type)) + md = native.NewMethodAndPrice(g.balanceOf, 1<<15, callflag.ReadStates) + g.AddMethod(md, desc) + + transferParams := []manifest.Parameter{ + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("to", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + } + desc = native.NewDescriptor("transfer", smartcontract.BoolType, + append(transferParams, manifest.NewParameter("data", smartcontract.AnyType))..., + ) + md = native.NewMethodAndPrice(g.Transfer, 1<<17, callflag.States|callflag.AllowCall|callflag.AllowNotify) + md.StorageFee = 50 + g.AddMethod(md, desc) + + eDesc := native.NewEventDescriptor("Transfer", transferParams...) + eMD := native.NewEvent(eDesc) + g.AddEvent(eMD) + + return g +} + +// Initialize initializes a GAS contract. +func (g *GAS) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + return nil +} + +// InitializeCache implements the [interop.Contract] interface. +func (g *GAS) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + return nil +} + +// OnPersist implements the [interop.Contract] interface. +func (g *GAS) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements the [interop.Contract] interface. +func (g *GAS) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn implements the [interop.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 big.NewInt(DefaultBalance * native.GASFactor) +} + +func (g *GAS) Symbol(_ *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewByteArray([]byte(g.symbol)) +} + +func (g *GAS) Decimals(_ *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(g.decimals)) +} + +func (g *GAS) TotalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(DefaultBalance * native.GASFactor)) +} + +func (g *GAS) Transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return stackitem.NewBool(true) +} + +// balanceOf is the only difference with default native GAS implementation: +// it always returns fixed number of tokens. +func (g *GAS) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(DefaultBalance * native.GASFactor)) +} + +func (g *GAS) Mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) { +} + +func (g *GAS) Burn(ic *interop.Context, h util.Uint160, amount *big.Int) { +} diff --git a/pkg/innerring/internal/metachain/gas/gas_test.go b/pkg/innerring/internal/metachain/gas/gas_test.go new file mode 100644 index 0000000000..622e1f011c --- /dev/null +++ b/pkg/innerring/internal/metachain/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" +) + +func newGasClient(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker) { + ch, validators, committee := chain.NewMultiWithOptions(t, &chain.Options{ + NewNatives: metachain.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/meta/const.go b/pkg/innerring/internal/metachain/meta/const.go new file mode 100644 index 0000000000..3cb1609db3 --- /dev/null +++ b/pkg/innerring/internal/metachain/meta/const.go @@ -0,0 +1,29 @@ +package meta + +import ( + "math" +) + +const ( + // Metadata contract identifiers. + MetaDataContractID = math.MinInt32 + MetaDataContractName = "MetaData" +) + +const ( + // storage prefixes. + metaContainersPrefix = iota + containerPlacementPrefix + + // object prefixes. + addrIndex + lockedByIndex +) + +const ( + // event names. + putObjectEvent = "ObjectPut" + + // limits. + maxREPsClauses = 255 +) diff --git a/pkg/innerring/internal/metachain/meta/meta.go b/pkg/innerring/internal/metachain/meta/meta.go new file mode 100644 index 0000000000..02971ab563 --- /dev/null +++ b/pkg/innerring/internal/metachain/meta/meta.go @@ -0,0 +1,96 @@ +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" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +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)) + md := native.NewMethodAndPrice(m.submitObjectPut, 1<<15, callflag.States|callflag.AllowNotify) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("registerMetaContainer", smartcontract.VoidType, + manifest.NewParameter("container", smartcontract.Hash256Type)) + md = native.NewMethodAndPrice(m.registerMetaContainer, 1<<15, callflag.WriteStates) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("unregisterMetaContainer", smartcontract.VoidType, + manifest.NewParameter("container", smartcontract.Hash256Type)) + md = native.NewMethodAndPrice(m.unregisterMetaContainer, 1<<15, callflag.WriteStates) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("updateContainerList", smartcontract.VoidType, + manifest.NewParameter("container", smartcontract.Hash256Type), + manifest.NewParameter("vectors", smartcontract.ArrayType)) + md = native.NewMethodAndPrice(m.updateContainerList, 1<<15, callflag.WriteStates) + m.AddMethod(md, desc) + + desc = native.NewDescriptor("verifyPlacementSignatures", smartcontract.BoolType, + manifest.NewParameter("container", smartcontract.Hash256Type), + manifest.NewParameter("signatures", smartcontract.ArrayType)) + md = native.NewMethodAndPrice(m.verifyPlacementSignatures, 1<<15, callflag.ReadOnly) + m.AddMethod(md, desc) + + eDesc := native.NewEventDescriptor(putObjectEvent, + manifest.NewParameter("container", smartcontract.Hash256Type), + manifest.NewParameter("object", smartcontract.Hash256Type), + manifest.NewParameter("meta", smartcontract.MapType), + ) + eMD := native.NewEvent(eDesc) + m.AddEvent(eMD) + + return m +} + +func objectIDFromStackItem(i stackitem.Item) (util.Uint256, error) { + return stackitem.ToUint256(i) +} diff --git a/pkg/innerring/internal/metachain/meta/meta_test.go b/pkg/innerring/internal/metachain/meta/meta_test.go new file mode 100644 index 0000000000..7a8b950630 --- /dev/null +++ b/pkg/innerring/internal/metachain/meta/meta_test.go @@ -0,0 +1,420 @@ +package meta_test + +import ( + "fmt" + "math" + "slices" + "sort" + "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/core/transaction" + "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/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain" + "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/metachain/meta" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + "github.com/stretchr/testify/require" +) + +const ( + metaContainersPrefix = iota + containerPlacementPrefix +) + +func newMetaClient(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker) { + ch, validators, committee := chain.NewMultiWithOptions(t, &chain.Options{ + NewNatives: metachain.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()) + } + sort.Sort(nodes) + + 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 ( + // not more than 15 signatures are acceptable in invocation script + numOfVectors = 3 + nodesInVector = 4 + ) + + 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) + snMultisigner := nodesMultiSigner(metaCommitteeI.Hash, cID, nodes) + + t.Run("meta disabled", func(t *testing.T) { + oID := oidtest.ID() + metaInfo, err := stackitem.Serialize(testMeta(cID[:], oID[:])) + require.NoError(t, err) + + metaCommitteeI.InvokeFail(t, "container does not support chained metadata", "submitObjectPut", metaInfo) + }) + + 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) + badNodes := slices.Clone(nodes) + badNodes = badNodes[:len(badNodes)/2] + + verificationFailWithBadSigner(t, metaCommitteeI, nodesMultiSigner(metaCommitteeI.Hash, cID, badNodes), "unexpected", "submitObjectPut", rawMeta) + }) + + t.Run("correct meta data", func(t *testing.T) { + t.Run("notification", func(t *testing.T) { + m := testMeta(cID[:], oID[:]) + rawMeta, err := stackitem.Serialize(m) + require.NoError(t, err) + + h := invokeWithCustomSigner(t, metaCommitteeI, snMultisigner, stackitem.Null{}, "submitObjectPut", rawMeta) + res := metaCommitteeI.GetTxExecResult(t, h) + 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("storage", func(t *testing.T) { + anotherOID := oidtest.ID() + lockedObj := oidtest.ID() + m := testMeta(cID[:], anotherOID[:]) + m.Drop(m.Index(stackitem.Make("deleted"))) + m.Add(stackitem.Make("locked"), stackitem.Make(lockedObj[:])) + m.Add(stackitem.Make("type"), stackitem.Make(int(object.TypeLock))) + + rawMeta, err := stackitem.Serialize(m) + require.NoError(t, err) + + invokeWithCustomSigner(t, metaCommitteeI, snMultisigner, stackitem.Null{}, "submitObjectPut", rawMeta) + + k := make([]byte, 1+cid.Size+oid.Size) + k[0] = 2 // address prefix + copy(k[1:], cID[:]) + copy(k[1+cid.Size:], anotherOID[:]) + + require.Equal(t, rawMeta, []byte(metaCommitteeI.Chain.GetStorageItem(meta.MetaDataContractID, k))) + + k[0] = 3 // lockedBy prefix + copy(k[1+32:], lockedObj[:]) + require.Equal(t, anotherOID[:], []byte(metaCommitteeI.Chain.GetStorageItem(meta.MetaDataContractID, k))) + }) + }) + + 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) + + h := invokeWithCustomSigner(t, metaCommitteeI, snMultisigner, stackitem.Null{}, "submitObjectPut", rawMeta) + res := metaCommitteeI.GetTxExecResult(t, h) + 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) + + invokeFailWithCustomSigner(t, metaCommitteeI, snMultisigner, fmt.Sprintf("missing required '%s' key in map", key), "submitObjectPut", raw) + } + + 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) + + invokeFailWithCustomSigner(t, metaCommitteeI, snMultisigner, "incorrect", "submitObjectPut", raw) + } + + 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() + + 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("type"), Value: stackitem.Make(1)}, + {Key: stackitem.Make("firstPart"), Value: stackitem.Make(oid)}, + {Key: stackitem.Make("previousPart"), Value: stackitem.Make(oid)}, + {Key: stackitem.Make("size"), Value: stackitem.Make(123)}, + {Key: stackitem.Make("deleted"), Value: stackitem.Make(deleted[:])}, + {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 verificationFailWithBadSigner(t testing.TB, validator *neotest.ContractInvoker, customSigner neotest.Signer, message string, method string, args ...any) { + tx := validator.WithSigners(customSigner).PrepareInvoke(t, method, args...) + // `VerifyTx`, not `AddNewBlock`, since broken invocation scripts (for test + // purposes) breaks block chain + err := validator.Executor.Chain.VerifyTx(tx) + require.ErrorContains(t, err, message) +} + +func invokeFailWithCustomSigner(t testing.TB, validator *neotest.ContractInvoker, customSigner neotest.Signer, message string, method string, args ...any) util.Uint256 { + tx := validator.WithSigners(customSigner).PrepareInvoke(t, method, args...) + validator.AddNewBlock(t, tx) + validator.CheckFault(t, tx.Hash(), message) + return tx.Hash() +} + +func invokeWithCustomSigner(t testing.TB, validator *neotest.ContractInvoker, customSigner neotest.Signer, result any, method string, args ...any) util.Uint256 { + tx := validator.WithSigners(customSigner).PrepareInvoke(t, method, args...) + validator.AddNewBlock(t, tx) + validator.CheckHalt(t, tx.Hash(), stackitem.Make(result)) + return tx.Hash() +} + +type signer struct { + verif []byte + nodes [][]*keys.PrivateKey +} + +func (s signer) Script() []byte { + return s.verif +} + +func (s signer) ScriptHash() util.Uint160 { + return hash.Hash160(s.verif) +} + +func (s signer) SignHashable(u uint32, hashable hash.Hashable) []byte { + var ( + invokBuff = io.NewBufBinWriter() + writer = invokBuff.BinWriter + ) + for i := len(s.nodes) - 1; i >= 0; i-- { + vectorLen := len(s.nodes[i]) + for j := vectorLen - 1; j >= 0; j-- { + emit.Bytes(writer, s.nodes[i][j].SignHashable(u, hashable)) + } + emit.Int(writer, int64(vectorLen)) + emit.Opcodes(writer, opcode.PACK) + } + + return invokBuff.Bytes() +} + +func (s signer) SignTx(magic netmode.Magic, tx *transaction.Transaction) error { + if len(tx.Signers) != 1 { + return fmt.Errorf("expected 1 meta signer, got %d", len(tx.Signers)) + } + if acc := hash.Hash160(s.verif); !tx.Signers[0].Account.Equals(acc) { + return fmt.Errorf("expected signer %s, got %s", acc, tx.Signers[0].Account) + } + + // neotest does not support custom scripts and cannot calculate network + // fee correctly so change it there manually to smth that is currently + // enough for tests + tx.NetworkFee = 10_000_000 + + tx.Scripts = append(tx.Scripts[:0], transaction.Witness{ + InvocationScript: s.SignHashable(uint32(magic), tx), + VerificationScript: s.verif, + }) + + return nil +} + +func nodesMultiSigner(contractHash util.Uint160, cID cid.ID, nodes [][]*keys.PrivateKey) neotest.Signer { + for _, vector := range nodes { + slices.SortFunc(vector, func(a, b *keys.PrivateKey) int { + return a.PublicKey().Cmp(b.PublicKey()) + }) + } + + return signer{ + verif: verifScript(contractHash, cID[:], len(nodes)), + nodes: nodes, + } +} + +func verifScript(hash util.Uint160, cID []byte, placementVectorsNumber int) []byte { + var ( + verifScriptBuf = io.NewBufBinWriter() + writer = verifScriptBuf.BinWriter + ) + emit.Int(writer, int64(placementVectorsNumber)) // sigs array length + emit.Opcodes(writer, opcode.PACK) + emit.Bytes(writer, cID) + emit.Int(writer, 2) // number or args + emit.Opcodes(writer, opcode.PACK) + emit.AppCallNoArgs(writer, hash, "verifyPlacementSignatures", callflag.ReadOnly) + + return verifScriptBuf.Bytes() +} diff --git a/pkg/innerring/internal/metachain/meta/method_containers.go b/pkg/innerring/internal/metachain/meta/method_containers.go new file mode 100644 index 0000000000..530c59af6a --- /dev/null +++ b/pkg/innerring/internal/metachain/meta/method_containers.go @@ -0,0 +1,50 @@ +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/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, err := stackitem.ToUint256(args[0]) + if err != nil { + panic(err) + } + + 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, err := stackitem.ToUint256(args[0]) + if err != nil { + panic(err) + } + + 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/meta/method_netmap.go b/pkg/innerring/internal/metachain/meta/method_netmap.go new file mode 100644 index 0000000000..8463c7fe90 --- /dev/null +++ b/pkg/innerring/internal/metachain/meta/method_netmap.go @@ -0,0 +1,121 @@ +package meta + +import ( + "errors" + "fmt" + "sort" + + "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/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)) + } + + rep, err := stackitem.ToUint8(vectorRaw[0]) + if err != nil { + return fmt.Errorf("%d vector has incorrect REP: %w", i, err) + } + if rep > maxREPsClauses { + return fmt.Errorf("%d vector exceeds maximum number of REP: max %d expetected, %d given", i, maxREPsClauses, rep) + } else if rep < 0 { + return fmt.Errorf("%d vector exceeds has negative REP: %d", i, 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, err := stackitem.ToUint256(args[0]) + if err != nil { + panic(err) + } + + 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) + } + + for _, vector := range newPlacement { + sort.Sort(vector.Nodes) + } + + err = ic.DAO.PutStorageConvertible(m.ID, append([]byte{containerPlacementPrefix}, cID[:]...), &newPlacement) + if err != nil { + panic(fmt.Errorf("cannot put updated placement: %w", err)) + } + + return stackitem.Null{} +} diff --git a/pkg/innerring/internal/metachain/meta/method_objects.go b/pkg/innerring/internal/metachain/meta/method_objects.go new file mode 100644 index 0000000000..b0538a5ade --- /dev/null +++ b/pkg/innerring/internal/metachain/meta/method_objects.go @@ -0,0 +1,373 @@ +package meta + +import ( + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "slices" + + "github.com/mr-tron/base58" + "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/core/storage" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" +) + +type objectMeta struct { + cID []byte + oID []byte + size uint64 + typ uint8 + + firstPart []byte + previousPart []byte + locked []byte + deleted []byte +} + +func (m *objectMeta) parse(ic *interop.Context, metaInfo []stackitem.MapElement) error { + // required + + cID, err := requiredInMap(metaInfo, "cid").TryBytes() + if err != nil || len(cID) != smartcontract.Hash256Len { + panic("invalid container ID") + } + m.cID = cID + + oID, err := requiredInMap(metaInfo, "oid").TryBytes() + if err != nil || len(oID) != smartcontract.Hash256Len { + panic("incorrect object ID") + } + m.oID = oID + + sizeB, err := requiredInMap(metaInfo, "size").TryInteger() + if err != nil { + panic("incorrect object size") + } + size := sizeB.Int64() + if size < 0 { + panic(fmt.Sprintf("negative object size: %d", size)) + } + m.size = uint64(size) + + vub, err := requiredInMap(metaInfo, "validUntil").TryInteger() + if err != nil { + panic("incorrect vub") + } + if v, current := vub.Int64(), ic.BlockHeight(); v <= int64(current) { + panic(fmt.Sprintf("incorrect vub: object cannot be accepted: %d <= %d (current height)", v, current)) + } + + magic, err := requiredInMap(metaInfo, "network").TryInteger() + if err != nil { + panic(fmt.Sprintf("incorrect network magic: %s", err.Error())) + } + if v, actual := magic.Int64(), ic.Network; v != int64(actual) { + panic(fmt.Sprintf("incorrect network magic: %d != %d (actual network magic number)", v, actual)) + } + + // optional + if v, ok := getFromMap(metaInfo, "type"); ok { + typ, err := v.TryInteger() + if err != nil { + panic(fmt.Sprintf("incorrect object type: %s", err.Error())) + } + switch object.Type(typ.Int64()) { + case object.TypeRegular, object.TypeTombstone, object.TypeLock, object.TypeLink: + default: + panic(fmt.Errorf("incorrect object type: %d", typ.Int64())) + } + m.typ = uint8(typ.Int64()) + } + + if v, ok := getFromMap(metaInfo, "firstPart"); ok { + first, err := objectIDFromStackItem(v) + if err != nil { + panic(fmt.Errorf("incorrect first part object ID: %w", err)) + } + m.firstPart = first[:] + } + if v, ok := getFromMap(metaInfo, "previousPart"); ok { + prev, err := objectIDFromStackItem(v) + if err != nil { + panic(fmt.Errorf("incorrect previous part object ID: %w", err)) + } + m.previousPart = prev[:] + } + if v, ok := getFromMap(metaInfo, "locked"); ok { + locked, err := objectIDFromStackItem(v) + if err != nil { + panic(fmt.Sprintf("incorrect locked object: %s", err)) + } + if m.typ != 3 { + panic("non-LOCK object with associated locked object") + } + + m.locked = locked[:] + } + if v, ok := getFromMap(metaInfo, "deleted"); ok { + deleted, err := objectIDFromStackItem(v) + if err != nil { + panic(fmt.Sprintf("incorrect deleted object: %s", err)) + } + if m.typ != 1 { + panic("non-TS object with associated deleted object") + } + + m.deleted = deleted[:] + } + + return nil +} + +func (m *MetaData) submitObjectPut(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))) + } + 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())) + } + + var o objectMeta + err = o.parse(ic, metaInfo) + if err != nil { + panic(err) + } + + if ic.DAO.GetStorageItem(m.ID, append([]byte{metaContainersPrefix}, o.cID...)) == nil { + panic("container does not support chained metadata") + } + + cnrListRaw := ic.DAO.GetStorageItem(m.ID, append([]byte{containerPlacementPrefix}, o.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 retrieve placement vector from stack item: %w", err)) + } + + err = isSignedBySNs(ic, m.Hash, o.cID, len(placement)) + if err != nil { + panic(err) + } + + err = storeObject(ic, o, metaInfoRaw) + if err != nil { + panic(fmt.Errorf("cannot store %s/%s object: %w", base58.Encode(o.cID), base58.Encode(o.oID), err)) + } + + err = ic.AddNotification(m.Hash, putObjectEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(o.cID), + stackitem.NewByteArray(o.oID), + stackitem.NewMapWithValue(metaInfo)})) + if err != nil { + panic(err) + } + + return stackitem.Null{} +} + +func storageKey(storagePrefix storage.KeyPrefix, contractID int32, key []byte) []byte { + // 1 for prefix + 4 for Uint32 + len(key) for key + k := make([]byte, 5+len(key)) + k[0] = byte(storagePrefix) + binary.LittleEndian.PutUint32(k[1:], uint32(contractID)) + copy(k[5:], key) + + return k +} + +func putStorageItem(ic *interop.Context, key, value []byte) { + ic.DAO.Store.Put(storageKey(ic.DAO.Version.StoragePrefix, MetaDataContractID, key), value) +} + +func getStorageItem(ic *interop.Context, key []byte) state.StorageItem { + return ic.DAO.GetStorageItem(MetaDataContractID, key) +} + +func deleteStorageItem(ic *interop.Context, key []byte) { + ic.DAO.DeleteStorageItem(MetaDataContractID, key) +} + +func storeObject(ic *interop.Context, parsed objectMeta, rawMeta []byte) error { + key := make([]byte, 1+cid.Size+oid.Size) + copy(key[1:], parsed.cID) + + if parsed.deleted != nil { + key[0] = lockedByIndex + copy(key[1+32:], parsed.deleted) + + if l := getStorageItem(ic, key); l != nil { + return errors.New("locked object deletion") + } + + key[0] = addrIndex + deleteStorageItem(ic, key) + } + + copy(key[1+32:], parsed.oID) + + key[0] = addrIndex + putStorageItem(ic, key, rawMeta) + + if parsed.locked != nil { + key[0] = lockedByIndex + copy(key[1+32:], parsed.locked) + putStorageItem(ic, key, parsed.oID) + } + + return nil +} + +func (m *MetaData) verifyPlacementSignatures(ic *interop.Context, args []stackitem.Item) stackitem.Item { + var ( + signedData = make([]byte, 4, 4+util.Uint256Size) + h = ic.Container.Hash() + ) + binary.LittleEndian.PutUint32(signedData, ic.Network) + signedData = append(signedData, h[:]...) + signedDataHash := sha256.Sum256(signedData) + + const expectedNumberOfArgs = 2 + if len(args) != expectedNumberOfArgs { + return stackitem.NewBool(false) + } + + cID, err := stackitem.ToUint256(args[0]) + if err != nil { + panic(err) + } + 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 retrieve 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))) + } + + for i, vector := range placement { + var foundSigs, lastFoundSig int + for _, sig := range sigVectors[i] { + // placement nodes are sorted by their public keys, so the signers are expected to be + for j := max(0, lastFoundSig); j < len(vector.Nodes); j++ { + if vector.Nodes[j].Verify(sig, signedDataHash[:]) { + foundSigs++ + lastFoundSig = j + break + } + } + if foundSigs == int(vector.REP) { + break + } + } + if foundSigs < int(vector.REP) { + panic(fmt.Sprintf("REP %d is not sufficient for %d placement vector, %d signatures found", vector.REP, i, foundSigs)) + } + } + + return stackitem.NewBool(true) +} + +func verifScript(hash util.Uint160, cID []byte, placementVectorsNumber int) []byte { + var ( + verifScriptBuf = io.NewBufBinWriter() + writer = verifScriptBuf.BinWriter + ) + emit.Int(writer, int64(placementVectorsNumber)) // sigs array length + emit.Opcodes(writer, opcode.PACK) + emit.Bytes(writer, cID) + emit.Int(writer, 2) // number or args + emit.Opcodes(writer, opcode.PACK) + emit.AppCallNoArgs(writer, hash, "verifyPlacementSignatures", callflag.ReadOnly) + + return verifScriptBuf.Bytes() +} + +func isSignedBySNs(ic *interop.Context, contract util.Uint160, cID []byte, placementVectorsNumber int) error { + if l := len(ic.Tx.Scripts); l != 1 { + return fmt.Errorf("expected exactly 1 witness script, got %d", l) + } + + acc := hash.Hash160(verifScript(contract, cID, placementVectorsNumber)) + if !ic.Tx.Signers[0].Account.Equals(acc) { + return fmt.Errorf("not signed by %s account", acc) + } + + return nil +} + +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 +}