neo-go/pkg/core/native/policy.go

375 lines
12 KiB
Go
Raw Normal View History

package native
import (
"fmt"
"math/big"
"sort"
"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/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/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
const (
2021-02-15 15:43:10 +00:00
policyContractID = -7
defaultExecFeeFactor = interop.DefaultBaseExecFee
defaultFeePerByte = 1000
defaultMaxVerificationGas = 1_50000000
// DefaultStoragePrice is the price to pay for 1 byte of storage.
DefaultStoragePrice = 100000
// maxExecFeeFactor is the maximum allowed execution fee factor.
maxExecFeeFactor = 100
// maxFeePerByte is the maximum allowed fee per byte value.
maxFeePerByte = 100_000_000
// maxStoragePrice is the maximum allowed price for a byte of storage.
maxStoragePrice = 10000000
2020-10-21 12:51:59 +00:00
// blockedAccountPrefix is a prefix used to store blocked account.
blockedAccountPrefix = 15
)
var (
// execFeeFactorKey is a key used to store execution fee factor.
execFeeFactorKey = []byte{18}
// feePerByteKey is a key used to store the minimum fee per byte for
// transaction.
feePerByteKey = []byte{10}
// storagePriceKey is a key used to store storage price.
storagePriceKey = []byte{19}
)
// Policy represents Policy native contract.
type Policy struct {
interop.ContractMD
NEO *NEO
}
type PolicyCache struct {
// isValid defies whether cached values were changed during the current
// consensus iteration. If false, these values will be updated after
// blockchain DAO persisting. If true, we can safely use cached values.
isValid bool
execFeeFactor uint32
feePerByte int64
maxVerificationGas int64
storagePrice uint32
blockedAccounts []util.Uint160
}
var (
_ interop.Contract = (*Policy)(nil)
_ storage.NativeContractCache = (*PolicyCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *PolicyCache) Copy() storage.NativeContractCache {
cp := &PolicyCache{}
copyPolicyCache(c, cp)
return cp
}
func copyPolicyCache(src, dst *PolicyCache) {
*dst = *src
dst.blockedAccounts = make([]util.Uint160, len(src.blockedAccounts))
copy(dst.blockedAccounts, src.blockedAccounts)
}
// newPolicy returns Policy native contract.
func newPolicy() *Policy {
p := &Policy{ContractMD: *interop.NewContractMD(nativenames.Policy, policyContractID)}
defer p.UpdateHash()
desc := newDescriptor("getFeePerByte", smartcontract.IntegerType)
md := newMethodAndPrice(p.getFeePerByte, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
2020-10-21 12:51:59 +00:00
desc = newDescriptor("isBlocked", smartcontract.BoolType,
manifest.NewParameter("account", smartcontract.Hash160Type))
md = newMethodAndPrice(p.isBlocked, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = newDescriptor("getExecFeeFactor", smartcontract.IntegerType)
md = newMethodAndPrice(p.getExecFeeFactor, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = newDescriptor("setExecFeeFactor", smartcontract.VoidType,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setExecFeeFactor, 1<<15, callflag.States)
p.AddMethod(md, desc)
desc = newDescriptor("getStoragePrice", smartcontract.IntegerType)
md = newMethodAndPrice(p.getStoragePrice, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = newDescriptor("setStoragePrice", smartcontract.VoidType,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setStoragePrice, 1<<15, callflag.States)
p.AddMethod(md, desc)
desc = newDescriptor("setFeePerByte", smartcontract.VoidType,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setFeePerByte, 1<<15, callflag.States)
p.AddMethod(md, desc)
desc = newDescriptor("blockAccount", smartcontract.BoolType,
manifest.NewParameter("account", smartcontract.Hash160Type))
md = newMethodAndPrice(p.blockAccount, 1<<15, callflag.States)
p.AddMethod(md, desc)
desc = newDescriptor("unblockAccount", smartcontract.BoolType,
manifest.NewParameter("account", smartcontract.Hash160Type))
md = newMethodAndPrice(p.unblockAccount, 1<<15, callflag.States)
p.AddMethod(md, desc)
return p
}
// Metadata implements Contract interface.
func (p *Policy) Metadata() *interop.ContractMD {
return &p.ContractMD
}
// Initialize initializes Policy native contract and implements Contract interface.
func (p *Policy) Initialize(ic *interop.Context) error {
setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte)
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor)
setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice)
cache := &PolicyCache{}
cache.isValid = true
cache.execFeeFactor = defaultExecFeeFactor
cache.feePerByte = defaultFeePerByte
cache.maxVerificationGas = defaultMaxVerificationGas
cache.storagePrice = DefaultStoragePrice
cache.blockedAccounts = make([]util.Uint160, 0)
ic.DAO.Store.SetCache(p.ID, cache)
return nil
}
func (p *Policy) InitializeCache(d *dao.Simple) error {
cache := &PolicyCache{}
err := p.fillCacheFromDAO(cache, d)
if err != nil {
return err
}
d.Store.SetCache(p.ID, cache)
return nil
}
func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error {
cache.execFeeFactor = uint32(getIntWithKey(p.ID, d, execFeeFactorKey))
cache.feePerByte = getIntWithKey(p.ID, d, feePerByteKey)
cache.maxVerificationGas = defaultMaxVerificationGas
cache.storagePrice = uint32(getIntWithKey(p.ID, d, storagePriceKey))
core: fix bug with policy.MaxVerificationGas In 121c9664b we should take into account isValid flag of NativePolicy contract while retrieving MaxVerificationGas native policy value. Otherwise we won't be able to get MaxVerificationGas after the node was restarted, because this value is not truly stored along with the other native policy values. This commit fixes bug with headers verification after the node restarting with an existing storage: ``` 2020-08-03T12:52:56.158+0300 WARN failed processing headers {"error": "vm failed to execute the script with error: error encountered at instruction 0 (PUSHDATA1): gas limit is exceeded", "errorVerbose": "vm failed to execute the script with error: error encountered at instruction 0 (PUSHDATA1): gas limit is exceeded\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).verifyHashAgainstScript\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:1454\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).verifyHeaderWitnesses\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:1517\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).verifyHeader\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:1175\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).addHeaders\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:484\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).AddHeaders\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:453\ngithub.com/nspcc-dev/neo-go/pkg/network.(*Server).handleHeadersCmd\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/network/server.go:454\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1373"} ```
2020-08-03 10:30:51 +00:00
cache.blockedAccounts = make([]util.Uint160, 0)
var fErr error
d.Seek(p.ID, storage.SeekRange{Prefix: []byte{blockedAccountPrefix}}, func(k, _ []byte) bool {
hash, err := util.Uint160DecodeBytesBE(k)
2020-10-21 12:51:59 +00:00
if err != nil {
fErr = fmt.Errorf("failed to decode blocked account hash: %w", err)
return false
2020-10-21 12:51:59 +00:00
}
cache.blockedAccounts = append(cache.blockedAccounts, hash)
return true
})
if fErr != nil {
return fmt.Errorf("failed to initialize blocked accounts: %w", fErr)
2020-10-21 12:51:59 +00:00
}
cache.isValid = true
return nil
}
// OnPersist implements Contract interface.
func (p *Policy) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements Contract interface.
func (p *Policy) PostPersist(ic *interop.Context) error {
cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache)
if cache.isValid {
return nil
}
return p.fillCacheFromDAO(cache, ic.DAO)
}
// getFeePerByte is Policy contract method and returns required transaction's fee
// per byte.
func (p *Policy) getFeePerByte(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(p.GetFeePerByteInternal(ic.DAO)))
}
// GetFeePerByteInternal returns required transaction's fee per byte.
func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 {
cache := dao.Store.GetROCache(p.ID).(*PolicyCache)
if cache.isValid {
return cache.feePerByte
}
return getIntWithKey(p.ID, dao, feePerByteKey)
}
// GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion.
func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 {
cache := dao.Store.GetROCache(p.ID).(*PolicyCache)
if cache.isValid {
return cache.maxVerificationGas
core: fix bug with policy.MaxVerificationGas In 121c9664b we should take into account isValid flag of NativePolicy contract while retrieving MaxVerificationGas native policy value. Otherwise we won't be able to get MaxVerificationGas after the node was restarted, because this value is not truly stored along with the other native policy values. This commit fixes bug with headers verification after the node restarting with an existing storage: ``` 2020-08-03T12:52:56.158+0300 WARN failed processing headers {"error": "vm failed to execute the script with error: error encountered at instruction 0 (PUSHDATA1): gas limit is exceeded", "errorVerbose": "vm failed to execute the script with error: error encountered at instruction 0 (PUSHDATA1): gas limit is exceeded\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).verifyHashAgainstScript\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:1454\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).verifyHeaderWitnesses\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:1517\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).verifyHeader\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:1175\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).addHeaders\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:484\ngithub.com/nspcc-dev/neo-go/pkg/core.(*Blockchain).AddHeaders\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/core/blockchain.go:453\ngithub.com/nspcc-dev/neo-go/pkg/network.(*Server).handleHeadersCmd\n\t/home/neospcc/Documents/GitProjects/nspcc-dev/neo-go/pkg/network/server.go:454\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1373"} ```
2020-08-03 10:30:51 +00:00
}
return defaultMaxVerificationGas
}
func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(int64(p.GetExecFeeFactorInternal(ic.DAO))))
}
// GetExecFeeFactorInternal returns current execution fee factor.
func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 {
cache := d.Store.GetROCache(p.ID).(*PolicyCache)
if cache.isValid {
return int64(cache.execFeeFactor)
}
return getIntWithKey(p.ID, d, execFeeFactorKey)
}
func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toUint32(args[0])
if value <= 0 || maxExecFeeFactor < value {
panic(fmt.Errorf("ExecFeeFactor must be between 0 and %d", maxExecFeeFactor))
}
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache)
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value))
cache.isValid = false
return stackitem.Null{}
}
2020-10-21 12:51:59 +00:00
// isBlocked is Policy contract method and checks whether provided account is blocked.
func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item {
hash := toUint160(args[0])
return stackitem.NewBool(p.IsBlockedInternal(ic.DAO, hash))
}
// IsBlockedInternal checks whether provided account is blocked.
func (p *Policy) IsBlockedInternal(dao *dao.Simple, hash util.Uint160) bool {
cache := dao.Store.GetROCache(p.ID).(*PolicyCache)
if cache.isValid {
length := len(cache.blockedAccounts)
2020-10-21 12:51:59 +00:00
i := sort.Search(length, func(i int) bool {
return !cache.blockedAccounts[i].Less(hash)
2020-10-21 12:51:59 +00:00
})
if length != 0 && i != length && cache.blockedAccounts[i].Equals(hash) {
2020-10-21 12:51:59 +00:00
return true
}
return false
}
2020-10-21 12:51:59 +00:00
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
return dao.GetStorageItem(p.ID, key) != nil
}
func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(p.GetStoragePriceInternal(ic.DAO)))
}
// GetStoragePriceInternal returns current execution fee factor.
func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 {
cache := d.Store.GetROCache(p.ID).(*PolicyCache)
if cache.isValid {
return int64(cache.storagePrice)
}
return getIntWithKey(p.ID, d, storagePriceKey)
}
func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toUint32(args[0])
if value <= 0 || maxStoragePrice < value {
panic(fmt.Errorf("StoragePrice must be between 0 and %d", maxStoragePrice))
}
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache)
setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value))
cache.isValid = false
return stackitem.Null{}
}
// setFeePerByte is Policy contract method and sets transaction's fee per byte.
func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toBigInt(args[0]).Int64()
if value < 0 || value > maxFeePerByte {
panic(fmt.Errorf("FeePerByte shouldn't be negative or greater than %d", maxFeePerByte))
}
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache)
setIntWithKey(p.ID, ic.DAO, feePerByteKey, value)
cache.isValid = false
return stackitem.Null{}
}
// blockAccount is Policy contract method and adds given account hash to the list
// of blocked accounts.
func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
2020-10-21 12:51:59 +00:00
hash := toUint160(args[0])
for i := range ic.Natives {
if ic.Natives[i].Metadata().Hash == hash {
panic("cannot block native contract")
}
}
2020-10-21 12:51:59 +00:00
if p.IsBlockedInternal(ic.DAO, hash) {
return stackitem.NewBool(false)
}
2020-10-21 12:51:59 +00:00
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache)
ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{})
cache.isValid = false
return stackitem.NewBool(true)
}
// unblockAccount is Policy contract method and removes given account hash from
// the list of blocked accounts.
func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
2020-10-21 12:51:59 +00:00
hash := toUint160(args[0])
if !p.IsBlockedInternal(ic.DAO, hash) {
return stackitem.NewBool(false)
}
2020-10-21 12:51:59 +00:00
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache)
ic.DAO.DeleteStorageItem(p.ID, key)
cache.isValid = false
return stackitem.NewBool(true)
}
// CheckPolicy checks whether transaction conforms to current policy restrictions
// like not being signed by blocked account or not exceeding block-level system
// fee limit.
func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error {
2020-10-21 12:51:59 +00:00
for _, signer := range tx.Signers {
if p.IsBlockedInternal(d, signer.Account) {
return fmt.Errorf("account %s is blocked", signer.Account.StringLE())
}
}
return nil
}