diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index dc9205953..b9ccc3032 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -20,13 +20,14 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" - "github.com/nspcc-dev/neo-go/pkg/rpc/request" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" "gopkg.in/yaml.v2" @@ -770,10 +771,16 @@ func contractDeploy(ctx *cli.Context) error { return err } - txScript, err := request.CreateDeploymentScript(&nefFile, m) + mgmtHash, err := c.GetNativeContractHash("Neo Contract Management") if err != nil { - return cli.NewExitError(fmt.Errorf("failed to create deployment script: %w", err), 1) + return cli.NewExitError(fmt.Errorf("failed to get management contract's hash: %w", err), 1) } + buf := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(buf.BinWriter, mgmtHash, "deploy", f, manifestBytes) + if buf.Err != nil { + return cli.NewExitError(fmt.Errorf("failed to create deployment script: %w", buf.Err), 1) + } + txScript := buf.Bytes() // It doesn't require any signers. invRes, err := c.InvokeScript(txScript, nil) if err == nil && invRes.FaultException != "" { diff --git a/cli/testdata/deploy/main.go b/cli/testdata/deploy/main.go index 6182751c9..525d932d9 100644 --- a/cli/testdata/deploy/main.go +++ b/cli/testdata/deploy/main.go @@ -2,18 +2,28 @@ package deploy import ( "github.com/nspcc-dev/neo-go/cli/testdata/deploy/sub" + "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/storage" ) var key = "key" +const mgmtKey = "mgmt" + func _deploy(isUpdate bool) { + var value string + ctx := storage.GetContext() - value := "on create" if isUpdate { value = "on update" + } else { + value = "on create" + sh := runtime.GetCallingScriptHash() + storage.Put(ctx, mgmtKey, sh) } + storage.Put(ctx, key, value) } @@ -24,7 +34,9 @@ func Fail() { // Update updates contract with the new one. func Update(script, manifest []byte) { - contract.Update(script, manifest) + ctx := storage.GetReadOnlyContext() + mgmt := storage.Get(ctx, mgmtKey).(interop.Hash160) + contract.Call(mgmt, "update", script, manifest) } // GetValue returns stored value. diff --git a/examples/timer/timer.go b/examples/timer/timer.go index 0b0839df2..1d58a20fe 100644 --- a/examples/timer/timer.go +++ b/examples/timer/timer.go @@ -1,6 +1,7 @@ package timer import ( + "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/binary" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" @@ -9,6 +10,7 @@ import ( ) const defaultTicks = 3 +const mgmtKey = "mgmt" var ( // ctx holds storage context for contract methods @@ -30,6 +32,8 @@ func _deploy(isUpdate bool) { runtime.Log("One more tick is added.") return } + sh := runtime.GetCallingScriptHash() + storage.Put(ctx, mgmtKey, sh) storage.Put(ctx, ticksKey, defaultTicks) i := binary.Itoa(defaultTicks, 10) runtime.Log("Timer set to " + i + " ticks.") @@ -41,7 +45,8 @@ func Migrate(script []byte, manifest []byte) bool { runtime.Log("Only owner is allowed to update the contract.") return false } - contract.Update(script, manifest) + mgmt := storage.Get(ctx, mgmtKey).(interop.Hash160) + contract.Call(mgmt, "update", script, manifest) runtime.Log("Contract updated.") return true } @@ -67,7 +72,8 @@ func SelfDestroy() bool { runtime.Log("Only owner or the contract itself are allowed to destroy the contract.") return false } - contract.Destroy() + mgmt := storage.Get(ctx, mgmtKey).(interop.Hash160) + contract.Call(mgmt, "destroy") runtime.Log("Destroyed.") return true } diff --git a/internal/testchain/transaction.go b/internal/testchain/transaction.go index 10e71529e..044f82d82 100644 --- a/internal/testchain/transaction.go +++ b/internal/testchain/transaction.go @@ -1,6 +1,7 @@ package testchain import ( + "encoding/json" gio "io" "github.com/nspcc-dev/neo-go/pkg/compiler" @@ -12,7 +13,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/rpc/request" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -48,7 +48,7 @@ func NewTransferFromOwner(bc blockchainer.Blockchainer, contractHash, to util.Ui } // NewDeployTx returns new deployment tx for contract with name with Go code read from r. -func NewDeployTx(name string, sender util.Uint160, r gio.Reader) (*transaction.Transaction, util.Uint160, error) { +func NewDeployTx(bc blockchainer.Blockchainer, name string, sender util.Uint160, r gio.Reader) (*transaction.Transaction, util.Uint160, error) { // nef.NewFile() cares about version a lot. config.Version = "0.90.0-test" @@ -67,12 +67,21 @@ func NewDeployTx(name string, sender util.Uint160, r gio.Reader) (*transaction.T return nil, util.Uint160{}, err } - txScript, err := request.CreateDeploymentScript(ne, m) + rawManifest, err := json.Marshal(m) if err != nil { return nil, util.Uint160{}, err } + neb, err := ne.Bytes() + if err != nil { + return nil, util.Uint160{}, err + } + buf := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(buf.BinWriter, bc.ManagementContractHash(), "deploy", neb, rawManifest) + if buf.Err != nil { + return nil, util.Uint160{}, buf.Err + } - tx := transaction.New(Network(), txScript, 100*native.GASFactor) + tx := transaction.New(Network(), buf.Bytes(), 100*native.GASFactor) tx.Signers = []transaction.Signer{{Account: sender}} h := state.CreateContractHash(tx.Sender(), avm) diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go index fcced5851..506142f6d 100644 --- a/pkg/compiler/debug_test.go +++ b/pkg/compiler/debug_test.go @@ -17,7 +17,6 @@ func TestCodeGen_DebugInfo(t *testing.T) { import "github.com/nspcc-dev/neo-go/pkg/interop" import "github.com/nspcc-dev/neo-go/pkg/interop/storage" import "github.com/nspcc-dev/neo-go/pkg/interop/blockchain" - import "github.com/nspcc-dev/neo-go/pkg/interop/contract" func Main(op string) bool { var s string _ = s @@ -47,7 +46,7 @@ func MethodStruct() struct{} { return struct{}{} } func unexportedMethod() int { return 1 } func MethodParams(addr interop.Hash160, h interop.Hash256, sig interop.Signature, pub interop.PublicKey, - inter interop.Interface, ctr contract.Contract, + inter interop.Interface, ctx storage.Context, tx blockchain.Transaction) bool { return true } @@ -238,7 +237,6 @@ func _deploy(isUpdate bool) {} manifest.NewParameter("sig", smartcontract.SignatureType), manifest.NewParameter("pub", smartcontract.PublicKeyType), manifest.NewParameter("inter", smartcontract.InteropInterfaceType), - manifest.NewParameter("ctr", smartcontract.ArrayType), manifest.NewParameter("ctx", smartcontract.InteropInterfaceType), manifest.NewParameter("tx", smartcontract.ArrayType), }, diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index eb81b717b..ee618b999 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -1,6 +1,7 @@ package compiler_test import ( + "errors" "fmt" "math/big" "strings" @@ -18,6 +19,7 @@ import ( cinterop "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/stretchr/testify/require" @@ -132,12 +134,6 @@ func TestAppCall(t *testing.T) { require.NoError(t, err) barH := hash.Hash160(barCtr) - ic := interop.NewContext(trigger.Application, nil, dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false), nil, nil, nil, zaptest.NewLogger(t)) - require.NoError(t, ic.DAO.PutContractState(&state.Contract{ - Hash: barH, - Script: barCtr, - Manifest: *mBar, - })) srcInner := `package foo import "github.com/nspcc-dev/neo-go/pkg/interop/contract" @@ -164,11 +160,24 @@ func TestAppCall(t *testing.T) { require.NoError(t, err) ih := hash.Hash160(inner) - require.NoError(t, ic.DAO.PutContractState(&state.Contract{ - Hash: ih, - Script: inner, - Manifest: *m, - })) + var contractGetter = func(_ dao.DAO, h util.Uint160) (*state.Contract, error) { + if h.Equals(ih) { + return &state.Contract{ + Hash: ih, + Script: inner, + Manifest: *m, + }, nil + } else if h.Equals(barH) { + return &state.Contract{ + Hash: barH, + Script: barCtr, + Manifest: *mBar, + }, nil + } + return nil, errors.New("not found") + } + + ic := interop.NewContext(trigger.Application, nil, dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false), contractGetter, nil, nil, nil, zaptest.NewLogger(t)) t.Run("valid script", func(t *testing.T) { src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index 964564582..abc5bebe0 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -16,7 +16,6 @@ var syscalls = map[string]map[string]string{ }, "blockchain": { "GetBlock": interopnames.SystemBlockchainGetBlock, - "GetContract": interopnames.SystemBlockchainGetContract, "GetHeight": interopnames.SystemBlockchainGetHeight, "GetTransaction": interopnames.SystemBlockchainGetTransaction, "GetTransactionFromBlock": interopnames.SystemBlockchainGetTransactionFromBlock, @@ -25,12 +24,9 @@ var syscalls = map[string]map[string]string{ "contract": { "Call": interopnames.SystemContractCall, "CallEx": interopnames.SystemContractCallEx, - "Create": interopnames.SystemContractCreate, "CreateStandardAccount": interopnames.SystemContractCreateStandardAccount, - "Destroy": interopnames.SystemContractDestroy, "IsStandard": interopnames.SystemContractIsStandard, "GetCallFlags": interopnames.SystemContractGetCallFlags, - "Update": interopnames.SystemContractUpdate, }, "crypto": { "ECDsaSecp256k1Verify": interopnames.NeoCryptoVerifyWithECDsaSecp256k1, diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 544294129..0604f1847 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -841,7 +841,7 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b if nativeContract != nil { id = nativeContract.Metadata().ContractID } else { - assetContract, err := cache.GetContractState(sc) + assetContract, err := bc.contracts.Management.GetContract(cache, sc) if err != nil { return } @@ -1141,7 +1141,7 @@ func (bc *Blockchain) HeaderHeight() uint32 { // GetContractState returns contract by its script hash. func (bc *Blockchain) GetContractState(hash util.Uint160) *state.Contract { - contract, err := bc.dao.GetContractState(hash) + contract, err := bc.contracts.Management.GetContract(bc.dao, hash) if contract == nil && err != storage.ErrKeyNotFound { bc.log.Warn("failed to get contract state", zap.Error(err)) } @@ -1663,7 +1663,7 @@ func (bc *Blockchain) initVerificationVM(ic *interop.Context, hash util.Uint160, } v.LoadScriptWithFlags(witness.VerificationScript, smartcontract.NoneFlag) } else { - cs, err := ic.DAO.GetContractState(hash) + cs, err := ic.GetContract(hash) if err != nil { return ErrUnknownVerificationContract } @@ -1786,6 +1786,11 @@ func (bc *Blockchain) UtilityTokenHash() util.Uint160 { return bc.contracts.GAS.Hash } +// ManagementContractHash returns management contract's hash. +func (bc *Blockchain) ManagementContractHash() util.Uint160 { + return bc.contracts.Management.Hash +} + func hashAndIndexToBytes(h util.Uint256, index uint32) []byte { buf := io.NewBufBinWriter() buf.WriteBytes(h.BytesLE()) @@ -1794,7 +1799,7 @@ func hashAndIndexToBytes(h util.Uint256, index uint32) []byte { } func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block *block.Block, tx *transaction.Transaction) *interop.Context { - ic := interop.NewContext(trigger, bc, d, bc.contracts.Contracts, block, tx, bc.log) + ic := interop.NewContext(trigger, bc, d, bc.contracts.Management.GetContract, bc.contracts.Contracts, block, tx, bc.log) ic.Functions = [][]interop.Function{systemInterops, neoInterops} switch { case tx != nil: diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 785c18e79..ea3fbb437 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -1030,10 +1030,10 @@ func TestVerifyHashAgainstScript(t *testing.T) { bc := newTestChain(t) defer bc.Close() - cs, csInvalid := getTestContractState() + cs, csInvalid := getTestContractState(bc) ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil) - require.NoError(t, ic.DAO.PutContractState(cs)) - require.NoError(t, ic.DAO.PutContractState(csInvalid)) + require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) + require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, csInvalid)) gas := bc.contracts.Policy.GetMaxVerificationGas(ic.DAO) t.Run("Contract", func(t *testing.T) { @@ -1169,7 +1169,7 @@ func TestIsTxStillRelevant(t *testing.T) { currentHeight := blockchain.GetHeight() return currentHeight < %d }`, bc.BlockHeight()+2) // deploy + next block - txDeploy, h, err := testchain.NewDeployTx("TestVerify", neoOwner, strings.NewReader(src)) + txDeploy, h, err := testchain.NewDeployTx(bc, "TestVerify", neoOwner, strings.NewReader(src)) require.NoError(t, err) txDeploy.ValidUntilBlock = bc.BlockHeight() + 1 addSigners(txDeploy) diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index bcbc2c780..0bbd90fe8 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -58,6 +58,7 @@ type Blockchainer interface { GetTestVM(tx *transaction.Transaction, b *block.Block) *vm.VM GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) mempool.Feer // fee interface + ManagementContractHash() util.Uint160 PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) error PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc Blockchainer, t *transaction.Transaction, data interface{}) error) error RegisterPostBlock(f func(Blockchainer, *mempool.Pool, *block.Block)) diff --git a/pkg/core/dao/cacheddao.go b/pkg/core/dao/cacheddao.go index 037dc64a5..32e5019b5 100644 --- a/pkg/core/dao/cacheddao.go +++ b/pkg/core/dao/cacheddao.go @@ -9,11 +9,10 @@ import ( ) // Cached is a data access object that mimics DAO, but has a write cache -// for accounts and read cache for contracts. These are the most frequently used +// for accounts and NEP17 transfer data. These are the most frequently used // objects in the storeBlock(). type Cached struct { DAO - contracts map[util.Uint160]*state.Contract balances map[util.Uint160]*state.NEP17Balances transfers map[util.Uint160]map[uint32]*state.NEP17TransferLog @@ -22,34 +21,9 @@ type Cached struct { // NewCached returns new Cached wrapping around given backing store. func NewCached(d DAO) *Cached { - ctrs := make(map[util.Uint160]*state.Contract) balances := make(map[util.Uint160]*state.NEP17Balances) transfers := make(map[util.Uint160]map[uint32]*state.NEP17TransferLog) - return &Cached{d.GetWrapped(), ctrs, balances, transfers, false} -} - -// GetContractState returns contract state from cache or underlying store. -func (cd *Cached) GetContractState(hash util.Uint160) (*state.Contract, error) { - if cd.contracts[hash] != nil { - return cd.contracts[hash], nil - } - cs, err := cd.DAO.GetContractState(hash) - if err == nil { - cd.contracts[hash] = cs - } - return cs, err -} - -// PutContractState puts given contract state into the given store. -func (cd *Cached) PutContractState(cs *state.Contract) error { - cd.contracts[cs.Hash] = cs - return cd.DAO.PutContractState(cs) -} - -// DeleteContractState deletes given contract state in cache and backing store. -func (cd *Cached) DeleteContractState(hash util.Uint160) error { - cd.contracts[hash] = nil - return cd.DAO.DeleteContractState(hash) + return &Cached{d.GetWrapped(), balances, transfers, false} } // GetNEP17Balances retrieves NEP17Balances for the acc. @@ -105,7 +79,7 @@ func (cd *Cached) Persist() (int, error) { // If the lower DAO is Cached, we only need to flush the MemCached DB. // This actually breaks DAO interface incapsulation, but for our current // usage scenario it should be good enough if cd doesn't modify object - // caches (accounts/contracts/etc) in any way. + // caches (accounts/transfer data) in any way. if ok { if cd.dropNEP17Cache { lowerCache.balances = make(map[util.Uint160]*state.NEP17Balances) @@ -145,7 +119,6 @@ func (cd *Cached) Persist() (int, error) { // GetWrapped implements DAO interface. func (cd *Cached) GetWrapped() DAO { return &Cached{cd.DAO.GetWrapped(), - cd.contracts, cd.balances, cd.transfers, false, diff --git a/pkg/core/dao/cacheddao_test.go b/pkg/core/dao/cacheddao_test.go index f38987e85..ee8211ade 100644 --- a/pkg/core/dao/cacheddao_test.go +++ b/pkg/core/dao/cacheddao_test.go @@ -7,51 +7,10 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/smartcontract/manifest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestCachedDaoContracts(t *testing.T) { - store := storage.NewMemoryStore() - pdao := NewSimple(store, netmode.UnitTestNet, false) - dao := NewCached(pdao) - - script := []byte{0xde, 0xad, 0xbe, 0xef} - sh := hash.Hash160(script) - _, err := dao.GetContractState(sh) - require.NotNil(t, err) - - m := manifest.NewManifest("Test") - - cs := &state.Contract{ - ID: 123, - Hash: sh, - Script: script, - Manifest: *m, - } - - require.NoError(t, dao.PutContractState(cs)) - cs2, err := dao.GetContractState(sh) - require.Nil(t, err) - require.Equal(t, cs, cs2) - - _, err = dao.Persist() - require.Nil(t, err) - dao2 := NewCached(pdao) - cs2, err = dao2.GetContractState(sh) - require.Nil(t, err) - require.Equal(t, cs, cs2) - - require.NoError(t, dao.DeleteContractState(sh)) - cs2, err = dao2.GetContractState(sh) - require.Nil(t, err) - require.Equal(t, cs, cs2) - _, err = dao.GetContractState(sh) - require.NotNil(t, err) -} - func TestCachedCachedDao(t *testing.T) { store := storage.NewMemoryStore() // Persistent DAO to check for backing storage. diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index d7af83996..40911e227 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -33,13 +33,12 @@ type DAO interface { AppendAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error AppendNEP17Transfer(acc util.Uint160, index uint32, tr *state.NEP17Transfer) (bool, error) DeleteBlock(h util.Uint256, buf *io.BufBinWriter) error - DeleteContractState(hash util.Uint160) error + DeleteContractID(id int32) error DeleteStorageItem(id int32, key []byte) error GetAndDecode(entity io.Serializable, key []byte) error GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) GetBatch() *storage.MemBatch GetBlock(hash util.Uint256) (*block.Block, error) - GetContractState(hash util.Uint160) (*state.Contract, error) GetContractScriptHash(id int32) (util.Uint160, error) GetCurrentBlockHeight() (uint32, error) GetCurrentHeaderHeight() (i uint32, h util.Uint256, err error) @@ -47,7 +46,6 @@ type DAO interface { GetHeaderHashes() ([]util.Uint256, error) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) GetNEP17TransferLog(acc util.Uint160, index uint32) (*state.NEP17TransferLog, error) - GetAndUpdateNextContractID() (int32, error) GetStateRoot(height uint32) (*state.MPTRootState, error) PutStateRoot(root *state.MPTRootState) error GetStorageItem(id int32, key []byte) *state.StorageItem @@ -59,7 +57,7 @@ type DAO interface { HasTransaction(hash util.Uint256) error Persist() (int, error) PutAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error - PutContractState(cs *state.Contract) error + PutContractID(id int32, hash util.Uint160) error PutCurrentHeader(hashAndIndex []byte) error PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error PutNEP17TransferLog(acc util.Uint160, index uint32, lg *state.NEP17TransferLog) error @@ -125,72 +123,32 @@ func (dao *Simple) putWithBuffer(entity io.Serializable, key []byte, buf *io.Buf return dao.Store.Put(key, buf.Bytes()) } -// -- start contracts. - -// GetContractState returns contract state as recorded in the given -// store by the given script hash. -func (dao *Simple) GetContractState(hash util.Uint160) (*state.Contract, error) { - contract := &state.Contract{} - key := storage.AppendPrefix(storage.STContract, hash.BytesBE()) - err := dao.GetAndDecode(contract, key) - if err != nil { - return nil, err - } - - return contract, nil -} - -// PutContractState puts given contract state into the given store. -func (dao *Simple) PutContractState(cs *state.Contract) error { - key := storage.AppendPrefix(storage.STContract, cs.Hash.BytesBE()) - if err := dao.Put(cs, key); err != nil { - return err - } - if cs.UpdateCounter != 0 { // Update. - return nil - } - key = key[:5] - key[0] = byte(storage.STContractID) - binary.LittleEndian.PutUint32(key[1:], uint32(cs.ID)) - return dao.Store.Put(key, cs.Hash.BytesBE()) -} - -// DeleteContractState deletes given contract state in the given store. -func (dao *Simple) DeleteContractState(hash util.Uint160) error { - key := storage.AppendPrefix(storage.STContract, hash.BytesBE()) - return dao.Store.Delete(key) -} - -// GetAndUpdateNextContractID returns id for the next contract and increases stored ID. -func (dao *Simple) GetAndUpdateNextContractID() (int32, error) { - var id = int32(1) - key := storage.SYSContractID.Bytes() - data, err := dao.Store.Get(key) - if err == nil { - id = int32(binary.LittleEndian.Uint32(data)) - } else if err != storage.ErrKeyNotFound { - return 0, err - } - data = make([]byte, 4) - binary.LittleEndian.PutUint32(data, uint32(id+1)) - return id, dao.Store.Put(key, data) -} - -// GetContractScriptHash returns script hash of the contract with the specified ID. -// Contract with the script hash may be destroyed. -func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) { +func makeContractIDKey(id int32) []byte { key := make([]byte, 5) key[0] = byte(storage.STContractID) binary.LittleEndian.PutUint32(key[1:], uint32(id)) - data := &util.Uint160{} - if err := dao.GetAndDecode(data, key); err != nil { + return key +} + +// DeleteContractID deletes contract's id to hash mapping. +func (dao *Simple) DeleteContractID(id int32) error { + return dao.Store.Delete(makeContractIDKey(id)) +} + +// PutContractID adds a mapping from contract's ID to its hash. +func (dao *Simple) PutContractID(id int32, hash util.Uint160) error { + return dao.Store.Put(makeContractIDKey(id), hash.BytesBE()) +} + +// GetContractScriptHash retrieves contract's hash given its ID. +func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) { + var data = new(util.Uint160) + if err := dao.GetAndDecode(data, makeContractIDKey(id)); err != nil { return *data, err } return *data, nil } -// -- end contracts. - // -- start nep17 balances. // GetNEP17Balances retrieves nep17 balances from the cache. diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index b3971c74c..97b1ddb54 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -10,7 +10,6 @@ import ( "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/core/transaction" - "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/trigger" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -43,45 +42,6 @@ func (t *TestSerializable) DecodeBinary(reader *io.BinReader) { t.field = reader.ReadString() } -func TestPutAndGetContractState(t *testing.T) { - dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) - script := []byte{} - h := hash.Hash160(script) - contractState := &state.Contract{Hash: h, Script: script} - err := dao.PutContractState(contractState) - require.NoError(t, err) - gotContractState, err := dao.GetContractState(contractState.Hash) - require.NoError(t, err) - require.Equal(t, contractState, gotContractState) -} - -func TestDeleteContractState(t *testing.T) { - dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) - script := []byte{} - h := hash.Hash160(script) - contractState := &state.Contract{Hash: h, Script: script} - err := dao.PutContractState(contractState) - require.NoError(t, err) - err = dao.DeleteContractState(h) - require.NoError(t, err) - gotContractState, err := dao.GetContractState(h) - require.Error(t, err) - require.Nil(t, gotContractState) -} - -func TestSimple_GetAndUpdateNextContractID(t *testing.T) { - dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) - id, err := dao.GetAndUpdateNextContractID() - require.NoError(t, err) - require.EqualValues(t, 1, id) - id, err = dao.GetAndUpdateNextContractID() - require.NoError(t, err) - require.EqualValues(t, 2, id) - id, err = dao.GetAndUpdateNextContractID() - require.NoError(t, err) - require.EqualValues(t, 3, id) -} - func TestPutGetAppExecResult(t *testing.T) { dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) hash := random.Uint256() diff --git a/pkg/core/gas_price.go b/pkg/core/gas_price.go index e8641bc1a..b5ead6426 100644 --- a/pkg/core/gas_price.go +++ b/pkg/core/gas_price.go @@ -6,9 +6,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) -// StoragePrice is a price for storing 1 byte of storage. -const StoragePrice = 100000 - // getPrice returns a price for executing op with the provided parameter. func getPrice(v *vm.VM, op opcode.Opcode, parameter []byte) int64 { return fee.Opcode(op) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 653c9d2a4..76d91f05f 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -271,7 +271,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { acc0 := wallet.NewAccountFromPrivateKey(priv0) // Push some contract into the chain. - txDeploy, cHash := newDeployTx(t, priv0ScriptHash, prefix+"test_contract.go", "Rubl") + txDeploy, cHash := newDeployTx(t, bc, priv0ScriptHash, prefix+"test_contract.go", "Rubl") txDeploy.Nonce = getNextNonce() txDeploy.ValidUntilBlock = validUntilBlock require.NoError(t, addNetworkFee(bc, txDeploy, acc0)) @@ -357,7 +357,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { t.Logf("sendRublesTx: %v", transferTx.Hash().StringLE()) // Push verification contract into the chain. - txDeploy2, _ := newDeployTx(t, priv0ScriptHash, prefix+"verification_contract.go", "Verify") + txDeploy2, _ := newDeployTx(t, bc, priv0ScriptHash, prefix+"verification_contract.go", "Verify") txDeploy2.Nonce = getNextNonce() txDeploy2.ValidUntilBlock = validUntilBlock require.NoError(t, addNetworkFee(bc, txDeploy2, acc0)) @@ -375,10 +375,10 @@ func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs .. return transaction.New(testchain.Network(), script, 10000000) } -func newDeployTx(t *testing.T, sender util.Uint160, name, ctrName string) (*transaction.Transaction, util.Uint160) { +func newDeployTx(t *testing.T, bc *Blockchain, sender util.Uint160, name, ctrName string) (*transaction.Transaction, util.Uint160) { c, err := ioutil.ReadFile(name) require.NoError(t, err) - tx, h, err := testchain.NewDeployTx(ctrName, sender, bytes.NewReader(c)) + tx, h, err := testchain.NewDeployTx(bc, ctrName, sender, bytes.NewReader(c)) require.NoError(t, err) t.Logf("contractHash (%s): %s", name, h.StringLE()) return tx, h diff --git a/pkg/core/interop/callback/method.go b/pkg/core/interop/callback/method.go index ada8e2d41..4c6764a89 100644 --- a/pkg/core/interop/callback/method.go +++ b/pkg/core/interop/callback/method.go @@ -2,6 +2,7 @@ package callback import ( "errors" + "fmt" "strings" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -39,15 +40,15 @@ func CreateFromMethod(ic *interop.Context) error { if err != nil { return err } - cs, err := ic.DAO.GetContractState(h) + cs, err := ic.GetContract(h) if err != nil { - return errors.New("contract not found") + return fmt.Errorf("contract not found: %w", err) } method := string(ic.VM.Estack().Pop().Bytes()) if strings.HasPrefix(method, "_") { return errors.New("invalid method name") } - currCs, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) + currCs, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) if err == nil && !currCs.Manifest.CanCall(h, &cs.Manifest, method) { return errors.New("method call is not allowed") } diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index f1d858901..2e26d349f 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -37,10 +37,13 @@ type Context struct { Log *zap.Logger VM *vm.VM Functions [][]Function + getContract func(dao.DAO, util.Uint160) (*state.Contract, error) } // NewContext returns new interop context. -func NewContext(trigger trigger.Type, bc blockchainer.Blockchainer, d dao.DAO, natives []Contract, block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context { +func NewContext(trigger trigger.Type, bc blockchainer.Blockchainer, d dao.DAO, + getContract func(dao.DAO, util.Uint160) (*state.Contract, error), natives []Contract, + block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context { dao := dao.NewCached(d) nes := make([]state.NotificationEvent, 0) return &Context{ @@ -53,7 +56,8 @@ func NewContext(trigger trigger.Type, bc blockchainer.Blockchainer, d dao.DAO, n Notifications: nes, Log: log, // Functions is a slice of slices of interops sorted by ID. - Functions: [][]Function{}, + Functions: [][]Function{}, + getContract: getContract, } } @@ -142,6 +146,11 @@ func Sort(fs []Function) { sort.Slice(fs, func(i, j int) bool { return fs[i].ID < fs[j].ID }) } +// GetContract returns contract by its hash in current interop context. +func (ic *Context) GetContract(hash util.Uint160) (*state.Contract, error) { + return ic.getContract(ic.DAO, hash) +} + // GetFunction returns metadata for interop with the specified id. func (ic *Context) GetFunction(id uint32) *Function { for _, slice := range ic.Functions { diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 6c6943c67..c828b6f54 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -39,9 +39,9 @@ func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem if err != nil { return errors.New("invalid contract hash") } - cs, err := ic.DAO.GetContractState(u) + cs, err := ic.GetContract(u) if err != nil { - return errors.New("contract not found") + return fmt.Errorf("contract not found: %w", err) } if strings.HasPrefix(name, "_") { return errors.New("invalid method name (starts with '_')") @@ -53,7 +53,7 @@ func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem if md.Safe { f &^= smartcontract.WriteStates } else if ctx := ic.VM.Context(); ctx != nil && ctx.IsDeployed() { - curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) + curr, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) if err == nil { if !curr.Manifest.CanCall(u, &cs.Manifest, name) { return errors.New("disallowed method call") diff --git a/pkg/core/interop/interopnames/names.go b/pkg/core/interop/interopnames/names.go index ac60bca12..ae1167e2f 100644 --- a/pkg/core/interop/interopnames/names.go +++ b/pkg/core/interop/interopnames/names.go @@ -11,7 +11,6 @@ const ( SystemBinaryItoa = "System.Binary.Itoa" SystemBinarySerialize = "System.Binary.Serialize" SystemBlockchainGetBlock = "System.Blockchain.GetBlock" - SystemBlockchainGetContract = "System.Blockchain.GetContract" SystemBlockchainGetHeight = "System.Blockchain.GetHeight" SystemBlockchainGetTransaction = "System.Blockchain.GetTransaction" SystemBlockchainGetTransactionFromBlock = "System.Blockchain.GetTransactionFromBlock" @@ -22,14 +21,11 @@ const ( SystemCallbackInvoke = "System.Callback.Invoke" SystemContractCall = "System.Contract.Call" SystemContractCallEx = "System.Contract.CallEx" - SystemContractCreate = "System.Contract.Create" SystemContractCreateStandardAccount = "System.Contract.CreateStandardAccount" - SystemContractDestroy = "System.Contract.Destroy" SystemContractIsStandard = "System.Contract.IsStandard" SystemContractGetCallFlags = "System.Contract.GetCallFlags" SystemContractNativeOnPersist = "System.Contract.NativeOnPersist" SystemContractNativePostPersist = "System.Contract.NativePostPersist" - SystemContractUpdate = "System.Contract.Update" SystemEnumeratorConcat = "System.Enumerator.Concat" SystemEnumeratorCreate = "System.Enumerator.Create" SystemEnumeratorNext = "System.Enumerator.Next" @@ -81,7 +77,6 @@ var names = []string{ SystemBinaryItoa, SystemBinarySerialize, SystemBlockchainGetBlock, - SystemBlockchainGetContract, SystemBlockchainGetHeight, SystemBlockchainGetTransaction, SystemBlockchainGetTransactionFromBlock, @@ -92,14 +87,11 @@ var names = []string{ SystemCallbackInvoke, SystemContractCall, SystemContractCallEx, - SystemContractCreate, SystemContractCreateStandardAccount, - SystemContractDestroy, SystemContractIsStandard, SystemContractGetCallFlags, SystemContractNativeOnPersist, SystemContractNativePostPersist, - SystemContractUpdate, SystemEnumeratorConcat, SystemEnumeratorCreate, SystemEnumeratorNext, diff --git a/pkg/core/interop/runtime/witness.go b/pkg/core/interop/runtime/witness.go index bdee68beb..5123128a0 100644 --- a/pkg/core/interop/runtime/witness.go +++ b/pkg/core/interop/runtime/witness.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" - "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/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -22,13 +21,13 @@ func CheckHashedWitness(ic *interop.Context, hash util.Uint160) (bool, error) { return true, nil } if tx, ok := ic.Container.(*transaction.Transaction); ok { - return checkScope(ic.DAO, tx, ic.VM, hash) + return checkScope(ic, tx, ic.VM, hash) } return false, errors.New("script container is not a transaction") } -func checkScope(d dao.DAO, tx *transaction.Transaction, v *vm.VM, hash util.Uint160) (bool, error) { +func checkScope(ic *interop.Context, tx *transaction.Transaction, v *vm.VM, hash util.Uint160) (bool, error) { for _, c := range tx.Signers { if c.Account == hash { if c.Scopes == transaction.Global { @@ -57,9 +56,9 @@ func checkScope(d dao.DAO, tx *transaction.Transaction, v *vm.VM, hash util.Uint if !v.Context().GetCallFlags().Has(smartcontract.ReadStates) { return false, errors.New("missing ReadStates call flag") } - cs, err := d.GetContractState(callingScriptHash) + cs, err := ic.GetContract(callingScriptHash) if err != nil { - return false, err + return false, fmt.Errorf("unable to find calling script: %w", err) } // check if the current group is the required one for _, allowedGroup := range c.AllowedGroups { diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 2971d57b3..adf7d3f6a 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -2,33 +2,15 @@ package core import ( "bytes" - "encoding/json" "errors" "fmt" - "math" "sort" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/core/state" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) -const ( - // MaxContractDescriptionLen is the maximum length for contract description. - MaxContractDescriptionLen = 65536 - // MaxContractScriptSize is the maximum script size for a contract. - MaxContractScriptSize = 1024 * 1024 - // MaxContractParametersNum is the maximum number of parameters for a contract. - MaxContractParametersNum = 252 - // MaxContractStringLen is the maximum length for contract metadata strings. - MaxContractStringLen = 252 -) - var errGasLimitExceeded = errors.New("gas limit exceeded") // storageFind finds stored key-value pair. @@ -58,136 +40,3 @@ func storageFind(ic *interop.Context) error { return nil } - -// getNefAndManifestFromVM pops NEF and manifest from the VM's evaluation stack, -// does a lot of checks and returns deserialized structures if succeeds. -func getNefAndManifestFromVM(v *vm.VM) (*nef.File, *manifest.Manifest, error) { - // Always pop both elements. - nefBytes := v.Estack().Pop().BytesOrNil() - manifestBytes := v.Estack().Pop().BytesOrNil() - - if err := checkNonEmpty(nefBytes, math.MaxInt32); err != nil { // Upper limits are checked during NEF deserialization. - return nil, nil, fmt.Errorf("invalid NEF file: %w", err) - } - if err := checkNonEmpty(manifestBytes, manifest.MaxManifestSize); err != nil { - return nil, nil, fmt.Errorf("invalid manifest: %w", err) - } - - if !v.AddGas(int64(StoragePrice * (len(nefBytes) + len(manifestBytes)))) { - return nil, nil, errGasLimitExceeded - } - var resManifest *manifest.Manifest - var resNef *nef.File - if nefBytes != nil { - nf, err := nef.FileFromBytes(nefBytes) - if err != nil { - return nil, nil, fmt.Errorf("invalid NEF file: %w", err) - } - resNef = &nf - } - if manifestBytes != nil { - resManifest = new(manifest.Manifest) - err := json.Unmarshal(manifestBytes, resManifest) - if err != nil { - return nil, nil, fmt.Errorf("invalid manifest: %w", err) - } - } - return resNef, resManifest, nil -} - -// contractCreate creates a contract. -func contractCreate(ic *interop.Context) error { - neff, manif, err := getNefAndManifestFromVM(ic.VM) - if err != nil { - return err - } - if neff == nil { - return errors.New("no valid NEF provided") - } - if manif == nil { - return errors.New("no valid manifest provided") - } - if ic.Tx == nil { - return errors.New("no transaction provided") - } - h := state.CreateContractHash(ic.Tx.Sender(), neff.Script) - contract, err := ic.DAO.GetContractState(h) - if contract != nil && err == nil { - return errors.New("contract already exists") - } - if !manif.IsValid(h) { - return errors.New("failed to check contract script hash against manifest") - } - id, err := ic.DAO.GetAndUpdateNextContractID() - if err != nil { - return err - } - newcontract := &state.Contract{ - ID: id, - Hash: h, - Script: neff.Script, - Manifest: *manif, - } - if err := ic.DAO.PutContractState(newcontract); err != nil { - return err - } - cs, err := contractToStackItem(newcontract) - if err != nil { - return fmt.Errorf("cannot convert contract to stack item: %w", err) - } - ic.VM.Estack().PushVal(cs) - return callDeploy(ic, newcontract, false) -} - -func checkNonEmpty(b []byte, max int) error { - if b != nil { - if l := len(b); l == 0 { - return errors.New("empty") - } else if l > max { - return fmt.Errorf("len is %d (max %d)", l, max) - } - } - return nil -} - -// contractUpdate migrates a contract. This method assumes that Manifest and Script -// of the contract can be updated independently. -func contractUpdate(ic *interop.Context) error { - neff, manif, err := getNefAndManifestFromVM(ic.VM) - if err != nil { - return err - } - if neff == nil && manif == nil { - return errors.New("both NEF and manifest are nil") - } - contract, _ := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) - if contract == nil { - return errors.New("contract doesn't exist") - } - // if NEF was provided, update the contract script - if neff != nil { - contract.Script = neff.Script - } - // if manifest was provided, update the contract manifest - if manif != nil { - contract.Manifest = *manif - if !contract.Manifest.IsValid(contract.Hash) { - return errors.New("failed to check contract script hash against new manifest") - } - } - contract.UpdateCounter++ - - if err := ic.DAO.PutContractState(contract); err != nil { - return fmt.Errorf("failed to update contract: %w", err) - } - return callDeploy(ic, contract, true) -} - -func callDeploy(ic *interop.Context, cs *state.Contract, isUpdate bool) error { - md := cs.Manifest.ABI.GetMethod(manifest.MethodDeploy) - if md != nil { - return contract.CallExInternal(ic, cs, manifest.MethodDeploy, - []stackitem.Item{stackitem.NewBool(isUpdate)}, smartcontract.All, vm.EnsureIsEmpty) - } - return nil -} diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index 5366e4d91..eeb29a22c 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -49,7 +49,7 @@ func TestStorageFind(t *testing.T) { }, } - require.NoError(t, context.DAO.PutContractState(contractState)) + require.NoError(t, chain.contracts.Management.PutContractState(chain.dao, contractState)) id := contractState.ID diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 50e2f377b..e455f482c 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -2,7 +2,6 @@ package core import ( "crypto/elliptic" - "encoding/json" "errors" "fmt" "math" @@ -12,6 +11,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "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/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -93,41 +93,6 @@ func bcGetBlock(ic *interop.Context) error { return nil } -// contractToStackItem converts state.Contract to stackitem.Item -func contractToStackItem(cs *state.Contract) (stackitem.Item, error) { - manifest, err := json.Marshal(cs.Manifest) - if err != nil { - return nil, err - } - return stackitem.NewArray([]stackitem.Item{ - stackitem.Make(cs.ID), - stackitem.Make(cs.UpdateCounter), - stackitem.NewByteArray(cs.Hash.BytesBE()), - stackitem.NewByteArray(cs.Script), - stackitem.NewByteArray(manifest), - }), nil -} - -// bcGetContract returns contract. -func bcGetContract(ic *interop.Context) error { - hashbytes := ic.VM.Estack().Pop().Bytes() - hash, err := util.Uint160DecodeBytesBE(hashbytes) - if err != nil { - return err - } - cs, err := ic.DAO.GetContractState(hash) - if err != nil { - ic.VM.Estack().PushVal(stackitem.Null{}) - } else { - item, err := contractToStackItem(cs) - if err != nil { - return err - } - ic.VM.Estack().PushVal(item) - } - return nil -} - // bcGetHeight returns blockchain height. func bcGetHeight(ic *interop.Context) error { ic.VM.Estack().PushVal(ic.Chain.BlockHeight()) @@ -274,7 +239,7 @@ func storageGetReadOnlyContext(ic *interop.Context) error { // storageGetContextInternal is internal version of storageGetContext and // storageGetReadOnlyContext which allows to specify ReadOnly context flag. func storageGetContextInternal(ic *interop.Context, isReadOnly bool) error { - contract, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) + contract, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) if err != nil { return err } @@ -311,7 +276,7 @@ func putWithContextAndFlags(ic *interop.Context, stc *StorageContext, key []byte sizeInc = (len(si.Value)-1)/4 + 1 + len(value) - len(si.Value) } } - if !ic.VM.AddGas(int64(sizeInc) * StoragePrice) { + if !ic.VM.AddGas(int64(sizeInc) * native.StoragePrice) { return errGasLimitExceeded } si.Value = value @@ -363,27 +328,6 @@ func storageContextAsReadOnly(ic *interop.Context) error { return nil } -// contractDestroy destroys a contract. -func contractDestroy(ic *interop.Context) error { - hash := ic.VM.GetCurrentScriptHash() - cs, err := ic.DAO.GetContractState(hash) - if err != nil { - return nil - } - err = ic.DAO.DeleteContractState(hash) - if err != nil { - return err - } - siMap, err := ic.DAO.GetStorageItems(cs.ID) - if err != nil { - return err - } - for k := range siMap { - _ = ic.DAO.DeleteStorageItem(cs.ID, []byte(k)) - } - return nil -} - // contractIsStandard checks if contract is standard (sig or multisig) contract. func contractIsStandard(ic *interop.Context) error { h := ic.VM.Estack().Pop().Bytes() @@ -392,7 +336,7 @@ func contractIsStandard(ic *interop.Context) error { return err } var result bool - cs, _ := ic.DAO.GetContractState(u) + cs, _ := ic.GetContract(u) if cs != nil { result = vm.IsStandardContract(cs.Script) } else { diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index b8b2ab42c..19781c46e 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -1,20 +1,19 @@ package core import ( - "encoding/json" "errors" "math/big" "testing" "github.com/nspcc-dev/dbft/crypto" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/callback" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "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/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" @@ -22,7 +21,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -228,7 +226,7 @@ func TestContractIsStandard(t *testing.T) { require.NoError(t, err) pub := priv.PublicKey() - err = ic.DAO.PutContractState(&state.Contract{ID: 42, Hash: pub.GetScriptHash(), Script: pub.GetVerificationScript()}) + err = chain.contracts.Management.PutContractState(ic.DAO, &state.Contract{ID: 42, Hash: pub.GetScriptHash(), Script: pub.GetVerificationScript()}) require.NoError(t, err) v.Estack().PushVal(pub.GetScriptHash().BytesBE()) @@ -237,7 +235,7 @@ func TestContractIsStandard(t *testing.T) { }) t.Run("contract stored, false", func(t *testing.T) { script := []byte{byte(opcode.PUSHT)} - require.NoError(t, ic.DAO.PutContractState(&state.Contract{ID: 24, Hash: hash.Hash160(script), Script: script})) + require.NoError(t, chain.contracts.Management.PutContractState(ic.DAO, &state.Contract{ID: 24, Hash: hash.Hash160(script), Script: script})) v.Estack().PushVal(crypto.Hash160(script).BytesBE()) require.NoError(t, contractIsStandard(ic)) @@ -266,25 +264,74 @@ func TestContractCreateAccount(t *testing.T) { }) } -func TestBlockchainGetContractState(t *testing.T) { - v, cs, ic, bc := createVMAndContractState(t) - defer bc.Close() - require.NoError(t, ic.DAO.PutContractState(cs)) +func TestRuntimeGasLeft(t *testing.T) { + v, ic, chain := createVM(t) + defer chain.Close() - t.Run("positive", func(t *testing.T) { - v.Estack().PushVal(cs.Hash.BytesBE()) - require.NoError(t, bcGetContract(ic)) + v.GasLimit = 100 + v.AddGas(58) + require.NoError(t, runtime.GasLeft(ic)) + require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) +} - actual := v.Estack().Pop().Item() - compareContractStates(t, cs, actual) +func TestRuntimeGetNotifications(t *testing.T) { + v, ic, chain := createVM(t) + defer chain.Close() + + ic.Notifications = []state.NotificationEvent{ + {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{11})})}, + {ScriptHash: util.Uint160{2}, Name: "Event2", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{22})})}, + {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{33})})}, + } + + t.Run("NoFilter", func(t *testing.T) { + v.Estack().PushVal(stackitem.Null{}) + require.NoError(t, runtime.GetNotifications(ic)) + + arr := v.Estack().Pop().Array() + require.Equal(t, len(ic.Notifications), len(arr)) + for i := range arr { + elem := arr[i].Value().([]stackitem.Item) + require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value()) + name, err := stackitem.ToString(elem[1]) + require.NoError(t, err) + require.Equal(t, ic.Notifications[i].Name, name) + require.Equal(t, ic.Notifications[i].Item, elem[2]) + } }) - t.Run("uncknown contract state", func(t *testing.T) { - v.Estack().PushVal(util.Uint160{1, 2, 3}.BytesBE()) - require.NoError(t, bcGetContract(ic)) + t.Run("WithFilter", func(t *testing.T) { + h := util.Uint160{2}.BytesBE() + v.Estack().PushVal(h) + require.NoError(t, runtime.GetNotifications(ic)) - actual := v.Estack().Pop().Item() - require.Equal(t, stackitem.Null{}, actual) + arr := v.Estack().Pop().Array() + require.Equal(t, 1, len(arr)) + elem := arr[0].Value().([]stackitem.Item) + require.Equal(t, h, elem[0].Value()) + name, err := stackitem.ToString(elem[1]) + require.NoError(t, err) + require.Equal(t, ic.Notifications[1].Name, name) + require.Equal(t, ic.Notifications[1].Item, elem[2]) + }) +} + +func TestRuntimeGetInvocationCounter(t *testing.T) { + v, ic, chain := createVM(t) + defer chain.Close() + + ic.VM.Invocations[hash.Hash160([]byte{2})] = 42 + + t.Run("No invocations", func(t *testing.T) { + v.LoadScript([]byte{1}) + // do not return an error in this case. + require.NoError(t, runtime.GetInvocationCounter(ic)) + require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) + }) + t.Run("NonZero", func(t *testing.T) { + v.LoadScript([]byte{2}) + require.NoError(t, runtime.GetInvocationCounter(ic)) + require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) }) } @@ -292,7 +339,7 @@ func TestStoragePut(t *testing.T) { _, cs, ic, bc := createVMAndContractState(t) defer bc.Close() - require.NoError(t, ic.DAO.PutContractState(cs)) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) initVM := func(t *testing.T, key, value []byte, gas int64) { v := ic.SpawnVM() @@ -304,23 +351,23 @@ func TestStoragePut(t *testing.T) { } t.Run("create, not enough gas", func(t *testing.T) { - initVM(t, []byte{1}, []byte{2, 3}, 2*StoragePrice) + initVM(t, []byte{1}, []byte{2, 3}, 2*native.StoragePrice) err := storagePut(ic) require.True(t, errors.Is(err, errGasLimitExceeded), "got: %v", err) }) - initVM(t, []byte{4}, []byte{5, 6}, 3*StoragePrice) + initVM(t, []byte{4}, []byte{5, 6}, 3*native.StoragePrice) require.NoError(t, storagePut(ic)) t.Run("update", func(t *testing.T) { t.Run("not enough gas", func(t *testing.T) { - initVM(t, []byte{4}, []byte{5, 6, 7, 8}, StoragePrice) + initVM(t, []byte{4}, []byte{5, 6, 7, 8}, native.StoragePrice) err := storagePut(ic) require.True(t, errors.Is(err, errGasLimitExceeded), "got: %v", err) }) - initVM(t, []byte{4}, []byte{5, 6, 7, 8}, 3*StoragePrice) + initVM(t, []byte{4}, []byte{5, 6, 7, 8}, 3*native.StoragePrice) require.NoError(t, storagePut(ic)) - initVM(t, []byte{4}, []byte{5, 6}, StoragePrice) + initVM(t, []byte{4}, []byte{5, 6}, native.StoragePrice) require.NoError(t, storagePut(ic)) }) @@ -365,7 +412,7 @@ func TestStorageDelete(t *testing.T) { v, cs, ic, bc := createVMAndContractState(t) defer bc.Close() - require.NoError(t, ic.DAO.PutContractState(cs)) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) v.LoadScriptWithHash(cs.Script, cs.Hash, smartcontract.All) put := func(key, value string, flag int) { v.Estack().PushVal(flag) @@ -403,7 +450,9 @@ func TestStorageDelete(t *testing.T) { } // getTestContractState returns 2 contracts second of which is allowed to call the first. -func getTestContractState() (*state.Contract, *state.Contract) { +func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) { + mgmtHash := bc.ManagementContractHash() + w := io.NewBufBinWriter() emit.Opcodes(w.BinWriter, opcode.ABORT) addOff := w.Len() @@ -446,6 +495,17 @@ func getTestContractState() (*state.Contract, *state.Contract) { emit.String(w.BinWriter, "LastPayment") emit.Syscall(w.BinWriter, interopnames.SystemRuntimeNotify) emit.Opcodes(w.BinWriter, opcode.RET) + updateOff := w.Len() + emit.Int(w.BinWriter, 2) + emit.Opcodes(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "update") + emit.AppCall(w.BinWriter, mgmtHash) + emit.Opcodes(w.BinWriter, opcode.RET) + destroyOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.NEWARRAY0) + emit.String(w.BinWriter, "destroy") + emit.AppCall(w.BinWriter, mgmtHash) + emit.Opcodes(w.BinWriter, opcode.RET) script := w.Bytes() h := hash.Hash160(script) @@ -530,6 +590,20 @@ func getTestContractState() (*state.Contract, *state.Contract) { }, ReturnType: smartcontract.VoidType, }, + { + Name: "update", + Offset: updateOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("nef", smartcontract.ByteArrayType), + manifest.NewParameter("manifest", smartcontract.ByteArrayType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "destroy", + Offset: destroyOff, + ReturnType: smartcontract.VoidType, + }, } cs := &state.Contract{ Script: script, @@ -579,9 +653,9 @@ func TestContractCall(t *testing.T) { _, ic, bc := createVM(t) defer bc.Close() - cs, currCs := getTestContractState() - require.NoError(t, ic.DAO.PutContractState(cs)) - require.NoError(t, ic.DAO.PutContractState(currCs)) + cs, currCs := getTestContractState(bc) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, currCs)) currScript := currCs.Script h := hash.Hash160(cs.Script) @@ -682,325 +756,6 @@ func TestContractCall(t *testing.T) { }) } -func TestContractCreate(t *testing.T) { - v, cs, ic, bc := createVMAndContractState(t) - v.GasLimit = -1 - defer bc.Close() - - // nef.NewFile() cares about version a lot. - config.Version = "0.90.0-test" - - ne, err := nef.NewFile(cs.Script) - require.NoError(t, err) - neb, err := ne.Bytes() - require.NoError(t, err) - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - sender := util.Uint160{1, 2, 3} - h := state.CreateContractHash(sender, ne.Script) - sig := priv.Sign(h.BytesBE()) - cs.Manifest.Groups = []manifest.Group{{ - PublicKey: priv.PublicKey(), - Signature: sig, - }} - m, err := json.Marshal(cs.Manifest) - require.NoError(t, err) - putArgsOnStack := func() { - v.Estack().PushVal(m) - v.Estack().PushVal(neb) - } - - t.Run("no tx", func(t *testing.T) { - putArgsOnStack() - - require.Error(t, contractCreate(ic)) - }) - - ic.Tx = transaction.New(netmode.UnitTestNet, []byte{1}, 0) - ic.Tx.Signers = append(ic.Tx.Signers, transaction.Signer{Account: sender}) - cs.ID = 1 - cs.Hash = state.CreateContractHash(sender, cs.Script) - - t.Run("missing NEF", func(t *testing.T) { - v.Estack().PushVal(m) - v.Estack().PushVal(stackitem.Null{}) - require.Error(t, contractCreate(ic)) - }) - t.Run("missing manifest", func(t *testing.T) { - v.Estack().PushVal(stackitem.Null{}) - v.Estack().PushVal(neb) - require.Error(t, contractCreate(ic)) - }) - t.Run("invalid manifest (empty)", func(t *testing.T) { - v.Estack().PushVal([]byte{}) - v.Estack().PushVal(neb) - require.Error(t, contractCreate(ic)) - }) - - t.Run("invalid manifest (group signature)", func(t *testing.T) { - cs.Manifest.Groups[0].Signature = make([]byte, 11) - rawManif, err := json.Marshal(cs.Manifest) - require.NoError(t, err) - v.Estack().PushVal(rawManif) - v.Estack().PushVal(neb) - require.Error(t, contractCreate(ic)) - }) - - cs.Manifest.Groups[0].Signature = sig - t.Run("positive", func(t *testing.T) { - putArgsOnStack() - - require.NoError(t, contractCreate(ic)) - actual := v.Estack().Pop().Item() - compareContractStates(t, cs, actual) - }) - - t.Run("contract already exists", func(t *testing.T) { - putArgsOnStack() - - require.Error(t, contractCreate(ic)) - }) -} - -func compareContractStates(t *testing.T, expected *state.Contract, actual stackitem.Item) { - act, ok := actual.Value().([]stackitem.Item) - require.True(t, ok) - - expectedManifest, err := json.Marshal(expected.Manifest) - require.NoError(t, err) - - require.Equal(t, 5, len(act)) - require.Equal(t, expected.ID, int32(act[0].Value().(*big.Int).Int64())) - require.Equal(t, expected.UpdateCounter, uint16(act[1].Value().(*big.Int).Int64())) - require.Equal(t, expected.Hash.BytesBE(), act[2].Value().([]byte)) - require.Equal(t, expected.Script, act[3].Value().([]byte)) - require.Equal(t, expectedManifest, act[4].Value().([]byte)) -} - -func TestContractUpdate(t *testing.T) { - v, cs, ic, bc := createVMAndContractState(t) - defer bc.Close() - v.GasLimit = -1 - - putArgsOnStack := func(script, manifest interface{}) { - v.Estack().PushVal(manifest) - b, ok := script.([]byte) - if ok { - ne, err := nef.NewFile(b) - require.NoError(t, err) - script, err = ne.Bytes() - require.NoError(t, err) - } - v.Estack().PushVal(script) - } - - t.Run("no args", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack(stackitem.Null{}, stackitem.Null{}) - require.Error(t, contractUpdate(ic)) - }) - - t.Run("no contract", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{8, 9, 7}, smartcontract.All) - putArgsOnStack([]byte{1}, stackitem.Null{}) - require.Error(t, contractUpdate(ic)) - }) - - t.Run("too large script", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack(make([]byte, MaxContractScriptSize+1), stackitem.Null{}) - require.Error(t, contractUpdate(ic)) - }) - - t.Run("too large manifest", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack(stackitem.Null{}, make([]byte, manifest.MaxManifestSize+1)) - require.Error(t, contractUpdate(ic)) - }) - - t.Run("gas limit exceeded", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.GasLimit = 0 - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack([]byte{1}, []byte{2}) - require.Error(t, contractUpdate(ic)) - }) - - v.GasLimit = -1 - t.Run("update script, positive", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - t.Run("empty manifest", func(t *testing.T) { - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - newScript := []byte{9, 8, 7, 6, 5} - putArgsOnStack(newScript, []byte{}) - require.Error(t, contractUpdate(ic)) - }) - - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - newScript := []byte{9, 8, 7, 6, 5} - putArgsOnStack(newScript, stackitem.Null{}) - - require.NoError(t, contractUpdate(ic)) - - // updated contract should have the same scripthash - actual, err := ic.DAO.GetContractState(cs.Hash) - require.NoError(t, err) - expected := &state.Contract{ - ID: cs.ID, - UpdateCounter: 1, - Hash: cs.Hash, - Script: newScript, - Manifest: cs.Manifest, - } - require.Equal(t, expected, actual) - }) - - t.Run("update manifest, bad manifest", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack(stackitem.Null{}, []byte{1, 2, 3}) - - require.Error(t, contractUpdate(ic)) - }) - - t.Run("update manifest, positive", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - manifest := &manifest.Manifest{ - ABI: manifest.ABI{}, - } - manifestBytes, err := json.Marshal(manifest) - require.NoError(t, err) - - t.Run("empty script", func(t *testing.T) { - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack([]byte{}, manifestBytes) - require.Error(t, contractUpdate(ic)) - }) - - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - putArgsOnStack(stackitem.Null{}, manifestBytes) - require.NoError(t, contractUpdate(ic)) - - // updated contract should have old scripthash - actual, err := ic.DAO.GetContractState(cs.Hash) - require.NoError(t, err) - expected := &state.Contract{ - ID: cs.ID, - UpdateCounter: 2, - Hash: cs.Hash, - Script: cs.Script, - Manifest: *manifest, - } - require.Equal(t, expected, actual) - }) - - t.Run("update both script and manifest", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) - newScript := []byte{12, 13, 14} - newManifest := manifest.Manifest{ - ABI: manifest.ABI{}, - } - newManifestBytes, err := json.Marshal(newManifest) - require.NoError(t, err) - - putArgsOnStack(newScript, newManifestBytes) - - require.NoError(t, contractUpdate(ic)) - - // updated contract should have new script and manifest - actual, err := ic.DAO.GetContractState(cs.Hash) - require.NoError(t, err) - expected := &state.Contract{ - ID: cs.ID, - UpdateCounter: 3, - Hash: cs.Hash, - Script: newScript, - Manifest: newManifest, - } - require.Equal(t, expected, actual) - }) -} - -func TestContractDestroy(t *testing.T) { - v, cs, ic, bc := createVMAndContractState(t) - defer bc.Close() - - v.LoadScriptWithHash(cs.Script, cs.Hash, smartcontract.All) - require.NoError(t, contractDestroy(ic)) // silent error when contract is missing - require.NoError(t, ic.DAO.PutContractState(cs)) - - v.Estack().PushVal("value") - v.Estack().PushVal("key") - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storagePut(ic)) - require.NotNil(t, ic.DAO.GetStorageItem(cs.ID, []byte("key"))) - require.NoError(t, contractDestroy(ic)) - require.Nil(t, ic.DAO.GetStorageItem(cs.ID, []byte("key"))) - require.Error(t, storageGetContext(ic)) -} - -// TestContractCreateDeploy checks that `_deploy` method was called -// during contract creation or update. -func TestContractCreateDeploy(t *testing.T) { - v, ic, bc := createVM(t) - defer bc.Close() - v.GasLimit = -1 - - putArgs := func(cs *state.Contract) { - rawManifest, err := json.Marshal(cs.Manifest) - require.NoError(t, err) - v.Estack().PushVal(rawManifest) - ne, err := nef.NewFile(cs.Script) - require.NoError(t, err) - b, err := ne.Bytes() - require.NoError(t, err) - v.Estack().PushVal(b) - } - cs, currCs := getTestContractState() - - ic.Tx = transaction.New(netmode.UnitTestNet, []byte{1}, 0) - var sender = util.Uint160{1, 2, 3} - ic.Tx.Signers = append(ic.Tx.Signers, transaction.Signer{Account: sender}) - v.LoadScriptWithFlags([]byte{byte(opcode.RET)}, smartcontract.All) - putArgs(cs) - require.NoError(t, contractCreate(ic)) - require.NoError(t, ic.VM.Run()) - - cs.Hash = state.CreateContractHash(sender, cs.Script) - v.LoadScriptWithHash(currCs.Script, cs.Hash, smartcontract.All) - err := contract.CallExInternal(ic, cs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty) - require.NoError(t, err) - require.NoError(t, v.Run()) - require.Equal(t, "create", v.Estack().Pop().String()) - - v.LoadScriptWithHash(cs.Script, cs.Hash, smartcontract.All) - md := cs.Manifest.ABI.GetMethod("justReturn") - v.Jump(v.Context(), md.Offset) - - t.Run("Update", func(t *testing.T) { - newCs := &state.Contract{ - ID: cs.ID, - Hash: cs.Hash, - Script: append(cs.Script, byte(opcode.RET)), - Manifest: cs.Manifest, - } - putArgs(newCs) - require.NoError(t, contractUpdate(ic)) - require.NoError(t, v.Run()) - - v.LoadScriptWithHash(currCs.Script, cs.Hash, smartcontract.All) - err = contract.CallExInternal(ic, newCs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty) - require.NoError(t, err) - require.NoError(t, v.Run()) - require.Equal(t, "update", v.Estack().Pop().String()) - }) -} - func TestContractGetCallFlags(t *testing.T) { v, ic, bc := createVM(t) defer bc.Close() @@ -1049,12 +804,12 @@ func TestMethodCallback(t *testing.T) { _, ic, bc := createVM(t) defer bc.Close() - cs, currCs := getTestContractState() - require.NoError(t, ic.DAO.PutContractState(cs)) - require.NoError(t, ic.DAO.PutContractState(currCs)) + cs, currCs := getTestContractState(bc) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs)) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, currCs)) ic.Functions = append(ic.Functions, systemInterops) - rawHash := hash.Hash160(cs.Script).BytesBE() + rawHash := cs.Hash.BytesBE() t.Run("Invalid", func(t *testing.T) { runInvalid := func(args ...interface{}) func(t *testing.T) { @@ -1318,7 +1073,7 @@ func TestRuntimeCheckWitness(t *testing.T) { Groups: []manifest.Group{{PublicKey: pk.PublicKey()}}, }, } - require.NoError(t, ic.DAO.PutContractState(contractState)) + require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, contractState)) loadScriptWithHashAndFlags(ic, contractScript, contractScriptHash, smartcontract.All) ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), smartcontract.ReadStates) ic.Container = tx diff --git a/pkg/core/interops.go b/pkg/core/interops.go index bb7eda1ed..aa69c2769 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -43,8 +43,6 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemBinarySerialize, Func: binary.Serialize, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBlockchainGetBlock, Func: bcGetBlock, Price: 2500000, RequiredFlags: smartcontract.ReadStates, ParamCount: 1}, - {Name: interopnames.SystemBlockchainGetContract, Func: bcGetContract, Price: 1000000, - RequiredFlags: smartcontract.ReadStates, ParamCount: 1}, {Name: interopnames.SystemBlockchainGetHeight, Func: bcGetHeight, Price: 400, RequiredFlags: smartcontract.ReadStates}, {Name: interopnames.SystemBlockchainGetTransaction, Func: bcGetTransaction, Price: 1000000, @@ -61,16 +59,11 @@ var systemInterops = []interop.Function{ RequiredFlags: smartcontract.AllowCall, ParamCount: 3, DisallowCallback: true}, {Name: interopnames.SystemContractCallEx, Func: contract.CallEx, Price: 1000000, RequiredFlags: smartcontract.AllowCall, ParamCount: 4, DisallowCallback: true}, - {Name: interopnames.SystemContractCreate, Func: contractCreate, Price: 0, - RequiredFlags: smartcontract.WriteStates, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemContractCreateStandardAccount, Func: contractCreateStandardAccount, Price: 10000, ParamCount: 1, DisallowCallback: true}, - {Name: interopnames.SystemContractDestroy, Func: contractDestroy, Price: 1000000, RequiredFlags: smartcontract.WriteStates, DisallowCallback: true}, {Name: interopnames.SystemContractIsStandard, Func: contractIsStandard, Price: 30000, RequiredFlags: smartcontract.ReadStates, ParamCount: 1}, {Name: interopnames.SystemContractGetCallFlags, Func: contractGetCallFlags, Price: 30000, DisallowCallback: true}, {Name: interopnames.SystemContractNativeOnPersist, Func: native.OnPersist, Price: 0, DisallowCallback: true}, {Name: interopnames.SystemContractNativePostPersist, Func: native.PostPersist, Price: 0, DisallowCallback: true}, - {Name: interopnames.SystemContractUpdate, Func: contractUpdate, Price: 0, - RequiredFlags: smartcontract.WriteStates, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemEnumeratorConcat, Func: enumerator.Concat, Price: 400, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemEnumeratorCreate, Func: enumerator.Create, Price: 400, ParamCount: 1, DisallowCallback: true}, {Name: interopnames.SystemEnumeratorNext, Func: enumerator.Next, Price: 1000000, ParamCount: 1, DisallowCallback: true}, @@ -98,7 +91,7 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemRuntimeNotify, Func: runtime.Notify, Price: 1000000, RequiredFlags: smartcontract.AllowNotify, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemRuntimePlatform, Func: runtime.Platform, Price: 250}, - {Name: interopnames.SystemStorageDelete, Func: storageDelete, Price: StoragePrice, + {Name: interopnames.SystemStorageDelete, Func: storageDelete, Price: native.StoragePrice, RequiredFlags: smartcontract.WriteStates, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemStorageFind, Func: storageFind, Price: 1000000, RequiredFlags: smartcontract.ReadStates, ParamCount: 2, DisallowCallback: true}, diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 9df29fb97..e4afa20f4 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -1,10 +1,23 @@ package native import ( + "encoding/json" + "errors" "fmt" + "math" + "math/big" + "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/interop/contract" "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/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) // Management is contract-managing native contract. @@ -12,19 +25,288 @@ type Management struct { interop.ContractMD } +// StoragePrice is the price to pay for 1 byte of storage. +const StoragePrice = 100000 + const ( - managementName = "Neo Contract Management" - prefixContract = 8 - prefixNextAvailableId = 15 + managementName = "Neo Contract Management" + prefixContract = 8 ) +var errGasLimitExceeded = errors.New("gas limit exceeded") +var keyNextAvailableID = []byte{15} + +// makeContractKey creates a key from account script hash. +func makeContractKey(h util.Uint160) []byte { + return makeUint160Key(prefixContract, h) +} + // newManagement creates new Management native contract. func newManagement() *Management { var m = &Management{ContractMD: *interop.NewContractMD(managementName)} + desc := newDescriptor("getContract", smartcontract.ArrayType, + manifest.NewParameter("hash", smartcontract.Hash160Type)) + md := newMethodAndPrice(m.getContract, 1000000, smartcontract.ReadStates) + m.AddMethod(md, desc) + + desc = newDescriptor("deploy", smartcontract.ArrayType, + manifest.NewParameter("script", smartcontract.ByteArrayType), + manifest.NewParameter("manifest", smartcontract.ByteArrayType)) + md = newMethodAndPrice(m.deploy, 0, smartcontract.WriteStates) + m.AddMethod(md, desc) + + desc = newDescriptor("update", smartcontract.VoidType, + manifest.NewParameter("script", smartcontract.ByteArrayType), + manifest.NewParameter("manifest", smartcontract.ByteArrayType)) + md = newMethodAndPrice(m.update, 0, smartcontract.WriteStates) + m.AddMethod(md, desc) + + desc = newDescriptor("destroy", smartcontract.VoidType) + md = newMethodAndPrice(m.destroy, 10000000, smartcontract.WriteStates) + m.AddMethod(md, desc) + return m } +// getContract is an implementation of public getContract method, it's run under +// VM protections, so it's OK for it to panic instead of returning errors. +func (m *Management) getContract(ic *interop.Context, args []stackitem.Item) stackitem.Item { + hashBytes, err := args[0].TryBytes() + if err != nil { + panic(err) + } + hash, err := util.Uint160DecodeBytesBE(hashBytes) + if err != nil { + panic(err) + } + ctr, err := m.GetContract(ic.DAO, hash) + if err != nil { + panic(err) + } + return contractToStack(ctr) +} + +// GetContract returns contract with given hash from given DAO. +func (m *Management) GetContract(d dao.DAO, hash util.Uint160) (*state.Contract, error) { + contract := new(state.Contract) + key := makeContractKey(hash) + err := getSerializableFromDAO(m.ContractID, d, key, contract) + if err != nil { + return nil, err + } + return contract, nil +} + +func getLimitedSlice(arg stackitem.Item, max int) ([]byte, error) { + _, isNull := arg.(stackitem.Null) + if isNull { + return nil, nil + } + b, err := arg.TryBytes() + if err != nil { + return nil, err + } + l := len(b) + if l == 0 { + return nil, errors.New("empty") + } else if l > max { + return nil, fmt.Errorf("len is %d (max %d)", l, max) + } + + return b, nil +} + +// getNefAndManifestFromItems converts input arguments into NEF and manifest +// adding appropriate deployment GAS price and sanitizing inputs. +func getNefAndManifestFromItems(args []stackitem.Item, v *vm.VM) (*nef.File, *manifest.Manifest, error) { + nefBytes, err := getLimitedSlice(args[0], math.MaxInt32) // Upper limits are checked during NEF deserialization. + if err != nil { + return nil, nil, fmt.Errorf("invalid NEF file: %w", err) + } + manifestBytes, err := getLimitedSlice(args[1], manifest.MaxManifestSize) + if err != nil { + return nil, nil, fmt.Errorf("invalid manifest: %w", err) + } + + if !v.AddGas(int64(StoragePrice * (len(nefBytes) + len(manifestBytes)))) { + return nil, nil, errGasLimitExceeded + } + var resManifest *manifest.Manifest + var resNef *nef.File + if nefBytes != nil { + nf, err := nef.FileFromBytes(nefBytes) + if err != nil { + return nil, nil, fmt.Errorf("invalid NEF file: %w", err) + } + resNef = &nf + } + if manifestBytes != nil { + resManifest = new(manifest.Manifest) + err := json.Unmarshal(manifestBytes, resManifest) + if err != nil { + return nil, nil, fmt.Errorf("invalid manifest: %w", err) + } + } + return resNef, resManifest, nil +} + +// deploy is an implementation of public deploy method, it's run under +// VM protections, so it's OK for it to panic instead of returning errors. +func (m *Management) deploy(ic *interop.Context, args []stackitem.Item) stackitem.Item { + neff, manif, err := getNefAndManifestFromItems(args, ic.VM) + if err != nil { + panic(err) + } + if neff == nil { + panic(errors.New("no valid NEF provided")) + } + if manif == nil { + panic(errors.New("no valid manifest provided")) + } + if ic.Tx == nil { + panic(errors.New("no transaction provided")) + } + newcontract, err := m.Deploy(ic.DAO, ic.Tx.Sender(), neff, manif) + if err != nil { + panic(err) + } + callDeploy(ic, newcontract, false) + return contractToStack(newcontract) + +} + +// Deploy creates contract's hash/ID and saves new contract into the given DAO. +// It doesn't run _deploy method. +func (m *Management) Deploy(d dao.DAO, sender util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) { + h := state.CreateContractHash(sender, neff.Script) + key := makeContractKey(h) + si := d.GetStorageItem(m.ContractID, key) + if si != nil { + return nil, errors.New("contract already exists") + } + id, err := m.getNextContractID(d) + if err != nil { + return nil, err + } + if !manif.IsValid(h) { + return nil, errors.New("invalid manifest for this contract") + } + newcontract := &state.Contract{ + ID: id, + Hash: h, + Script: neff.Script, + Manifest: *manif, + } + err = m.PutContractState(d, newcontract) + if err != nil { + return nil, err + } + return newcontract, nil +} + +// update is an implementation of public update method, it's run under +// VM protections, so it's OK for it to panic instead of returning errors. +func (m *Management) update(ic *interop.Context, args []stackitem.Item) stackitem.Item { + neff, manif, err := getNefAndManifestFromItems(args, ic.VM) + if err != nil { + panic(err) + } + if neff == nil && manif == nil { + panic(errors.New("both NEF and manifest are nil")) + } + contract, err := m.Update(ic.DAO, ic.VM.GetCallingScriptHash(), neff, manif) + if err != nil { + panic(err) + } + callDeploy(ic, contract, true) + return stackitem.Null{} +} + +// Update updates contract's script and/or manifest in the given DAO. +// It doesn't run _deploy method. +func (m *Management) Update(d dao.DAO, hash util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) { + contract, err := m.GetContract(d, hash) + if err != nil { + return nil, errors.New("contract doesn't exist") + } + // if NEF was provided, update the contract script + if neff != nil { + contract.Script = neff.Script + } + // if manifest was provided, update the contract manifest + if manif != nil { + contract.Manifest = *manif + if !contract.Manifest.IsValid(contract.Hash) { + return nil, errors.New("invalid manifest for this contract") + } + } + contract.UpdateCounter++ + err = m.PutContractState(d, contract) + if err != nil { + return nil, err + } + return contract, nil +} + +// destroy is an implementation of destroy update method, it's run under +// VM protections, so it's OK for it to panic instead of returning errors. +func (m *Management) destroy(ic *interop.Context, sis []stackitem.Item) stackitem.Item { + hash := ic.VM.GetCallingScriptHash() + err := m.Destroy(ic.DAO, hash) + if err != nil { + panic(err) + } + return stackitem.Null{} +} + +// Destroy drops given contract from DAO along with its storage. +func (m *Management) Destroy(d dao.DAO, hash util.Uint160) error { + contract, err := m.GetContract(d, hash) + if err != nil { + return err + } + key := makeContractKey(hash) + err = d.DeleteStorageItem(m.ContractID, key) + if err != nil { + return err + } + err = d.DeleteContractID(contract.ID) + if err != nil { + return err + } + siMap, err := d.GetStorageItems(contract.ID) + if err != nil { + return err + } + for k := range siMap { + err := d.DeleteStorageItem(contract.ID, []byte(k)) + if err != nil { + return err + } + } + return nil +} + +func callDeploy(ic *interop.Context, cs *state.Contract, isUpdate bool) { + md := cs.Manifest.ABI.GetMethod(manifest.MethodDeploy) + if md != nil { + err := contract.CallExInternal(ic, cs, manifest.MethodDeploy, + []stackitem.Item{stackitem.NewBool(isUpdate)}, smartcontract.All, vm.EnsureIsEmpty) + if err != nil { + panic(err) + } + } +} + +func contractToStack(cs *state.Contract) stackitem.Item { + si, err := cs.ToStackItem() + if err != nil { + panic(fmt.Errorf("contract to stack item: %w", err)) + } + return si +} + // Metadata implements Contract interface. func (m *Management) Metadata() *interop.ContractMD { return &m.ContractMD @@ -45,7 +327,8 @@ func (m *Management) OnPersist(ic *interop.Context) error { Script: md.Script, Manifest: md.Manifest, } - if err := ic.DAO.PutContractState(cs); err != nil { + err := m.PutContractState(ic.DAO, cs) + if err != nil { return err } if err := native.Initialize(ic); err != nil { @@ -65,3 +348,30 @@ func (m *Management) PostPersist(_ *interop.Context) error { func (m *Management) Initialize(_ *interop.Context) error { return nil } + +// PutContractState saves given contract state into given DAO. +func (m *Management) PutContractState(d dao.DAO, cs *state.Contract) error { + key := makeContractKey(cs.Hash) + if err := putSerializableToDAO(m.ContractID, d, key, cs); err != nil { + return err + } + if cs.UpdateCounter != 0 { // Update. + return nil + } + return d.PutContractID(cs.ID, cs.Hash) +} + +func (m *Management) getNextContractID(d dao.DAO) (int32, error) { + var id = big.NewInt(1) + si := d.GetStorageItem(m.ContractID, keyNextAvailableID) + if si != nil { + id = bigint.FromBytes(si.Value) + } else { + si = new(state.StorageItem) + si.Value = make([]byte, 0, 2) + } + ret := int32(id.Int64()) + id.Add(id, big.NewInt(1)) + si.Value = bigint.ToPreallocatedBytes(id, si.Value) + return ret, d.PutStorageItem(m.ContractID, keyNextAvailableID, si) +} diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go new file mode 100644 index 000000000..e708654a4 --- /dev/null +++ b/pkg/core/native/management_test.go @@ -0,0 +1,63 @@ +package native + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "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/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestDeployGetUpdateDestroyContract(t *testing.T) { + mgmt := newManagement() + d := dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) + script := []byte{1} + sender := util.Uint160{1, 2, 3} + h := state.CreateContractHash(sender, script) + + ne, err := nef.NewFile(script) + require.NoError(t, err) + manif := manifest.NewManifest("Test") + require.NoError(t, err) + + contract, err := mgmt.Deploy(d, sender, ne, manif) + require.NoError(t, err) + require.Equal(t, int32(1), contract.ID) + require.Equal(t, uint16(0), contract.UpdateCounter) + require.Equal(t, h, contract.Hash) + require.Equal(t, script, contract.Script) + require.Equal(t, *manif, contract.Manifest) + + // Double deploy. + _, err = mgmt.Deploy(d, sender, ne, manif) + require.Error(t, err) + + // Different sender. + sender2 := util.Uint160{3, 2, 1} + contract2, err := mgmt.Deploy(d, sender2, ne, manif) + require.NoError(t, err) + require.Equal(t, int32(2), contract2.ID) + require.Equal(t, uint16(0), contract2.UpdateCounter) + require.Equal(t, state.CreateContractHash(sender2, script), contract2.Hash) + require.Equal(t, script, contract2.Script) + require.Equal(t, *manif, contract2.Manifest) + + refContract, err := mgmt.GetContract(d, h) + require.NoError(t, err) + require.Equal(t, contract, refContract) + + upContract, err := mgmt.Update(d, h, ne, manif) + refContract.UpdateCounter++ + require.NoError(t, err) + require.Equal(t, refContract, upContract) + + err = mgmt.Destroy(d, h) + require.NoError(t, err) + _, err = mgmt.GetContract(d, h) + require.Error(t, err) +} diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index 9049bcbb7..9a2e8b1ca 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -23,10 +23,7 @@ const prefixAccount = 20 // makeAccountKey creates a key from account script hash. func makeAccountKey(h util.Uint160) []byte { - k := make([]byte, util.Uint160Size+1) - k[0] = prefixAccount - copy(k[1:], h.BytesBE()) - return k + return makeUint160Key(prefixAccount, h) } // nep17TokenNative represents NEP-17 token contract. @@ -132,7 +129,7 @@ func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint if to == nil || !callOnPayment { return } - cs, err := ic.DAO.GetContractState(*to) + cs, err := ic.GetContract(*to) if err != nil { return } diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 9a087b721..3fecfe7ef 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -276,7 +276,7 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem. if ic.Chain.BlockHeight() < deposit.Till { return stackitem.NewBool(false) } - cs, err := ic.DAO.GetContractState(n.GAS.Hash) + cs, err := ic.GetContract(n.GAS.Hash) if err != nil { panic(fmt.Errorf("failed to get GAS contract state: %w", err)) } diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index a2f1e8ac7..b97beb0bc 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -251,7 +251,7 @@ func (o *Oracle) FinishInternal(ic *interop.Context) error { stackitem.Make(resp.Code), stackitem.Make(resp.Result), } - cs, err := ic.DAO.GetContractState(req.CallbackContract) + cs, err := ic.GetContract(req.CallbackContract) if err != nil { return err } @@ -307,7 +307,7 @@ func (o *Oracle) RequestInternal(ic *interop.Context, url string, filter *string } // Should be executed from contract. - _, err := ic.DAO.GetContractState(ic.VM.GetCallingScriptHash()) + _, err := ic.GetContract(ic.VM.GetCallingScriptHash()) if err != nil { return err } diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go index 38a17e58a..d70d88799 100644 --- a/pkg/core/native/util.go +++ b/pkg/core/native/util.go @@ -9,6 +9,7 @@ import ( "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/io" + "github.com/nspcc-dev/neo-go/pkg/util" ) func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error { @@ -71,3 +72,11 @@ func checkValidators(ic *interop.Context) (bool, error) { } return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus) } + +// makeUint160Key creates a key from 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/core/native_contract_test.go b/pkg/core/native_contract_test.go index 0e1f9069a..074a0c7ce 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -136,12 +136,11 @@ func toUint160(item stackitem.Item) util.Uint160 { } func (tn *testNative) call(ic *interop.Context, args []stackitem.Item, checkReturn vm.CheckReturnState) { - h := toUint160(args[0]) - bs, err := args[1].TryBytes() + cs, err := ic.GetContract(toUint160(args[0])) if err != nil { panic(err) } - cs, err := ic.DAO.GetContractState(h) + bs, err := args[1].TryBytes() if err != nil { panic(err) } @@ -169,7 +168,8 @@ func TestNativeContract_Invoke(t *testing.T) { tn := newTestNative() chain.registerNative(tn) - err := chain.dao.PutContractState(&state.Contract{ + err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ + ID: 1, Script: tn.meta.Script, Hash: tn.meta.Hash, Manifest: tn.meta.Manifest, @@ -203,7 +203,8 @@ func TestNativeContract_InvokeInternal(t *testing.T) { tn := newTestNative() chain.registerNative(tn) - err := chain.dao.PutContractState(&state.Contract{ + err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ + ID: 1, Script: tn.meta.Script, Manifest: tn.meta.Manifest, }) @@ -243,7 +244,8 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { tn := newTestNative() chain.registerNative(tn) - err := chain.dao.PutContractState(&state.Contract{ + err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ + ID: 1, Hash: tn.meta.Hash, Script: tn.meta.Script, Manifest: tn.meta.Manifest, @@ -258,8 +260,8 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { } } - cs, _ := getTestContractState() - require.NoError(t, chain.dao.PutContractState(cs)) + cs, _ := getTestContractState(chain) + require.NoError(t, chain.contracts.Management.PutContractState(chain.dao, cs)) t.Run("non-native, no return", func(t *testing.T) { res, err := invokeContractMethod(chain, testSumPrice*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.Hash, "justReturn", []interface{}{}) diff --git a/pkg/core/native_management_test.go b/pkg/core/native_management_test.go new file mode 100644 index 000000000..43204e451 --- /dev/null +++ b/pkg/core/native_management_test.go @@ -0,0 +1,362 @@ +package core + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestContractDeploy(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + // nef.NewFile() cares about version a lot. + config.Version = "0.90.0-test" + mgmtHash := bc.ManagementContractHash() + cs1, _ := getTestContractState(bc) + cs1.ID = 1 + cs1.Hash = state.CreateContractHash(testchain.MultisigScriptHash(), cs1.Script) + manif1, err := json.Marshal(cs1.Manifest) + require.NoError(t, err) + nef1, err := nef.NewFile(cs1.Script) + require.NoError(t, err) + nef1b, err := nef1.Bytes() + require.NoError(t, err) + + t.Run("no NEF", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nil, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("no manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, nil) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("int for NEF", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", int64(1), manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("zero-length NEF", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", []byte{}, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("array for NEF", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", []interface{}{int64(1)}, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("int for manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, int64(1)) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("zero-length manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, []byte{}) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("too long manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 100_00000000, mgmtHash, "deploy", nef1b, append(manif1, make([]byte, manifest.MaxManifestSize)...)) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("array for manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, []interface{}{int64(1)}) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("invalid manifest", func(t *testing.T) { + pkey, err := keys.NewPrivateKey() + require.NoError(t, err) + + var badManifest = cs1.Manifest + badManifest.Groups = []manifest.Group{manifest.Group{PublicKey: pkey.PublicKey(), Signature: make([]byte, 64)}} + manifB, err := json.Marshal(badManifest) + require.NoError(t, err) + + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, manifB) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("not enough GAS", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "deploy", nef1b, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("positive", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, manif1) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res.VMState) + require.Equal(t, 1, len(res.Stack)) + compareContractStates(t, cs1, res.Stack[0]) + t.Run("_deploy called", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "getValue") + require.NoError(t, err) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, []byte("create"), res.Stack[0].Value()) + }) + }) + t.Run("contract already exists", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nef1b, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("failed _deploy", func(t *testing.T) { + deployScript := []byte{byte(opcode.ABORT)} + m := manifest.NewManifest("TestDeployAbort") + m.ABI.Methods = []manifest.Method{ + { + Name: manifest.MethodDeploy, + Offset: 0, + Parameters: []manifest.Parameter{ + manifest.NewParameter("isUpdate", smartcontract.BoolType), + }, + ReturnType: smartcontract.VoidType, + }, + } + nefD, err := nef.NewFile(deployScript) + require.NoError(t, err) + nefDb, err := nefD.Bytes() + require.NoError(t, err) + manifD, err := json.Marshal(m) + require.NoError(t, err) + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nefDb, manifD) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("bad _deploy", func(t *testing.T) { // invalid _deploy signature + deployScript := []byte{byte(opcode.RET)} + m := manifest.NewManifest("TestBadDeploy") + m.ABI.Methods = []manifest.Method{ + { + Name: manifest.MethodDeploy, + Offset: 0, + Parameters: []manifest.Parameter{ + manifest.NewParameter("isUpdate", smartcontract.BoolType), + manifest.NewParameter("param", smartcontract.IntegerType), + }, + ReturnType: smartcontract.VoidType, + }, + } + nefD, err := nef.NewFile(deployScript) + require.NoError(t, err) + nefDb, err := nefD.Bytes() + require.NoError(t, err) + manifD, err := json.Marshal(m) + require.NoError(t, err) + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "deploy", nefDb, manifD) + require.NoError(t, err) + checkFAULTState(t, res) + }) +} + +func TestContractUpdate(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + // nef.NewFile() cares about version a lot. + config.Version = "0.90.0-test" + mgmtHash := bc.ManagementContractHash() + cs1, _ := getTestContractState(bc) + // Allow calling management contract. + cs1.Manifest.Permissions = []manifest.Permission{*manifest.NewPermission(manifest.PermissionWildcard)} + err := bc.contracts.Management.PutContractState(bc.dao, cs1) + require.NoError(t, err) + manif1, err := json.Marshal(cs1.Manifest) + require.NoError(t, err) + nef1, err := nef.NewFile(cs1.Script) + require.NoError(t, err) + nef1b, err := nef1.Bytes() + require.NoError(t, err) + + t.Run("no contract", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, mgmtHash, "update", nef1b, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("zero-length NEF", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", []byte{}, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("zero-length manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nef1b, []byte{}) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("not enough GAS", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "update", nef1b, manif1) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("no real params", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nil, nil) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("invalid manifest", func(t *testing.T) { + pkey, err := keys.NewPrivateKey() + require.NoError(t, err) + + var badManifest = cs1.Manifest + badManifest.Groups = []manifest.Group{manifest.Group{PublicKey: pkey.PublicKey(), Signature: make([]byte, 64)}} + manifB, err := json.Marshal(badManifest) + require.NoError(t, err) + + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nef1b, manifB) + require.NoError(t, err) + checkFAULTState(t, res) + }) + + cs1.Script = append(cs1.Script, byte(opcode.RET)) + nef1, err = nef.NewFile(cs1.Script) + require.NoError(t, err) + nef1b, err = nef1.Bytes() + require.NoError(t, err) + cs1.UpdateCounter++ + + t.Run("update script, positive", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nef1b, nil) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res.VMState) + t.Run("_deploy called", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "getValue") + require.NoError(t, err) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, []byte("update"), res.Stack[0].Value()) + }) + t.Run("check contract", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", cs1.Hash.BytesBE()) + require.NoError(t, err) + require.Equal(t, 1, len(res.Stack)) + compareContractStates(t, cs1, res.Stack[0]) + }) + }) + + cs1.Manifest.Extra = "update me" + manif1, err = json.Marshal(cs1.Manifest) + require.NoError(t, err) + cs1.UpdateCounter++ + + t.Run("update manifest, positive", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nil, manif1) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res.VMState) + t.Run("check contract", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", cs1.Hash.BytesBE()) + require.NoError(t, err) + require.Equal(t, 1, len(res.Stack)) + compareContractStates(t, cs1, res.Stack[0]) + }) + }) + + cs1.Script = append(cs1.Script, byte(opcode.ABORT)) + nef1, err = nef.NewFile(cs1.Script) + require.NoError(t, err) + nef1b, err = nef1.Bytes() + require.NoError(t, err) + cs1.Manifest.Extra = "update me once more" + manif1, err = json.Marshal(cs1.Manifest) + require.NoError(t, err) + cs1.UpdateCounter++ + + t.Run("update both script and manifest", func(t *testing.T) { + res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nef1b, manif1) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res.VMState) + t.Run("check contract", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", cs1.Hash.BytesBE()) + require.NoError(t, err) + require.Equal(t, 1, len(res.Stack)) + compareContractStates(t, cs1, res.Stack[0]) + }) + }) +} + +func TestGetContract(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + mgmtHash := bc.ManagementContractHash() + cs1, _ := getTestContractState(bc) + err := bc.contracts.Management.PutContractState(bc.dao, cs1) + require.NoError(t, err) + + t.Run("bad parameter type", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", []interface{}{int64(1)}) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("not a hash", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", []byte{1, 2, 3}) + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("positive", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", cs1.Hash.BytesBE()) + require.NoError(t, err) + require.Equal(t, 1, len(res.Stack)) + compareContractStates(t, cs1, res.Stack[0]) + }) +} + +func TestContractDestroy(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + mgmtHash := bc.ManagementContractHash() + cs1, _ := getTestContractState(bc) + // Allow calling management contract. + cs1.Manifest.Permissions = []manifest.Permission{*manifest.NewPermission(manifest.PermissionWildcard)} + err := bc.contracts.Management.PutContractState(bc.dao, cs1) + require.NoError(t, err) + err = bc.dao.PutStorageItem(cs1.ID, []byte{1, 2, 3}, &state.StorageItem{Value: []byte{3, 2, 1}}) + require.NoError(t, err) + + t.Run("no contract", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "destroy") + require.NoError(t, err) + checkFAULTState(t, res) + }) + t.Run("positive", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "destroy") + require.NoError(t, err) + require.Equal(t, vm.HaltState, res.VMState) + t.Run("check contract", func(t *testing.T) { + res, err := invokeContractMethod(bc, 1_00000000, mgmtHash, "getContract", cs1.Hash.BytesBE()) + require.NoError(t, err) + checkFAULTState(t, res) + }) + + }) +} + +func compareContractStates(t *testing.T, expected *state.Contract, actual stackitem.Item) { + act, ok := actual.Value().([]stackitem.Item) + require.True(t, ok) + + expectedManifest, err := json.Marshal(expected.Manifest) + require.NoError(t, err) + + require.Equal(t, 5, len(act)) + require.Equal(t, expected.ID, int32(act[0].Value().(*big.Int).Int64())) + require.Equal(t, expected.UpdateCounter, uint16(act[1].Value().(*big.Int).Int64())) + require.Equal(t, expected.Hash.BytesBE(), act[2].Value().([]byte)) + require.Equal(t, expected.Script, act[3].Value().([]byte)) + require.Equal(t, expectedManifest, act[4].Value().([]byte)) +} diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 0e2da1511..4ef8759d4 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -309,8 +309,8 @@ func TestNEO_TransferOnPayment(t *testing.T) { bc := newTestChain(t) defer bc.Close() - cs, _ := getTestContractState() - require.NoError(t, bc.dao.PutContractState(cs)) + cs, _ := getTestContractState(bc) + require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) const amount = 2 tx := transferTokenFromMultisigAccount(t, bc, cs.Hash, bc.contracts.NEO.Hash, amount) diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go index 7cea35cea..d914d579b 100644 --- a/pkg/core/native_oracle_test.go +++ b/pkg/core/native_oracle_test.go @@ -109,7 +109,7 @@ func TestOracle_Request(t *testing.T) { orc := bc.contracts.Oracle cs := getOracleContractState(orc.Hash) - require.NoError(t, bc.dao.PutContractState(cs)) + require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) gasForResponse := int64(2000_1234) var filter = "flt" diff --git a/pkg/core/state/contract.go b/pkg/core/state/contract.go index f616d4d79..743713fce 100644 --- a/pkg/core/state/contract.go +++ b/pkg/core/state/contract.go @@ -1,12 +1,18 @@ package state import ( + "encoding/json" + "errors" + "math" + "math/big" + "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/manifest" "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" ) // Contract holds information about a smart contract in the NEO blockchain. @@ -19,21 +25,81 @@ type Contract struct { } // DecodeBinary implements Serializable interface. -func (cs *Contract) DecodeBinary(br *io.BinReader) { - cs.ID = int32(br.ReadU32LE()) - cs.UpdateCounter = br.ReadU16LE() - cs.Hash.DecodeBinary(br) - cs.Script = br.ReadVarBytes() - cs.Manifest.DecodeBinary(br) +func (c *Contract) DecodeBinary(r *io.BinReader) { + si := stackitem.DecodeBinaryStackItem(r) + if r.Err != nil { + return + } + r.Err = c.FromStackItem(si) } // EncodeBinary implements Serializable interface. -func (cs *Contract) EncodeBinary(bw *io.BinWriter) { - bw.WriteU32LE(uint32(cs.ID)) - bw.WriteU16LE(cs.UpdateCounter) - cs.Hash.EncodeBinary(bw) - bw.WriteVarBytes(cs.Script) - cs.Manifest.EncodeBinary(bw) +func (c *Contract) EncodeBinary(w *io.BinWriter) { + si, err := c.ToStackItem() + if err != nil { + w.Err = err + return + } + stackitem.EncodeBinaryStackItem(si, w) +} + +// ToStackItem converts state.Contract to stackitem.Item +func (c *Contract) ToStackItem() (stackitem.Item, error) { + manifest, err := json.Marshal(c.Manifest) + if err != nil { + return nil, err + } + return stackitem.NewArray([]stackitem.Item{ + stackitem.Make(c.ID), + stackitem.Make(c.UpdateCounter), + stackitem.NewByteArray(c.Hash.BytesBE()), + stackitem.NewByteArray(c.Script), + stackitem.NewByteArray(manifest), + }), nil +} + +// FromStackItem fills Contract's data from given stack itemized contract +// representation. +func (c *Contract) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + bi, ok := arr[0].Value().(*big.Int) + if !ok { + return errors.New("ID is not an integer") + } + if !bi.IsInt64() || bi.Int64() > math.MaxInt32 || bi.Int64() < math.MinInt32 { + return errors.New("ID not in int32 range") + } + c.ID = int32(bi.Int64()) + bi, ok = arr[1].Value().(*big.Int) + if !ok { + return errors.New("UpdateCounter is not an integer") + } + if !bi.IsInt64() || bi.Int64() > math.MaxUint16 || bi.Int64() < 0 { + return errors.New("UpdateCounter not in uint16 range") + } + c.UpdateCounter = uint16(bi.Int64()) + bytes, err := arr[2].TryBytes() + if err != nil { + return err + } + c.Hash, err = util.Uint160DecodeBytesBE(bytes) + if err != nil { + return err + } + bytes, err = arr[3].TryBytes() + if err != nil { + return err + } + c.Script = make([]byte, len(bytes)) + copy(c.Script, bytes) + bytes, err = arr[4].TryBytes() + if err != nil { + return err + } + return json.Unmarshal(bytes, &c.Manifest) } // CreateContractHash creates deployed contract hash from transaction sender diff --git a/pkg/core/state/contract_test.go b/pkg/core/state/contract_test.go index 5283b5acc..73d4e5ecf 100644 --- a/pkg/core/state/contract_test.go +++ b/pkg/core/state/contract_test.go @@ -1,6 +1,8 @@ package state import ( + "encoding/json" + "math" "testing" "github.com/nspcc-dev/neo-go/internal/testserdes" @@ -8,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract" "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" "github.com/stretchr/testify/require" ) @@ -58,3 +61,41 @@ func TestCreateContractHash(t *testing.T) { require.NoError(t, err) require.Equal(t, "e56e4ee87f89a70e9138432c387ad49f2ee5b55f", CreateContractHash(sender, script).StringLE()) } + +func TestContractFromStackItem(t *testing.T) { + var ( + id = stackitem.Make(42) + counter = stackitem.Make(11) + chash = stackitem.Make(util.Uint160{1, 2, 3}.BytesBE()) + script = stackitem.Make([]byte{0, 9, 8}) + manifest = manifest.DefaultManifest("stack item") + manifestB, _ = json.Marshal(manifest) + manifItem = stackitem.Make(manifestB) + + badCases = []struct { + name string + item stackitem.Item + }{ + {"not an array", stackitem.Make(1)}, + {"id is not a number", stackitem.Make([]stackitem.Item{manifItem, counter, chash, script, manifItem})}, + {"id is out of range", stackitem.Make([]stackitem.Item{stackitem.Make(math.MaxUint32), counter, chash, script, manifItem})}, + {"counter is not a number", stackitem.Make([]stackitem.Item{id, manifItem, chash, script, manifItem})}, + {"counter is out of range", stackitem.Make([]stackitem.Item{id, stackitem.Make(100500), chash, script, manifItem})}, + {"hash is not a byte string", stackitem.Make([]stackitem.Item{id, counter, stackitem.NewArray(nil), script, manifItem})}, + {"hash is not a hash", stackitem.Make([]stackitem.Item{id, counter, stackitem.Make([]byte{1, 2, 3}), script, manifItem})}, + {"script is not a byte string", stackitem.Make([]stackitem.Item{id, counter, chash, stackitem.NewArray(nil), manifItem})}, + {"manifest is not a byte string", stackitem.Make([]stackitem.Item{id, counter, chash, script, stackitem.NewArray(nil)})}, + {"manifest is not correct", stackitem.Make([]stackitem.Item{id, counter, chash, script, stackitem.Make(100500)})}, + } + ) + for _, cs := range badCases { + t.Run(cs.name, func(t *testing.T) { + var c = new(Contract) + err := c.FromStackItem(cs.item) + require.Error(t, err) + }) + } + var c = new(Contract) + err := c.FromStackItem(stackitem.Make([]stackitem.Item{id, counter, chash, script, manifItem})) + require.NoError(t, err) +} diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 4e099498c..485d45104 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -13,7 +13,6 @@ const ( DataMPT KeyPrefix = 0x03 STAccount KeyPrefix = 0x40 STNotification KeyPrefix = 0x4d - STContract KeyPrefix = 0x50 STContractID KeyPrefix = 0x51 STStorage KeyPrefix = 0x70 STNEP17Transfers KeyPrefix = 0x72 @@ -21,7 +20,6 @@ const ( IXHeaderHashList KeyPrefix = 0x80 SYSCurrentBlock KeyPrefix = 0xc0 SYSCurrentHeader KeyPrefix = 0xc1 - SYSContractID KeyPrefix = 0xc2 SYSVersion KeyPrefix = 0xf0 ) diff --git a/pkg/core/storage/store_test.go b/pkg/core/storage/store_test.go index 9b1e579e0..c20904bbd 100644 --- a/pkg/core/storage/store_test.go +++ b/pkg/core/storage/store_test.go @@ -11,7 +11,6 @@ var ( DataBlock, DataTransaction, STAccount, - STContract, STStorage, IXHeaderHashList, SYSCurrentBlock, @@ -23,7 +22,6 @@ var ( 0x01, 0x02, 0x40, - 0x50, 0x70, 0x80, 0xc0, diff --git a/pkg/interop/blockchain/blockchain.go b/pkg/interop/blockchain/blockchain.go index a8d8e1659..fffd667fa 100644 --- a/pkg/interop/blockchain/blockchain.go +++ b/pkg/interop/blockchain/blockchain.go @@ -5,7 +5,6 @@ package blockchain import ( "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/contract" ) // Transaction represents a NEO transaction. It's similar to Transaction class @@ -95,11 +94,3 @@ func GetTransactionFromBlock(heightOrHash interface{}, index int) interop.Hash25 func GetTransactionHeight(hash interop.Hash256) int { return 0 } - -// GetContract returns contract found by the given script hash (160 bit in BE -// format represented as a slice of 20 bytes). Refer to the `contract` package -// for details on how to use the returned structure. This function uses -// `System.Blockchain.GetContract` syscall. -func GetContract(scriptHash interop.Hash160) *contract.Contract { - return &contract.Contract{} -} diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index cd8ab67e3..a42be36c5 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -5,15 +5,6 @@ package contract import "github.com/nspcc-dev/neo-go/pkg/interop" -// Contract represents a Neo contract and is used in interop functions. It's -// a data structure that you can manipulate with using functions from -// this package. It's similar in function to the Contract class in the Neo .net -// framework. -type Contract struct { - Script []byte - Manifest []byte -} - // CallFlag specifies valid call flags. type CallFlag byte @@ -30,30 +21,6 @@ const ( NoneFlag CallFlag = 0 ) -// Create creates a new contract using a set of input parameters: -// script contract's bytecode (limited in length by 1M) -// manifest contract's manifest (limited in length by 2 KiB) -// It returns this new created Contract when successful (and fails transaction -// if not). It uses `System.Contract.Create` syscall. -func Create(script []byte, manifest []byte) *Contract { - return &Contract{} -} - -// Update updates script and manifest of the calling contract (that is the one that calls Update) -// to the new ones. Its parameters have exactly the same semantics as for -// Create. The old contract will be deleted by this call, if it has any storage -// associated it will be migrated to the new contract. New contract is returned. -// This function uses `System.Contract.Update` syscall. -func Update(script []byte, manifest []byte) { - return -} - -// Destroy deletes calling contract (the one that calls Destroy) from the -// blockchain, so it's only possible to do that from the contract itself and -// not by any outside code. When contract is deleted all associated storage -// items are deleted too. This function uses `System.Contract.Destroy` syscall. -func Destroy() {} - // IsStandard checks if contract with provided hash is a standard signature/multisig contract. // This function uses `System.Contract.IsStandard` syscall. func IsStandard(h interop.Hash160) bool { diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 346266f62..764e0efe5 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -264,6 +264,9 @@ func (chain *testChain) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int { } panic("TODO") } +func (chain testChain) ManagementContractHash() util.Uint160 { + panic("TODO") +} func (chain *testChain) PoolTx(tx *transaction.Transaction, _ ...*mempool.Pool) error { return chain.poolTx(tx) diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index a405d7de1..0c5f58336 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -1,40 +1,18 @@ package request import ( - "encoding/json" "errors" "fmt" "strconv" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "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" ) -// CreateDeploymentScript returns a script that deploys given smart contract -// with its metadata. -func CreateDeploymentScript(ne *nef.File, manif *manifest.Manifest) ([]byte, error) { - script := io.NewBufBinWriter() - rawManifest, err := json.Marshal(manif) - if err != nil { - return nil, err - } - neb, err := ne.Bytes() - if err != nil { - return nil, err - } - emit.Bytes(script.BinWriter, rawManifest) - emit.Bytes(script.BinWriter, neb) - emit.Syscall(script.BinWriter, interopnames.SystemContractCreate) - return script.Bytes(), nil -} - // expandArrayIntoScript pushes all FuncParam parameters from the given array // into the given buffer in reverse order. func expandArrayIntoScript(script *io.BinWriter, slice []Param) error { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index d8cf65ed5..c01db8a29 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -57,7 +57,7 @@ type rpcTestCase struct { } const testContractHash = "743ed26f78e29ecd595535b74a943b1f9ccbc444" -const deploymentTxHash = "a72dfaebf9543964d74e803723dae6a86196e0915ae9d76b3cc57c3b2e3e8c49" +const deploymentTxHash = "8ec2d061ecb22115f1e25a8bf79e536134d0cd7f0ac31b89cc40960a21dc8171" const genesisBlockHash = "0542f4350c6e236d0509bcd98188b0034bfbecc1a0c7fcdb8e4295310d468b70" const verifyContractHash = "a2eb22340979804cb10cc1add0b8822c201f4d8a" @@ -1347,7 +1347,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "80009698770", + Amount: "80009634770", LastUpdated: 7, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 1d0bc04ed..a3fc08b76 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/scripts/gendump/main.go b/scripts/gendump/main.go index 3c4f90496..3a8cfc35b 100644 --- a/scripts/gendump/main.go +++ b/scripts/gendump/main.go @@ -74,7 +74,7 @@ func main() { handleError("can't tranfser GAS", err) lastBlock = addBlock(bc, lastBlock, valScript, txMoveNeo, txMoveGas) - tx, contractHash, err := testchain.NewDeployTx("DumpContract", h, strings.NewReader(contract)) + tx, contractHash, err := testchain.NewDeployTx(bc, "DumpContract", h, strings.NewReader(contract)) handleError("can't create deploy tx", err) tx.NetworkFee = 10_000_000 tx.ValidUntilBlock = bc.BlockHeight() + 1