diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index fe1998a89..2be2c50d1 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1815,7 +1815,7 @@ func (bc *Blockchain) GetPolicer() blockchainer.Policer { // GetBaseExecFee return execution price for `NOP`. func (bc *Blockchain) GetBaseExecFee() int64 { - return interop.DefaultBaseExecFee + return bc.contracts.Policy.GetExecFeeFactorInternal(bc.dao) } // GetMaxBlockSize returns maximum allowed block size from native Policy contract. diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 2742ecfbc..003fea1f4 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -413,7 +413,8 @@ func addNetworkFee(bc *Blockchain, tx *transaction.Transaction, sender *wallet.A return nil } -func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { +func prepareContractMethodInvoke(chain *Blockchain, sysfee int64, + hash util.Uint160, method string, args ...interface{}) (*transaction.Transaction, error) { w := io.NewBufBinWriter() emit.AppCallWithOperationAndArgs(w.BinWriter, hash, method, args...) if w.Err != nil { @@ -427,17 +428,37 @@ func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, me if err != nil { return nil, err } - b := chain.newBlock(tx) - err = chain.AddBlock(b) + return tx, nil +} + +func persistBlock(chain *Blockchain, txs ...*transaction.Transaction) ([]*state.AppExecResult, error) { + b := chain.newBlock(txs...) + err := chain.AddBlock(b) if err != nil { return nil, err } - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) + aers := make([]*state.AppExecResult, len(txs)) + for i, tx := range txs { + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) + if err != nil { + return nil, err + } + aers[i] = &res[0] + } + return aers, nil +} + +func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { + tx, err := prepareContractMethodInvoke(chain, sysfee, hash, method, args...) if err != nil { return nil, err } - return &res[0], nil + aers, err := persistBlock(chain, tx) + if err != nil { + return nil, err + } + return aers[0], nil } func invokeContractMethodBy(t *testing.T, chain *Blockchain, signer *wallet.Account, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 192aa7728..e72b7b337 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -24,11 +24,14 @@ const ( defaultMaxBlockSize = 1024 * 256 defaultMaxTransactionsPerBlock = 512 + defaultExecFeeFactor = interop.DefaultBaseExecFee defaultFeePerByte = 1000 defaultMaxVerificationGas = 50000000 defaultMaxBlockSystemFee = 9000 * GASFactor // minBlockSystemFee is the minimum allowed system fee per block. minBlockSystemFee = 4007600 + // maxExecFeeFactor is the maximum allowed execution fee factor. + maxExecFeeFactor = 1000 // maxFeePerByte is the maximum allowed fee per byte value. maxFeePerByte = 100_000_000 @@ -40,6 +43,8 @@ var ( // maxTransactionsPerBlockKey is a key used to store the maximum number of // transactions allowed in block. maxTransactionsPerBlockKey = []byte{23} + // 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} @@ -59,6 +64,7 @@ type Policy struct { isValid bool maxTransactionsPerBlock uint32 maxBlockSize uint32 + execFeeFactor uint32 feePerByte int64 maxBlockSystemFee int64 maxVerificationGas int64 @@ -94,6 +100,15 @@ func newPolicy() *Policy { md = newMethodAndPrice(p.getMaxBlockSystemFee, 1000000, smartcontract.ReadStates) p.AddMethod(md, desc) + desc = newDescriptor("getExecFeeFactor", smartcontract.IntegerType) + md = newMethodAndPrice(p.getExecFeeFactor, 1000000, smartcontract.ReadStates) + p.AddMethod(md, desc) + + desc = newDescriptor("setExecFeeFactor", smartcontract.BoolType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(p.setExecFeeFactor, 3000000, smartcontract.WriteStates) + p.AddMethod(md, desc) + desc = newDescriptor("setMaxBlockSize", smartcontract.BoolType, manifest.NewParameter("value", smartcontract.IntegerType)) md = newMethodAndPrice(p.setMaxBlockSize, 3000000, smartcontract.WriteStates) @@ -137,6 +152,7 @@ func (p *Policy) Initialize(ic *interop.Context) error { p.isValid = true p.maxTransactionsPerBlock = defaultMaxTransactionsPerBlock p.maxBlockSize = defaultMaxBlockSize + p.execFeeFactor = defaultExecFeeFactor p.feePerByte = defaultFeePerByte p.maxBlockSystemFee = defaultMaxBlockSystemFee p.maxVerificationGas = defaultMaxVerificationGas @@ -160,6 +176,7 @@ func (p *Policy) PostPersist(ic *interop.Context) error { p.maxTransactionsPerBlock = getUint32WithKey(p.ContractID, ic.DAO, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock) p.maxBlockSize = getUint32WithKey(p.ContractID, ic.DAO, maxBlockSizeKey, defaultMaxBlockSize) + p.execFeeFactor = getUint32WithKey(p.ContractID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor) p.feePerByte = getInt64WithKey(p.ContractID, ic.DAO, feePerByteKey, defaultFeePerByte) p.maxBlockSystemFee = getInt64WithKey(p.ContractID, ic.DAO, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) p.maxVerificationGas = defaultMaxVerificationGas @@ -256,6 +273,41 @@ func (p *Policy) GetMaxBlockSystemFeeInternal(dao dao.DAO) int64 { return getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) } +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.DAO) int64 { + p.lock.RLock() + defer p.lock.RUnlock() + if p.isValid { + return int64(p.execFeeFactor) + } + return int64(getUint32WithKey(p.ContractID, d, execFeeFactorKey, defaultExecFeeFactor)) +} + +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)) + } + 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, execFeeFactorKey, uint32(value)) + if err != nil { + panic(err) + } + p.isValid = false + return stackitem.NewBool(true) +} + // 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]) diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index 8ecfd0a2a..2fd372cbd 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/core/block" + "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/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/network/payload" @@ -143,6 +144,62 @@ func TestFeePerByte(t *testing.T) { }) } +func TestExecFeeFactor(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.GetExecFeeFactorInternal(chain.dao) + require.EqualValues(t, interop.DefaultBaseExecFee, n) + }) + + t.Run("get", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, policyHash, "getExecFeeFactor") + require.NoError(t, err) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(interop.DefaultBaseExecFee))) + require.NoError(t, chain.persist()) + }) + + t.Run("set, zero fee", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, policyHash, "setExecFeeFactor", 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, "setExecFeeFactor", int64(1001)) + 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, "setExecFeeFactor", int64(123)) + require.NoError(t, err) + txGet1, err := prepareContractMethodInvoke(chain, 100000000, policyHash, "getExecFeeFactor") + 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(123)) + require.NoError(t, chain.persist()) + + // Get in the next block. + res, err := invokeContractMethod(chain, 100000000, policyHash, "getExecFeeFactor") + require.NoError(t, err) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(123))) + 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, "setExecFeeFactor", int64(100)) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) +} + func TestBlockSystemFee(t *testing.T) { chain := newTestChain(t) defer chain.Close() diff --git a/pkg/rpc/client/policy.go b/pkg/rpc/client/policy.go index 6c89baf68..26ff5ae9c 100644 --- a/pkg/rpc/client/policy.go +++ b/pkg/rpc/client/policy.go @@ -25,6 +25,11 @@ func (c *Client) GetFeePerByte() (int64, error) { return c.invokeNativePolicyMethod("getFeePerByte") } +// GetExecFeeFactor invokes `getExecFeeFactor` method on a native Policy contract. +func (c *Client) GetExecFeeFactor() (int64, error) { + return c.invokeNativePolicyMethod("getExecFeeFactor") +} + func (c *Client) invokeNativePolicyMethod(operation string) (int64, error) { if !c.initDone { return 0, errNetworkNotInitialized diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index c6eff745f..9103cea3b 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -9,7 +9,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/fee" - "github.com/nspcc-dev/neo-go/pkg/core/interop" "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" @@ -570,6 +569,7 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs return errors.New("number of signers must match number of scripts") } size := io.GetVarSize(tx) + var ef int64 for i, cosigner := range tx.Signers { if accs[i].Contract.Deployed { res, err := c.InvokeFunction(cosigner.Account, manifest.MethodVerify, []smartcontract.Parameter{}, tx.Signers) @@ -590,7 +590,15 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs size += io.GetVarSize([]byte{}) * 2 // both scripts are empty continue } - netFee, sizeDelta := fee.Calculate(interop.DefaultBaseExecFee, accs[i].Contract.Script) + + if ef == 0 { + var err error + ef, err = c.GetExecFeeFactor() + if err != nil { + return fmt.Errorf("can't get `ExecFeeFactor`: %w", err) + } + } + netFee, sizeDelta := fee.Calculate(ef, accs[i].Contract.Script) tx.NetworkFee += netFee size += sizeDelta }