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

477 lines
17 KiB
Go
Raw Normal View History

package native
import (
"encoding/hex"
"fmt"
"maps"
"math/big"
"slices"
"sort"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/native/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/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
const (
2021-02-15 15:43:10 +00:00
policyContractID = -7
defaultExecFeeFactor = interop.DefaultBaseExecFee
defaultFeePerByte = 1000
defaultMaxVerificationGas = 1_50000000
// defaultAttributeFee is a default fee for a transaction attribute those price wasn't set yet.
defaultAttributeFee = 0
// defaultNotaryAssistedFee is a default fee for a NotaryAssisted transaction attribute per key.
defaultNotaryAssistedFee = 1000_0000 // 0.1 GAS
// 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
// maxAttributeFee is the maximum allowed value for a transaction attribute fee.
maxAttributeFee = 10_00000000
2020-10-21 12:51:59 +00:00
// blockedAccountPrefix is a prefix used to store blocked account.
blockedAccountPrefix = 15
// attributeFeePrefix is a prefix used to store attribute fee.
attributeFeePrefix = 20
)
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
// p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant.
p2pSigExtensionsEnabled bool
}
type PolicyCache struct {
execFeeFactor uint32
feePerByte int64
maxVerificationGas int64
storagePrice uint32
attributeFee map[transaction.AttrType]uint32
blockedAccounts []util.Uint160
}
var (
_ interop.Contract = (*Policy)(nil)
_ dao.NativeContractCache = (*PolicyCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *PolicyCache) Copy() dao.NativeContractCache {
cp := &PolicyCache{}
copyPolicyCache(c, cp)
return cp
}
func copyPolicyCache(src, dst *PolicyCache) {
*dst = *src
dst.attributeFee = maps.Clone(src.attributeFee)
dst.blockedAccounts = slices.Clone(src.blockedAccounts)
}
// newPolicy returns Policy native contract.
func newPolicy(p2pSigExtensionsEnabled bool) *Policy {
p := &Policy{
ContractMD: *interop.NewContractMD(nativenames.Policy, policyContractID),
p2pSigExtensionsEnabled: p2pSigExtensionsEnabled,
}
defer p.BuildHFSpecificMD(p.ActiveIn())
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("getAttributeFee", smartcontract.IntegerType,
manifest.NewParameter("attributeType", smartcontract.IntegerType))
md = newMethodAndPrice(p.getAttributeFee, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = newDescriptor("setAttributeFee", smartcontract.VoidType,
manifest.NewParameter("attributeType", smartcontract.IntegerType),
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setAttributeFee, 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 the Contract interface.
func (p *Policy) Metadata() *interop.ContractMD {
return &p.ContractMD
}
// Initialize initializes Policy native contract and implements the Contract interface.
func (p *Policy) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
if hf != p.ActiveIn() {
return nil
}
setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte)
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor)
setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice)
cache := &PolicyCache{
execFeeFactor: defaultExecFeeFactor,
feePerByte: defaultFeePerByte,
maxVerificationGas: defaultMaxVerificationGas,
storagePrice: DefaultStoragePrice,
attributeFee: map[transaction.AttrType]uint32{},
blockedAccounts: make([]util.Uint160, 0),
}
if p.p2pSigExtensionsEnabled {
setIntWithKey(p.ID, ic.DAO, []byte{attributeFeePrefix, byte(transaction.NotaryAssistedT)}, defaultNotaryAssistedFee)
cache.attributeFee[transaction.NotaryAssistedT] = defaultNotaryAssistedFee
}
ic.DAO.SetCache(p.ID, cache)
return nil
}
func (p *Policy) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &PolicyCache{}
err := p.fillCacheFromDAO(cache, d)
if err != nil {
return err
}
d.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.attributeFee = make(map[transaction.AttrType]uint32)
d.Seek(p.ID, storage.SeekRange{Prefix: []byte{attributeFeePrefix}}, func(k, v []byte) bool {
if len(k) != 1 {
fErr = fmt.Errorf("unexpected attribute type len %d (%s)", len(k), hex.EncodeToString(k))
return false
}
t := transaction.AttrType(k[0])
value := bigint.FromBytes(v)
if value == nil {
fErr = fmt.Errorf("unexpected attribute value format: key=%s, value=%s", hex.EncodeToString(k), hex.EncodeToString(v))
return false
}
cache.attributeFee[t] = uint32(value.Int64())
return true
})
if fErr != nil {
return fmt.Errorf("failed to initialize attribute fees: %w", fErr)
}
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 {
return nil
}
// ActiveIn implements the Contract interface.
func (p *Policy) ActiveIn() *config.Hardfork {
return nil
}
// getFeePerByte is a Policy contract method that returns the 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.GetROCache(p.ID).(*PolicyCache)
return cache.feePerByte
}
// GetMaxVerificationGas returns the maximum gas allowed to be burned during verification.
func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 {
cache := dao.GetROCache(p.ID).(*PolicyCache)
return cache.maxVerificationGas
}
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.GetROCache(p.ID).(*PolicyCache)
return int64(cache.execFeeFactor)
}
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 1 and %d", maxExecFeeFactor))
}
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value))
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.execFeeFactor = value
return stackitem.Null{}
}
// isBlocked is Policy contract method that checks whether provided account is blocked.
2020-10-21 12:51:59 +00:00
func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item {
hash := toUint160(args[0])
_, blocked := p.isBlockedInternal(ic.DAO.GetROCache(p.ID).(*PolicyCache), hash)
return stackitem.NewBool(blocked)
}
// IsBlocked checks whether provided account is blocked. Normally it uses Policy
// cache, falling back to the DB queries when Policy cache is not available yet
// (the only case is native cache initialization pipeline, where native Neo cache
// is being initialized before the Policy's one).
func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool {
cache := dao.GetROCache(p.ID)
if cache == nil {
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
return dao.GetStorageItem(p.ID, key) != nil
}
_, isBlocked := p.isBlockedInternal(cache.(*PolicyCache), hash)
return isBlocked
}
// isBlockedInternal checks whether provided account is blocked. It returns position
// of the blocked account in the blocked accounts list (or the position it should be
// put at).
func (p *Policy) isBlockedInternal(roCache *PolicyCache, hash util.Uint160) (int, bool) {
length := len(roCache.blockedAccounts)
i := sort.Search(length, func(i int) bool {
return !roCache.blockedAccounts[i].Less(hash)
})
if length != 0 && i != length && roCache.blockedAccounts[i].Equals(hash) {
return i, true
}
return i, false
}
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.GetROCache(p.ID).(*PolicyCache)
return int64(cache.storagePrice)
}
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 1 and %d", maxStoragePrice))
}
2021-01-21 12:05:15 +00:00
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value))
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.storagePrice = value
return stackitem.Null{}
}
func (p *Policy) getAttributeFee(ic *interop.Context, args []stackitem.Item) stackitem.Item {
t := transaction.AttrType(toUint8(args[0]))
if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) {
panic(fmt.Errorf("invalid attribute type: %d", t))
}
return stackitem.NewBigInteger(big.NewInt(p.GetAttributeFeeInternal(ic.DAO, t)))
}
// GetAttributeFeeInternal returns required transaction's attribute fee.
func (p *Policy) GetAttributeFeeInternal(d *dao.Simple, t transaction.AttrType) int64 {
cache := d.GetROCache(p.ID).(*PolicyCache)
v, ok := cache.attributeFee[t]
if !ok {
// We may safely omit this part, but let it be here in case if defaultAttributeFee value is changed.
v = defaultAttributeFee
}
return int64(v)
}
func (p *Policy) setAttributeFee(ic *interop.Context, args []stackitem.Item) stackitem.Item {
t := transaction.AttrType(toUint8(args[0]))
value := toUint32(args[1])
if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) {
panic(fmt.Errorf("invalid attribute type: %d", t))
}
if value > maxAttributeFee {
panic(fmt.Errorf("attribute value is out of range: %d", value))
}
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
setIntWithKey(p.ID, ic.DAO, []byte{attributeFeePrefix, byte(t)}, int64(value))
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.attributeFee[t] = value
return stackitem.Null{}
}
// setFeePerByte is a Policy contract method that 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")
}
setIntWithKey(p.ID, ic.DAO, feePerByteKey, value)
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.feePerByte = value
return stackitem.Null{}
}
// blockAccount is a Policy contract method that adds the 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")
}
}
2022-05-04 10:27:41 +00:00
return stackitem.NewBool(p.blockAccountInternal(ic.DAO, hash))
}
func (p *Policy) blockAccountInternal(d *dao.Simple, hash util.Uint160) bool {
i, blocked := p.isBlockedInternal(d.GetROCache(p.ID).(*PolicyCache), hash)
if blocked {
2022-05-04 10:27:41 +00:00
return false
}
2020-10-21 12:51:59 +00:00
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
2022-05-04 10:27:41 +00:00
d.PutStorageItem(p.ID, key, state.StorageItem{})
cache := d.GetRWCache(p.ID).(*PolicyCache)
if len(cache.blockedAccounts) == i {
cache.blockedAccounts = append(cache.blockedAccounts, hash)
} else {
cache.blockedAccounts = append(cache.blockedAccounts[:i+1], cache.blockedAccounts[i:]...)
cache.blockedAccounts[i] = hash
}
2022-05-04 10:27:41 +00:00
return true
}
// unblockAccount is a Policy contract method that removes the 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])
i, blocked := p.isBlockedInternal(ic.DAO.GetROCache(p.ID).(*PolicyCache), hash)
if !blocked {
return stackitem.NewBool(false)
}
2020-10-21 12:51:59 +00:00
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
ic.DAO.DeleteStorageItem(p.ID, key)
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.blockedAccounts = append(cache.blockedAccounts[:i], cache.blockedAccounts[i+1:]...)
return stackitem.NewBool(true)
}
// CheckPolicy checks whether a transaction conforms to the current policy restrictions,
// like not being signed by a blocked account or not exceeding the block-level system
// fee limit.
func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error {
cache := d.GetROCache(p.ID).(*PolicyCache)
2020-10-21 12:51:59 +00:00
for _, signer := range tx.Signers {
if _, isBlocked := p.isBlockedInternal(cache, signer.Account); isBlocked {
2020-10-21 12:51:59 +00:00
return fmt.Errorf("account %s is blocked", signer.Account.StringLE())
}
}
return nil
}