From 62da365302bc8024b52e01cc314914409bc811d5 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 14 Dec 2020 12:41:23 +0300 Subject: [PATCH] native: allow to modify `StoragePrice` in the policy contract --- pkg/core/blockchain.go | 5 +++ pkg/core/blockchainer/policer.go | 1 + pkg/core/interop_system.go | 5 ++- pkg/core/native/management.go | 8 ++--- pkg/core/native/policy.go | 51 +++++++++++++++++++++++++++++ pkg/core/native_policy_test.go | 56 ++++++++++++++++++++++++++++++++ pkg/network/helper_test.go | 4 +++ 7 files changed, 123 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2be2c50d1..8098eef19 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1833,4 +1833,9 @@ func (bc *Blockchain) GetMaxVerificationGAS() int64 { return bc.contracts.Policy.GetMaxVerificationGas(bc.dao) } +// GetStoragePrice returns current storage price. +func (bc *Blockchain) GetStoragePrice() int64 { + return bc.contracts.Policy.GetStoragePriceInternal(bc.dao) +} + // -- end Policer. diff --git a/pkg/core/blockchainer/policer.go b/pkg/core/blockchainer/policer.go index 48ae7c0af..abfe4aa02 100644 --- a/pkg/core/blockchainer/policer.go +++ b/pkg/core/blockchainer/policer.go @@ -6,4 +6,5 @@ type Policer interface { GetMaxBlockSize() uint32 GetMaxBlockSystemFee() int64 GetMaxVerificationGAS() int64 + GetStoragePrice() int64 } diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 254ab9c44..1edeb9012 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -11,7 +11,6 @@ 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" @@ -201,7 +200,7 @@ func storageDelete(ic *interop.Context) error { if stc.ReadOnly { return errors.New("StorageContext is read only") } - ic.VM.AddGas(native.StoragePrice) + ic.VM.AddGas(ic.Chain.GetPolicer().GetStoragePrice()) key := ic.VM.Estack().Pop().Bytes() si := ic.DAO.GetStorageItem(stc.ID, key) if si != nil && si.IsConst { @@ -277,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) * native.StoragePrice) { + if !ic.VM.AddGas(int64(sizeInc) * ic.Chain.GetPolicer().GetStoragePrice()) { return errGasLimitExceeded } si.Value = value diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 04631c5e8..92be3e91f 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -119,7 +119,7 @@ func getLimitedSlice(arg stackitem.Item, max int) ([]byte, error) { // 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) { +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) @@ -129,7 +129,7 @@ func getNefAndManifestFromItems(args []stackitem.Item, v *vm.VM) (*nef.File, *ma return nil, nil, fmt.Errorf("invalid manifest: %w", err) } - if !v.AddGas(int64(StoragePrice * (len(nefBytes) + len(manifestBytes)))) { + if !ic.VM.AddGas(ic.Chain.GetPolicer().GetStoragePrice() * int64(len(nefBytes)+len(manifestBytes))) { return nil, nil, errGasLimitExceeded } var resManifest *manifest.Manifest @@ -154,7 +154,7 @@ func getNefAndManifestFromItems(args []stackitem.Item, v *vm.VM) (*nef.File, *ma // 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) + neff, manif, err := getNefAndManifestFromItems(ic, args) if err != nil { panic(err) } @@ -208,7 +208,7 @@ func (m *Management) Deploy(d dao.DAO, sender util.Uint160, neff *nef.File, mani // 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) + neff, manif, err := getNefAndManifestFromItems(ic, args) if err != nil { panic(err) } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index e72b7b337..3c7411315 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -34,6 +34,8 @@ const ( maxExecFeeFactor = 1000 // 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 // blockedAccountPrefix is a prefix used to store blocked account. blockedAccountPrefix = 15 @@ -52,6 +54,8 @@ var ( maxBlockSizeKey = []byte{12} // maxBlockSystemFeeKey is a key used to store the maximum block system fee value. maxBlockSystemFeeKey = []byte{17} + // storagePriceKey is a key used to store storage price. + storagePriceKey = []byte{19} ) // Policy represents Policy native contract. @@ -68,6 +72,7 @@ type Policy struct { feePerByte int64 maxBlockSystemFee int64 maxVerificationGas int64 + storagePrice uint32 blockedAccounts []util.Uint160 } @@ -109,6 +114,15 @@ func newPolicy() *Policy { md = newMethodAndPrice(p.setExecFeeFactor, 3000000, smartcontract.WriteStates) p.AddMethod(md, desc) + desc = newDescriptor("getStoragePrice", smartcontract.IntegerType) + md = newMethodAndPrice(p.getStoragePrice, 1000000, smartcontract.ReadStates) + p.AddMethod(md, desc) + + desc = newDescriptor("setStoragePrice", smartcontract.BoolType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(p.setStoragePrice, 3000000, smartcontract.WriteStates) + p.AddMethod(md, desc) + desc = newDescriptor("setMaxBlockSize", smartcontract.BoolType, manifest.NewParameter("value", smartcontract.IntegerType)) md = newMethodAndPrice(p.setMaxBlockSize, 3000000, smartcontract.WriteStates) @@ -156,6 +170,7 @@ func (p *Policy) Initialize(ic *interop.Context) error { p.feePerByte = defaultFeePerByte p.maxBlockSystemFee = defaultMaxBlockSystemFee p.maxVerificationGas = defaultMaxVerificationGas + p.storagePrice = StoragePrice p.blockedAccounts = make([]util.Uint160, 0) return nil @@ -180,6 +195,7 @@ func (p *Policy) PostPersist(ic *interop.Context) error { p.feePerByte = getInt64WithKey(p.ContractID, ic.DAO, feePerByteKey, defaultFeePerByte) p.maxBlockSystemFee = getInt64WithKey(p.ContractID, ic.DAO, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) p.maxVerificationGas = defaultMaxVerificationGas + p.storagePrice = getUint32WithKey(p.ContractID, ic.DAO, storagePriceKey, StoragePrice) p.blockedAccounts = make([]util.Uint160, 0) siMap, err := ic.DAO.GetStorageItemsWithPrefix(p.ContractID, []byte{blockedAccountPrefix}) @@ -332,6 +348,41 @@ func (p *Policy) IsBlockedInternal(dao dao.DAO, hash util.Uint160) bool { return dao.GetStorageItem(p.ContractID, 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.DAO) int64 { + p.lock.RLock() + defer p.lock.RUnlock() + if p.isValid { + return int64(p.storagePrice) + } + return int64(getUint32WithKey(p.ContractID, d, storagePriceKey, 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 0 and %d", maxStoragePrice)) + } + ok, err := checkValidators(ic) + if err != nil { + panic(err) + } else if !ok { + return stackitem.NewBool(false) + } + p.lock.Lock() + defer p.lock.Unlock() + err = setUint32WithKey(p.ContractID, ic.DAO, storagePriceKey, uint32(value)) + if err != nil { + panic(err) + } + p.isValid = false + return stackitem.NewBool(true) +} + // setMaxTransactionsPerBlock is Policy contract method and sets the upper limit // of transactions per block. func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item { diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index 2fd372cbd..d843b3109 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -242,6 +242,62 @@ func TestBlockSystemFee(t *testing.T) { }) } +func TestStoragePrice(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash + + t.Run("get, internal method", func(t *testing.T) { + n := chain.contracts.Policy.GetStoragePriceInternal(chain.dao) + require.Equal(t, int64(native.StoragePrice), n) + }) + + t.Run("get", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, policyHash, "getStoragePrice") + require.NoError(t, err) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(native.StoragePrice))) + require.NoError(t, chain.persist()) + }) + + t.Run("set, zero fee", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, policyHash, "setStoragePrice", int64(0)) + require.NoError(t, err) + checkFAULTState(t, res) + }) + + t.Run("set, too big fee", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, policyHash, "setStoragePrice", int64(10000001)) + require.NoError(t, err) + checkFAULTState(t, res) + }) + + t.Run("set, success", func(t *testing.T) { + // Set and get in the same block. + txSet, err := prepareContractMethodInvoke(chain, 100000000, policyHash, "setStoragePrice", int64(12345)) + require.NoError(t, err) + txGet1, err := prepareContractMethodInvoke(chain, 100000000, policyHash, "getStoragePrice") + require.NoError(t, err) + aers, err := persistBlock(chain, txSet, txGet1) + require.NoError(t, err) + checkResult(t, aers[0], stackitem.NewBool(true)) + checkResult(t, aers[1], stackitem.Make(12345)) + require.NoError(t, chain.persist()) + + // Get in the next block. + res, err := invokeContractMethod(chain, 100000000, policyHash, "getStoragePrice") + require.NoError(t, err) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(12345))) + require.NoError(t, chain.persist()) + }) + + t.Run("set, not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setStoragePrice", int64(100)) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) +} + func TestBlockedAccounts(t *testing.T) { chain := newTestChain(t) defer chain.Close() diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 7394cdf4c..b4c98b223 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "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" @@ -105,6 +106,9 @@ func (chain *testChain) GetPolicer() blockchainer.Policer { func (chain *testChain) GetBaseExecFee() int64 { return interop.DefaultBaseExecFee } +func (chain *testChain) GetStoragePrice() int64 { + return native.StoragePrice +} func (chain *testChain) GetMaxVerificationGAS() int64 { if chain.maxVerificationGAS != 0 { return chain.maxVerificationGAS