neoneo-go/pkg/core/native/management.go
2020-12-18 13:11:17 +03:00

455 lines
12 KiB
Go

package native
import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"sync"
"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/native/nativenames"
"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/encoding/bigint"
"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/stackitem"
)
// Management is contract-managing native contract.
type Management struct {
interop.ContractMD
mtx sync.RWMutex
contracts map[util.Uint160]*state.Contract
}
// StoragePrice is the price to pay for 1 byte of storage.
const StoragePrice = 100000
const (
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(nativenames.Management),
contracts: make(map[util.Uint160]*state.Contract),
}
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) {
m.mtx.RLock()
cs, ok := m.contracts[hash]
m.mtx.RUnlock()
if !ok {
return nil, storage.ErrKeyNotFound
} else if cs != nil {
return cs, nil
}
return m.getContractFromDAO(d, hash)
}
func (m *Management) getContractFromDAO(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(ic *interop.Context, args []stackitem.Item) (*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 !ic.VM.AddGas(ic.Chain.GetPolicer().GetStoragePrice() * int64(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(ic, args)
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)
}
func (m *Management) markUpdated(h util.Uint160) {
m.mtx.Lock()
// Just set it to nil, to refresh cache in `PostPersist`.
m.contracts[h] = nil
m.mtx.Unlock()
}
// 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
}
m.markUpdated(newcontract.Hash)
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(ic, args)
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 {
m.markUpdated(hash)
contract.Script = neff.Script
}
// if manifest was provided, update the contract manifest
if manif != nil {
if !manif.IsValid(contract.Hash) {
return nil, errors.New("invalid manifest for this contract")
}
m.markUpdated(hash)
contract.Manifest = *manif
}
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
}
}
m.markUpdated(hash)
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
}
// OnPersist implements Contract interface.
func (m *Management) OnPersist(ic *interop.Context) error {
if ic.Block.Index != 0 { // We're only deploying at 0 at the moment.
return nil
}
for _, native := range ic.Natives {
md := native.Metadata()
cs := &state.Contract{
ID: md.ContractID,
Hash: md.Hash,
Script: md.Script,
Manifest: md.Manifest,
}
err := m.PutContractState(ic.DAO, cs)
if err != nil {
return err
}
if err := native.Initialize(ic); err != nil {
return fmt.Errorf("initializing %s native contract: %w", md.Name, err)
}
m.mtx.Lock()
m.contracts[md.Hash] = cs
m.mtx.Unlock()
}
return nil
}
// InitializeCache initializes contract cache with the proper values from storage.
// Cache initialisation should be done apart from Initialize because Initialize is
// called only when deploying native contracts.
func (m *Management) InitializeCache(d dao.DAO) error {
m.mtx.Lock()
defer m.mtx.Unlock()
var initErr error
d.Seek(m.ContractID, []byte{prefixContract}, func(_, v []byte) {
var r = io.NewBinReaderFromBuf(v)
var si state.StorageItem
si.DecodeBinary(r)
if r.Err != nil {
initErr = r.Err
return
}
var cs state.Contract
r = io.NewBinReaderFromBuf(si.Value)
cs.DecodeBinary(r)
if r.Err != nil {
initErr = r.Err
return
}
m.contracts[cs.Hash] = &cs
})
return initErr
}
// PostPersist implements Contract interface.
func (m *Management) PostPersist(ic *interop.Context) error {
m.mtx.Lock()
for h, cs := range m.contracts {
if cs != nil {
continue
}
newCs, err := m.getContractFromDAO(ic.DAO, h)
if err != nil {
// Contract was destroyed.
delete(m.contracts, h)
continue
}
m.contracts[h] = newCs
}
m.mtx.Unlock()
return nil
}
// Initialize implements Contract interface.
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
}
m.markUpdated(cs.Hash)
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)
}