From 08cc04c3d5490385ae8827a6c14f51de62f45c26 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 15 Jun 2020 21:13:32 +0300 Subject: [PATCH 1/7] core: add native policy contract part of #904 --- pkg/core/blockchain.go | 13 +- pkg/core/helper_test.go | 11 + pkg/core/native/blocked_accounts.go | 53 +++ pkg/core/native/blocked_accounts_test.go | 53 +++ pkg/core/native/contract.go | 7 +- pkg/core/native/policy.go | 393 +++++++++++++++++++++++ pkg/core/native_policy_test.go | 260 +++++++++++++++ pkg/network/compress.go | 5 +- pkg/network/message.go | 10 +- pkg/network/payload/payload.go | 3 + 10 files changed, 794 insertions(+), 14 deletions(-) create mode 100644 pkg/core/native/blocked_accounts.go create mode 100644 pkg/core/native/blocked_accounts_test.go create mode 100644 pkg/core/native/policy.go create mode 100644 pkg/core/native_policy_test.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 1144b43f9..12ff6a22a 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1088,9 +1088,8 @@ func (bc *Blockchain) CalculateClaimable(value int64, startHeight, endHeight uin } // FeePerByte returns transaction network fee per byte. -// TODO: should be implemented as part of PolicyContract func (bc *Blockchain) FeePerByte() util.Fixed8 { - return util.Fixed8(1000) + return util.Fixed8(bc.contracts.Policy.GetFeePerByteInternal(bc.dao)) } // GetMemPool returns the memory pool of the blockchain. @@ -1101,8 +1100,9 @@ func (bc *Blockchain) GetMemPool() *mempool.Pool { // ApplyPolicyToTxSet applies configured policies to given transaction set. It // expects slice to be ordered by fee and returns a subslice of it. func (bc *Blockchain) ApplyPolicyToTxSet(txes []*transaction.Transaction) []*transaction.Transaction { - if bc.config.MaxTransactionsPerBlock != 0 && len(txes) > bc.config.MaxTransactionsPerBlock { - txes = txes[:bc.config.MaxTransactionsPerBlock] + maxTx := bc.contracts.Policy.GetMaxTransactionsPerBlockInternal(bc.dao) + if maxTx != 0 && len(txes) > int(maxTx) { + txes = txes[:maxTx] } return txes } @@ -1203,6 +1203,11 @@ func (bc *Blockchain) PoolTx(t *transaction.Transaction) error { return err } // Policying. + if ok, err := bc.contracts.Policy.CheckPolicy(bc.newInteropContext(trigger.Application, bc.dao, nil, t), t); err != nil { + return err + } else if !ok { + return ErrPolicy + } if err := bc.memPool.Add(t, bc); err != nil { switch err { case mempool.ErrOOM: diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index a334baa33..f7b840cc5 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -387,6 +387,17 @@ func addSender(txs ...*transaction.Transaction) error { return nil } +func addCosigners(txs ...*transaction.Transaction) { + for _, tx := range txs { + tx.Cosigners = []transaction.Cosigner{{ + Account: neoOwner, + Scopes: transaction.CalledByEntry, + AllowedContracts: nil, + AllowedGroups: nil, + }} + } +} + func signTx(bc *Blockchain, txs ...*transaction.Transaction) error { validators, err := getValidators(bc.config) if err != nil { diff --git a/pkg/core/native/blocked_accounts.go b/pkg/core/native/blocked_accounts.go new file mode 100644 index 000000000..fb27ce34f --- /dev/null +++ b/pkg/core/native/blocked_accounts.go @@ -0,0 +1,53 @@ +package native + +import ( + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// BlockedAccounts represents a slice of blocked accounts hashes. +type BlockedAccounts []util.Uint160 + +// Bytes returns serialized BlockedAccounts. +func (ba *BlockedAccounts) Bytes() []byte { + w := io.NewBufBinWriter() + ba.EncodeBinary(w.BinWriter) + if w.Err != nil { + panic(w.Err) + } + return w.Bytes() +} + +// EncodeBinary implements io.Serializable interface. +func (ba *BlockedAccounts) EncodeBinary(w *io.BinWriter) { + w.WriteArray(*ba) +} + +// BlockedAccountsFromBytes converts serialized BlockedAccounts to structure. +func BlockedAccountsFromBytes(b []byte) (BlockedAccounts, error) { + ba := new(BlockedAccounts) + if len(b) == 0 { + return *ba, nil + } + r := io.NewBinReaderFromBuf(b) + ba.DecodeBinary(r) + if r.Err != nil { + return nil, r.Err + } + return *ba, nil +} + +// DecodeBinary implements io.Serializable interface. +func (ba *BlockedAccounts) DecodeBinary(r *io.BinReader) { + r.ReadArray(ba) +} + +// ToStackItem converts BlockedAccounts to stackitem.Item +func (ba *BlockedAccounts) ToStackItem() stackitem.Item { + result := make([]stackitem.Item, len(*ba)) + for i, account := range *ba { + result[i] = stackitem.NewByteArray(account.BytesLE()) + } + return stackitem.NewArray(result) +} diff --git a/pkg/core/native/blocked_accounts_test.go b/pkg/core/native/blocked_accounts_test.go new file mode 100644 index 000000000..ac2a35a5a --- /dev/null +++ b/pkg/core/native/blocked_accounts_test.go @@ -0,0 +1,53 @@ +package native + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestEncodeDecodeBinary(t *testing.T) { + expected := &BlockedAccounts{ + util.Uint160{1, 2, 3}, + util.Uint160{4, 5, 6}, + } + actual := new(BlockedAccounts) + testserdes.EncodeDecodeBinary(t, expected, actual) + + expected = &BlockedAccounts{} + actual = new(BlockedAccounts) + testserdes.EncodeDecodeBinary(t, expected, actual) +} + +func TestBytesFromBytes(t *testing.T) { + expected := BlockedAccounts{ + util.Uint160{1, 2, 3}, + util.Uint160{4, 5, 6}, + } + actual, err := BlockedAccountsFromBytes(expected.Bytes()) + require.NoError(t, err) + require.Equal(t, expected, actual) + + expected = BlockedAccounts{} + actual, err = BlockedAccountsFromBytes(expected.Bytes()) + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +func TestToStackItem(t *testing.T) { + u1 := util.Uint160{1, 2, 3} + u2 := util.Uint160{4, 5, 6} + expected := BlockedAccounts{u1, u2} + actual := stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(u1.BytesLE()), + stackitem.NewByteArray(u2.BytesLE()), + }) + require.Equal(t, expected.ToStackItem(), actual) + + expected = BlockedAccounts{} + actual = stackitem.NewArray([]stackitem.Item{}) + require.Equal(t, expected.ToStackItem(), actual) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index e9dc4f418..47629e485 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -16,6 +16,7 @@ import ( type Contracts struct { NEO *NEO GAS *GAS + Policy *Policy Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -41,7 +42,7 @@ func (cs *Contracts) ByID(id uint32) interop.Contract { return nil } -// NewContracts returns new set of native contracts with new GAS and NEO +// NewContracts returns new set of native contracts with new GAS, NEO and Policy // contracts. func NewContracts() *Contracts { cs := new(Contracts) @@ -55,6 +56,10 @@ func NewContracts() *Contracts { cs.Contracts = append(cs.Contracts, gas) cs.NEO = neo cs.Contracts = append(cs.Contracts, neo) + + policy := newPolicy() + cs.Policy = policy + cs.Contracts = append(cs.Contracts, policy) return cs } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go new file mode 100644 index 000000000..6f379b1be --- /dev/null +++ b/pkg/core/native/policy.go @@ -0,0 +1,393 @@ +package native + +import ( + "encoding/binary" + "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/interop/runtime" + "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/network/payload" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/pkg/errors" +) + +const ( + policySyscallName = "Neo.Native.Policy" + policyContractID = -3 + + defaultMaxBlockSize = 1024 * 256 + defaultMaxTransactionsPerBlock = 512 + defaultFeePerByte = 1000 +) + +var ( + // maxTransactionsPerBlockKey is a key used to store the maximum number of + // transactions allowed in block. + maxTransactionsPerBlockKey = []byte{23} + // feePerByteKey is a key used to store the minimum fee per byte for + // transaction. + feePerByteKey = []byte{10} + // blockedAccountsKey is a key used to store the list of blocked accounts. + blockedAccountsKey = []byte{15} + // maxBlockSizeKey is a key used to store the maximum block size value. + maxBlockSizeKey = []byte{16} +) + +// Policy represents Policy native contract. +type Policy struct { + interop.ContractMD +} + +var _ interop.Contract = (*Policy)(nil) + +// newPolicy returns Policy native contract. +func newPolicy() *Policy { + p := &Policy{ContractMD: *interop.NewContractMD(policySyscallName)} + + p.ContractID = policyContractID + p.Manifest.Features |= smartcontract.HasStorage + + desc := newDescriptor("getMaxTransactionsPerBlock", smartcontract.IntegerType) + md := newMethodAndPrice(p.getMaxTransactionsPerBlock, 1000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, true) + + desc = newDescriptor("getMaxBlockSize", smartcontract.IntegerType) + md = newMethodAndPrice(p.getMaxBlockSize, 1000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, true) + + desc = newDescriptor("getFeePerByte", smartcontract.IntegerType) + md = newMethodAndPrice(p.getFeePerByte, 1000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, true) + + desc = newDescriptor("getBlockedAccounts", smartcontract.ArrayType) + md = newMethodAndPrice(p.getBlockedAccounts, 1000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, true) + + desc = newDescriptor("setMaxBlockSize", smartcontract.BoolType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(p.setMaxBlockSize, 3000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, false) + + desc = newDescriptor("setMaxTransactionsPerBlock", smartcontract.BoolType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(p.setMaxTransactionsPerBlock, 3000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, false) + + desc = newDescriptor("setFeePerByte", smartcontract.BoolType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(p.setFeePerByte, 3000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, false) + + desc = newDescriptor("blockAccount", smartcontract.BoolType, + manifest.NewParameter("account", smartcontract.Hash160Type)) + md = newMethodAndPrice(p.blockAccount, 3000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, false) + + desc = newDescriptor("unblockAccount", smartcontract.BoolType, + manifest.NewParameter("account", smartcontract.Hash160Type)) + md = newMethodAndPrice(p.unblockAccount, 3000000, smartcontract.NoneFlag) + p.AddMethod(md, desc, false) + + desc = newDescriptor("onPersist", smartcontract.BoolType) + md = newMethodAndPrice(getOnPersistWrapper(p.OnPersist), 0, smartcontract.AllowModifyStates) + p.AddMethod(md, desc, false) + 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 { + si := &state.StorageItem{ + Value: make([]byte, 4, 8), + } + binary.LittleEndian.PutUint32(si.Value, defaultMaxBlockSize) + err := ic.DAO.PutStorageItem(p.ContractID, maxBlockSizeKey, si) + if err != nil { + return err + } + + binary.LittleEndian.PutUint32(si.Value, defaultMaxTransactionsPerBlock) + err = ic.DAO.PutStorageItem(p.ContractID, maxTransactionsPerBlockKey, si) + if err != nil { + return err + } + + si.Value = si.Value[:8] + binary.LittleEndian.PutUint64(si.Value, defaultFeePerByte) + err = ic.DAO.PutStorageItem(p.ContractID, feePerByteKey, si) + if err != nil { + return err + } + + ba := new(BlockedAccounts) + si.Value = ba.Bytes() + err = ic.DAO.PutStorageItem(p.ContractID, blockedAccountsKey, si) + if err != nil { + return err + } + return nil +} + +// OnPersist implements Contract interface. +func (p *Policy) OnPersist(ic *interop.Context) error { + return nil +} + +// getMaxTransactionsPerBlock is Policy contract method and returns the upper +// limit of transactions per block. +func (p *Policy) getMaxTransactionsPerBlock(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(int64(p.GetMaxTransactionsPerBlockInternal(ic.DAO)))) +} + +// GetMaxTransactionsPerBlockInternal returns the upper limit of transactions per +// block. +func (p *Policy) GetMaxTransactionsPerBlockInternal(dao dao.DAO) uint32 { + return p.getUint32WithKey(dao, maxTransactionsPerBlockKey) +} + +// getMaxBlockSize is Policy contract method and returns maximum block size. +func (p *Policy) getMaxBlockSize(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(int64(p.getUint32WithKey(ic.DAO, maxBlockSizeKey)))) +} + +// 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.DAO) int64 { + return p.getInt64WithKey(dao, feePerByteKey) +} + +// getBlockedAccounts is Policy contract method and returns list of blocked +// accounts hashes. +func (p *Policy) getBlockedAccounts(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + ba, err := p.GetBlockedAccountsInternal(ic.DAO) + if err != nil { + panic(err) + } + return ba.ToStackItem() +} + +// GetBlockedAccountsInternal returns list of blocked accounts hashes. +func (p *Policy) GetBlockedAccountsInternal(dao dao.DAO) (BlockedAccounts, error) { + si := dao.GetStorageItem(p.ContractID, blockedAccountsKey) + if si == nil { + return nil, errors.New("BlockedAccounts uninitialized") + } + ba, err := BlockedAccountsFromBytes(si.Value) + if err != nil { + return nil, err + } + return ba, nil +} + +// 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 { + ok, err := p.checkValidators(ic) + if err != nil { + panic(err) + } + if !ok { + return stackitem.NewBool(false) + } + value := uint32(toBigInt(args[0]).Int64()) + err = p.setUint32WithKey(ic.DAO, maxTransactionsPerBlockKey, value) + if err != nil { + panic(err) + } + return stackitem.NewBool(true) +} + +// setMaxBlockSize is Policy contract method and sets maximum block size. +func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) stackitem.Item { + ok, err := p.checkValidators(ic) + if err != nil { + panic(err) + } + if !ok { + return stackitem.NewBool(false) + } + value := uint32(toBigInt(args[0]).Int64()) + if payload.MaxSize <= value { + return stackitem.NewBool(false) + } + err = p.setUint32WithKey(ic.DAO, maxBlockSizeKey, value) + if err != nil { + panic(err) + } + return stackitem.NewBool(true) +} + +// setFeePerByte is Policy contract method and sets transaction's fee per byte. +func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stackitem.Item { + ok, err := p.checkValidators(ic) + if err != nil { + panic(err) + } + if !ok { + return stackitem.NewBool(false) + } + value := toBigInt(args[0]).Int64() + err = p.setInt64WithKey(ic.DAO, feePerByteKey, value) + if err != nil { + panic(err) + } + return stackitem.NewBool(true) +} + +// 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 { + ok, err := p.checkValidators(ic) + if err != nil { + panic(err) + } + if !ok { + return stackitem.NewBool(false) + } + value := toUint160(args[0]) + si := ic.DAO.GetStorageItem(p.ContractID, blockedAccountsKey) + if si == nil { + panic("BlockedAccounts uninitialized") + } + ba, err := BlockedAccountsFromBytes(si.Value) + if err != nil { + panic(err) + } + indexToInsert := sort.Search(len(ba), func(i int) bool { + return !ba[i].Less(value) + }) + ba = append(ba, value) + if indexToInsert != len(ba)-1 && ba[indexToInsert].Equals(value) { + return stackitem.NewBool(false) + } + if len(ba) > 1 { + copy(ba[indexToInsert+1:], ba[indexToInsert:]) + ba[indexToInsert] = value + } + err = ic.DAO.PutStorageItem(p.ContractID, blockedAccountsKey, &state.StorageItem{ + Value: ba.Bytes(), + }) + if err != nil { + panic(err) + } + 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 { + ok, err := p.checkValidators(ic) + if err != nil { + panic(err) + } + if !ok { + return stackitem.NewBool(false) + } + value := toUint160(args[0]) + si := ic.DAO.GetStorageItem(p.ContractID, blockedAccountsKey) + if si == nil { + panic("BlockedAccounts uninitialized") + } + ba, err := BlockedAccountsFromBytes(si.Value) + if err != nil { + panic(err) + } + indexToRemove := sort.Search(len(ba), func(i int) bool { + return !ba[i].Less(value) + }) + if indexToRemove == len(ba) || !ba[indexToRemove].Equals(value) { + return stackitem.NewBool(false) + } + ba = append(ba[:indexToRemove], ba[indexToRemove+1:]...) + err = ic.DAO.PutStorageItem(p.ContractID, blockedAccountsKey, &state.StorageItem{ + Value: ba.Bytes(), + }) + if err != nil { + panic(err) + } + return stackitem.NewBool(true) +} + +func (p *Policy) getUint32WithKey(dao dao.DAO, key []byte) uint32 { + si := dao.GetStorageItem(p.ContractID, key) + if si == nil { + return 0 + } + return binary.LittleEndian.Uint32(si.Value) +} + +func (p *Policy) setUint32WithKey(dao dao.DAO, key []byte, value uint32) error { + si := dao.GetStorageItem(p.ContractID, key) + binary.LittleEndian.PutUint32(si.Value, value) + err := dao.PutStorageItem(p.ContractID, key, si) + if err != nil { + return err + } + return nil +} + +func (p *Policy) getInt64WithKey(dao dao.DAO, key []byte) int64 { + si := dao.GetStorageItem(p.ContractID, key) + if si == nil { + return 0 + } + return int64(binary.LittleEndian.Uint64(si.Value)) +} + +func (p *Policy) setInt64WithKey(dao dao.DAO, key []byte, value int64) error { + si := dao.GetStorageItem(p.ContractID, key) + binary.LittleEndian.PutUint64(si.Value, uint64(value)) + err := dao.PutStorageItem(p.ContractID, key, si) + if err != nil { + return err + } + return nil +} + +func (p *Policy) checkValidators(ic *interop.Context) (bool, error) { + prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash) + if err != nil { + return false, err + } + return runtime.CheckHashedWitness(ic, nep5ScriptHash{ + callingScriptHash: p.Hash, + entryScriptHash: p.Hash, + currentScriptHash: p.Hash, + }, prevBlock.NextConsensus) +} + +// CheckPolicy checks whether transaction's script hashes for verifying are +// included into blocked accounts list. +func (p *Policy) CheckPolicy(ic *interop.Context, tx *transaction.Transaction) (bool, error) { + ba, err := p.GetBlockedAccountsInternal(ic.DAO) + if err != nil { + return false, err + } + scriptHashes, err := ic.Chain.GetScriptHashesForVerifying(tx) + if err != nil { + return false, err + } + for _, acc := range ba { + for _, hash := range scriptHashes { + if acc.Equals(hash) { + return false, nil + } + } + } + return true, nil +} diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go new file mode 100644 index 000000000..765f114ec --- /dev/null +++ b/pkg/core/native_policy_test.go @@ -0,0 +1,260 @@ +package core + +import ( + "math/big" + "sort" + "testing" + + "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/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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/stretchr/testify/require" +) + +func TestMaxTransactionsPerBlock(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + t.Run("get, internal method", func(t *testing.T) { + n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao) + require.Equal(t, 512, int(n)) + }) + + t.Run("get, contract method", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "getMaxTransactionsPerBlock") + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.IntegerType, + Value: 512, + }) + require.NoError(t, chain.persist()) + }) + + t.Run("set", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + require.NoError(t, chain.persist()) + n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao) + require.Equal(t, 1024, int(n)) + }) +} + +func TestMaxBlockSize(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + t.Run("get", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "getMaxBlockSize") + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.IntegerType, + Value: 1024 * 256, + }) + require.NoError(t, chain.persist()) + }) + + t.Run("set", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + require.NoError(t, chain.persist()) + res, err = invokeNativePolicyMethod(chain, "getMaxBlockSize") + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.IntegerType, + Value: 102400, + }) + require.NoError(t, chain.persist()) + }) +} + +func TestFeePerByte(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + t.Run("get, internal method", func(t *testing.T) { + n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) + require.Equal(t, 1000, int(n)) + }) + + t.Run("get, contract method", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "getFeePerByte") + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.IntegerType, + Value: 1000, + }) + require.NoError(t, chain.persist()) + }) + + t.Run("set", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + require.NoError(t, chain.persist()) + n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) + require.Equal(t, 1024, int(n)) + }) +} + +func TestBlockedAccounts(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + account := util.Uint160{1, 2, 3} + + t.Run("get, internal method", func(t *testing.T) { + accounts, err := chain.contracts.Policy.GetBlockedAccountsInternal(chain.dao) + require.NoError(t, err) + require.Equal(t, native.BlockedAccounts{}, accounts) + }) + + t.Run("get, contract method", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "getBlockedAccounts") + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: []smartcontract.Parameter{}, + }) + require.NoError(t, chain.persist()) + }) + + t.Run("block-unblock account", func(t *testing.T) { + res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + + accounts, err := chain.contracts.Policy.GetBlockedAccountsInternal(chain.dao) + require.NoError(t, err) + require.Equal(t, native.BlockedAccounts{account}, accounts) + require.NoError(t, chain.persist()) + + res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + + accounts, err = chain.contracts.Policy.GetBlockedAccountsInternal(chain.dao) + require.NoError(t, err) + require.Equal(t, native.BlockedAccounts{}, accounts) + require.NoError(t, chain.persist()) + }) + + t.Run("double-block", func(t *testing.T) { + // block + res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + require.NoError(t, chain.persist()) + + // double-block should fail + res, err = invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: false, + }) + require.NoError(t, chain.persist()) + + // unblock + res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + require.NoError(t, chain.persist()) + + // unblock the same account should fail as we don't have it blocked + res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: false, + }) + require.NoError(t, chain.persist()) + }) + + t.Run("sorted", func(t *testing.T) { + accounts := []util.Uint160{ + {2, 3, 4}, + {4, 5, 6}, + {3, 4, 5}, + {1, 2, 3}, + } + for _, acc := range accounts { + res, err := invokeNativePolicyMethod(chain, "blockAccount", acc.BytesBE()) + require.NoError(t, err) + checkResult(t, res, smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: true, + }) + require.NoError(t, chain.persist()) + } + + sort.Slice(accounts, func(i, j int) bool { + return accounts[i].Less(accounts[j]) + }) + actual, err := chain.contracts.Policy.GetBlockedAccountsInternal(chain.dao) + require.NoError(t, err) + require.Equal(t, native.BlockedAccounts(accounts), actual) + }) +} + +func invokeNativePolicyMethod(chain *Blockchain, method string, args ...interface{}) (*state.AppExecResult, error) { + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, chain.contracts.Policy.Metadata().Hash, method, args...) + if w.Err != nil { + return nil, w.Err + } + script := w.Bytes() + tx := transaction.New(chain.GetConfig().Magic, script, 0) + validUntil := chain.blockHeight + 1 + tx.ValidUntilBlock = validUntil + err := addSender(tx) + if err != nil { + return nil, err + } + addCosigners(tx) + err = signTx(chain, tx) + if err != nil { + return nil, err + } + b := chain.newBlock(tx) + err = chain.AddBlock(b) + if err != nil { + return nil, err + } + + res, err := chain.GetAppExecResult(tx.Hash()) + if err != nil { + return nil, err + } + return res, nil +} + +func checkResult(t *testing.T, result *state.AppExecResult, expected smartcontract.Parameter) { + require.Equal(t, "HALT", result.VMState) + require.Equal(t, 1, len(result.Stack)) + require.Equal(t, expected.Type, result.Stack[0].Type) + require.EqualValues(t, expected.Value, result.Stack[0].Value) +} diff --git a/pkg/network/compress.go b/pkg/network/compress.go index ddde5e9d7..bed34690d 100644 --- a/pkg/network/compress.go +++ b/pkg/network/compress.go @@ -1,6 +1,7 @@ package network import ( + "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/pierrec/lz4" ) @@ -17,8 +18,8 @@ func compress(source []byte) ([]byte, error) { // decompress decompresses bytes using lz4. func decompress(source []byte) ([]byte, error) { maxSize := len(source) * 255 - if maxSize > PayloadMaxSize { - maxSize = PayloadMaxSize + if maxSize > payload.MaxSize { + maxSize = payload.MaxSize } dest := make([]byte, maxSize) size, err := lz4.UncompressBlock(source, dest) diff --git a/pkg/network/message.go b/pkg/network/message.go index a7ec61c9e..bb0678d79 100644 --- a/pkg/network/message.go +++ b/pkg/network/message.go @@ -14,12 +14,8 @@ import ( //go:generate stringer -type=CommandType -const ( - // PayloadMaxSize is maximum payload size in decompressed form. - PayloadMaxSize = 0x02000000 - // CompressionMinSize is the lower bound to apply compression. - CompressionMinSize = 1024 -) +// CompressionMinSize is the lower bound to apply compression. +const CompressionMinSize = 1024 // Message is the complete message send between nodes. type Message struct { @@ -114,7 +110,7 @@ func (m *Message) Decode(br *io.BinReader) error { } return nil } - if l > PayloadMaxSize { + if l > payload.MaxSize { return errors.New("invalid payload size") } m.compressedPayload = make([]byte, l) diff --git a/pkg/network/payload/payload.go b/pkg/network/payload/payload.go index a761de2e2..8fc3ef211 100644 --- a/pkg/network/payload/payload.go +++ b/pkg/network/payload/payload.go @@ -2,6 +2,9 @@ package payload import "github.com/nspcc-dev/neo-go/pkg/io" +// MaxSize is maximum payload size in decompressed form. +const MaxSize = 0x02000000 + // Payload is anything that can be binary encoded/decoded. type Payload interface { io.Serializable From c2735a4569029865a3fe4d2f584a728f742f4103 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 18 Jun 2020 21:36:20 +0300 Subject: [PATCH 2/7] core: add cache to native Policy contract We can cache maxBlockSize, maxTransactionsPerBlock and feePerByte in order to reduce the number of storage requests. --- pkg/core/blockchain.go | 1 + pkg/core/native/policy.go | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 12ff6a22a..8bf5c0fdf 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -640,6 +640,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { bc.lock.Unlock() return err } + bc.contracts.Policy.OnPersistEnd(bc.dao) bc.topBlock.Store(block) atomic.StoreUint32(&bc.blockHeight, block.Index) bc.memPool.RemoveStale(bc.isTxStillRelevant, bc) diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 6f379b1be..ef74af543 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "math/big" "sort" + "sync" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -42,6 +43,14 @@ var ( // Policy represents Policy native contract. type Policy struct { interop.ContractMD + lock sync.RWMutex + // 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 + maxTransactionsPerBlock uint32 + maxBlockSize uint32 + feePerByte int64 } var _ interop.Contract = (*Policy)(nil) @@ -135,6 +144,12 @@ func (p *Policy) Initialize(ic *interop.Context) error { if err != nil { return err } + + p.isValid = true + p.maxTransactionsPerBlock = defaultMaxTransactionsPerBlock + p.maxBlockSize = defaultMaxBlockSize + p.feePerByte = defaultFeePerByte + return nil } @@ -143,6 +158,26 @@ func (p *Policy) OnPersist(ic *interop.Context) error { return nil } +// OnPersistEnd updates cached Policy values if they've been changed +func (p *Policy) OnPersistEnd(dao dao.DAO) { + if p.isValid { + return + } + p.lock.Lock() + defer p.lock.Unlock() + + maxTxPerBlock := p.getUint32WithKey(dao, maxTransactionsPerBlockKey) + p.maxTransactionsPerBlock = maxTxPerBlock + + maxBlockSize := p.getUint32WithKey(dao, maxBlockSizeKey) + p.maxBlockSize = maxBlockSize + + feePerByte := p.getInt64WithKey(dao, feePerByteKey) + p.feePerByte = feePerByte + + p.isValid = true +} + // getMaxTransactionsPerBlock is Policy contract method and returns the upper // limit of transactions per block. func (p *Policy) getMaxTransactionsPerBlock(ic *interop.Context, _ []stackitem.Item) stackitem.Item { @@ -152,11 +187,21 @@ func (p *Policy) getMaxTransactionsPerBlock(ic *interop.Context, _ []stackitem.I // GetMaxTransactionsPerBlockInternal returns the upper limit of transactions per // block. func (p *Policy) GetMaxTransactionsPerBlockInternal(dao dao.DAO) uint32 { + p.lock.RLock() + defer p.lock.RUnlock() + if p.isValid { + return p.maxTransactionsPerBlock + } return p.getUint32WithKey(dao, maxTransactionsPerBlockKey) } // getMaxBlockSize is Policy contract method and returns maximum block size. func (p *Policy) getMaxBlockSize(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + p.lock.RLock() + defer p.lock.RUnlock() + if p.isValid { + return stackitem.NewBigInteger(big.NewInt(int64(p.maxBlockSize))) + } return stackitem.NewBigInteger(big.NewInt(int64(p.getUint32WithKey(ic.DAO, maxBlockSizeKey)))) } @@ -168,6 +213,11 @@ func (p *Policy) getFeePerByte(ic *interop.Context, _ []stackitem.Item) stackite // GetFeePerByteInternal returns required transaction's fee per byte. func (p *Policy) GetFeePerByteInternal(dao dao.DAO) int64 { + p.lock.RLock() + defer p.lock.RUnlock() + if p.isValid { + return p.feePerByte + } return p.getInt64WithKey(dao, feePerByteKey) } @@ -205,10 +255,13 @@ func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackite return stackitem.NewBool(false) } value := uint32(toBigInt(args[0]).Int64()) + p.lock.Lock() + defer p.lock.Unlock() err = p.setUint32WithKey(ic.DAO, maxTransactionsPerBlockKey, value) if err != nil { panic(err) } + p.isValid = false return stackitem.NewBool(true) } @@ -225,10 +278,13 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta if payload.MaxSize <= value { return stackitem.NewBool(false) } + p.lock.Lock() + defer p.lock.Unlock() err = p.setUint32WithKey(ic.DAO, maxBlockSizeKey, value) if err != nil { panic(err) } + p.isValid = false return stackitem.NewBool(true) } @@ -242,10 +298,13 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack return stackitem.NewBool(false) } value := toBigInt(args[0]).Int64() + p.lock.Lock() + defer p.lock.Unlock() err = p.setInt64WithKey(ic.DAO, feePerByteKey, value) if err != nil { panic(err) } + p.isValid = false return stackitem.NewBool(true) } From 8de03321079f99269842ecd3937152ccd4b94bba Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 16 Jun 2020 14:59:09 +0300 Subject: [PATCH 3/7] *: use MaxTransactionsPerBlock from Policy native contract part of #904 1. We now have MaxTransactionsPerBlock set in native Policy contract, so this value should be used in (dbft).GetVerified method instead of passing it as an argument. 2. Removed (dbft).WithTxPerBlock. 2. DBFT API has changed, so update it's version. 3. Removed MaxTransactionsPerBlock from node configuration, as we have it set in native Policy contract. --- config/protocol.mainnet.yml | 1 - config/protocol.testnet.yml | 1 - go.mod | 2 +- go.sum | 4 ++-- pkg/config/protocol_config.go | 5 ++--- pkg/consensus/consensus.go | 3 +-- pkg/consensus/consensus_test.go | 6 +++--- pkg/core/blockchain.go | 4 ---- 8 files changed, 9 insertions(+), 17 deletions(-) diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index c71bc54c9..1b4e8bada 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -18,7 +18,6 @@ ProtocolConfiguration: - seed5.neo.org:10333 VerifyBlocks: true VerifyTransactions: false - MaxTransactionsPerBlock: 500 ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index 36d7c46ab..2a9c152b3 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -18,7 +18,6 @@ ProtocolConfiguration: - seed5t.neo.org:20333 VerifyBlocks: true VerifyTransactions: false - MaxTransactionsPerBlock: 500 ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/go.mod b/go.mod index cc7bd8255..de9fc5226 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/go-yaml/yaml v2.1.0+incompatible github.com/gorilla/websocket v1.4.2 github.com/mr-tron/base58 v1.1.2 - github.com/nspcc-dev/dbft v0.0.0-20200531081613-7a39e7b757ac + github.com/nspcc-dev/dbft v0.0.0-20200623100921-5a182c20965e github.com/nspcc-dev/rfc6979 v0.2.0 github.com/pierrec/lz4 v2.5.2+incompatible github.com/pkg/errors v0.8.1 diff --git a/go.sum b/go.sum index f00239b2e..4950195d0 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,8 @@ github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a h1:ajvxgEe9qY4vvoSm github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a/go.mod h1:/YFK+XOxxg0Bfm6P92lY5eDSLYfp06XOdL8KAVgXjVk= github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1 h1:yEx9WznS+rjE0jl0dLujCxuZSIb+UTjF+005TJu/nNI= github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ= -github.com/nspcc-dev/dbft v0.0.0-20200531081613-7a39e7b757ac h1:cXPgsp4avJ7cR1nPRdpFRHmWoMSRZ41FSvlNjpsyTiA= -github.com/nspcc-dev/dbft v0.0.0-20200531081613-7a39e7b757ac/go.mod h1:1FYQXSbb6/9HQIkoF8XO7W/S8N7AZRkBsgwbcXRvk0E= +github.com/nspcc-dev/dbft v0.0.0-20200623100921-5a182c20965e h1:QOT9slflIkEKb5wY0ZUC0dCmCgoqGlhOAh9+xWMIxfg= +github.com/nspcc-dev/dbft v0.0.0-20200623100921-5a182c20965e/go.mod h1:1FYQXSbb6/9HQIkoF8XO7W/S8N7AZRkBsgwbcXRvk0E= github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg= github.com/nspcc-dev/neofs-crypto v0.2.0 h1:ftN+59WqxSWz/RCgXYOfhmltOOqU+udsNQSvN6wkFck= github.com/nspcc-dev/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA= diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 293664cdd..eb2dd973f 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -7,9 +7,8 @@ import ( // ProtocolConfiguration represents the protocol config. type ( ProtocolConfiguration struct { - Magic netmode.Magic `yaml:"Magic"` - MaxTransactionsPerBlock int `yaml:"MaxTransactionsPerBlock"` - MemPoolSize int `yaml:"MemPoolSize"` + Magic netmode.Magic `yaml:"Magic"` + MemPoolSize int `yaml:"MemPoolSize"` // SaveStorageBatch enables storage batch saving before every persist. SaveStorageBatch bool `yaml:"SaveStorageBatch"` SecondsPerBlock int `yaml:"SecondsPerBlock"` diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index 289ee5f89..6113753a9 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -121,7 +121,6 @@ func NewService(cfg Config) (Service, error) { dbft.WithLogger(srv.log), dbft.WithSecondsPerBlock(cfg.TimePerBlock), dbft.WithGetKeyPair(srv.getKeyPair), - dbft.WithTxPerBlock(10000), dbft.WithRequestTx(cfg.RequestTx), dbft.WithGetTx(srv.getTx), dbft.WithGetVerified(srv.getVerifiedTx), @@ -392,7 +391,7 @@ func (s *service) getBlock(h util.Uint256) block.Block { return &neoBlock{Block: *b} } -func (s *service) getVerifiedTx(count int) []block.Transaction { +func (s *service) getVerifiedTx() []block.Transaction { pool := s.Config.Chain.GetMemPool() var txx []*transaction.Transaction diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go index 2db405ef2..6ad040cc5 100644 --- a/pkg/consensus/consensus_test.go +++ b/pkg/consensus/consensus_test.go @@ -31,7 +31,7 @@ func TestNewService(t *testing.T) { require.NoError(t, srv.Chain.PoolTx(tx)) var txx []block.Transaction - require.NotPanics(t, func() { txx = srv.getVerifiedTx(1) }) + require.NotPanics(t, func() { txx = srv.getVerifiedTx() }) require.Len(t, txx, 1) require.Equal(t, tx, txx[0]) srv.Chain.Close() @@ -69,7 +69,7 @@ func TestService_GetVerified(t *testing.T) { srv.dbft.ViewNumber = 1 t.Run("new transactions will be proposed in case of failure", func(t *testing.T) { - txx := srv.getVerifiedTx(10) + txx := srv.getVerifiedTx() require.Equal(t, 1, len(txx), "there is only 1 tx in mempool") require.Equal(t, txs[3], txx[0]) }) @@ -79,7 +79,7 @@ func TestService_GetVerified(t *testing.T) { require.NoError(t, srv.Chain.PoolTx(tx)) } - txx := srv.getVerifiedTx(10) + txx := srv.getVerifiedTx() require.Contains(t, txx, txs[0]) require.Contains(t, txx, txs[1]) require.NotContains(t, txx, txs[2]) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 8bf5c0fdf..a9658d6e9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -152,10 +152,6 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L cfg.MemPoolSize = defaultMemPoolSize log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize)) } - if cfg.MaxTransactionsPerBlock <= 0 { - cfg.MaxTransactionsPerBlock = 0 - log.Info("MaxTransactionsPerBlock is not set or wrong, setting default value (unlimited)", zap.Int("MaxTransactionsPerBlock", cfg.MaxTransactionsPerBlock)) - } bc := &Blockchain{ config: cfg, dao: dao.NewSimple(s, cfg.Magic), From 9097a1a23d37b4e8595aef59d79387212e32994d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 23 Jun 2020 16:11:49 +0300 Subject: [PATCH 4/7] smartcontract: update (Parameter).MarshalJSON method MarshalJSON should be defined on structure (not pointer), as we use structures to marshal parameters (e.g. in NotificationEvent and Invoke of RPC result package) and never use pointers for that purpose. Also added marshalling of nil array into `[]` instead of `null` to follow C# implementation. --- pkg/core/native/blocked_accounts_test.go | 11 +++++++++++ pkg/smartcontract/parameter.go | 8 ++++++-- pkg/smartcontract/parameter_test.go | 9 ++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/core/native/blocked_accounts_test.go b/pkg/core/native/blocked_accounts_test.go index ac2a35a5a..76709395e 100644 --- a/pkg/core/native/blocked_accounts_test.go +++ b/pkg/core/native/blocked_accounts_test.go @@ -1,9 +1,11 @@ package native import ( + "encoding/json" "testing" "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" @@ -51,3 +53,12 @@ func TestToStackItem(t *testing.T) { actual = stackitem.NewArray([]stackitem.Item{}) require.Equal(t, expected.ToStackItem(), actual) } + +func TestMarshallJSON(t *testing.T) { + ba := &BlockedAccounts{} + p := smartcontract.ParameterFromStackItem(ba.ToStackItem(), make(map[stackitem.Item]bool)) + actual, err := json.Marshal(p) + require.NoError(t, err) + expected := `{"type":"Array","value":[]}` + require.Equal(t, expected, string(actual)) +} diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index e3ce46f1e..c4dfe7fc5 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -57,7 +57,7 @@ type rawParameter struct { } // MarshalJSON implements Marshaler interface. -func (p *Parameter) MarshalJSON() ([]byte, error) { +func (p Parameter) MarshalJSON() ([]byte, error) { var ( resultRawValue json.RawMessage resultErr error @@ -83,7 +83,11 @@ func (p *Parameter) MarshalJSON() ([]byte, error) { } case ArrayType: var value = p.Value.([]Parameter) - resultRawValue, resultErr = json.Marshal(value) + if value == nil { + resultRawValue, resultErr = json.Marshal([]Parameter{}) + } else { + resultRawValue, resultErr = json.Marshal(value) + } case MapType: ppair := p.Value.([]ParameterPair) resultRawValue, resultErr = json.Marshal(ppair) diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index a10a127a7..818181834 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -131,6 +131,13 @@ var marshalJSONTestCases = []struct { }, result: `{"type":"InteropInterface","value":null}`, }, + { + input: Parameter{ + Type: ArrayType, + Value: []Parameter{}, + }, + result: `{"type":"Array","value":[]}`, + }, } var marshalJSONErrorCases = []Parameter{ @@ -146,7 +153,7 @@ var marshalJSONErrorCases = []Parameter{ func TestParam_MarshalJSON(t *testing.T) { for _, tc := range marshalJSONTestCases { - res, err := json.Marshal(&tc.input) + res, err := json.Marshal(tc.input) assert.NoError(t, err) var actual, expected Parameter assert.NoError(t, json.Unmarshal(res, &actual)) From b88863948dc4e84cfa8711cfad12f2538a789258 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 16 Jun 2020 15:46:28 +0300 Subject: [PATCH 5/7] rpc: add native policy API to RPC client part of #904 --- pkg/rpc/client/policy.go | 77 ++++++++++++++++++++++++++++++++++++++ pkg/rpc/client/rpc.go | 12 +++--- pkg/rpc/client/rpc_test.go | 49 ++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 pkg/rpc/client/policy.go diff --git a/pkg/rpc/client/policy.go b/pkg/rpc/client/policy.go new file mode 100644 index 000000000..c366ce5f9 --- /dev/null +++ b/pkg/rpc/client/policy.go @@ -0,0 +1,77 @@ +package client + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/pkg/errors" +) + +// PolicyContractHash represents BE hash of native Policy contract. +var PolicyContractHash = util.Uint160{154, 97, 164, 110, 236, 151, 184, 147, 6, 215, 206, 129, 241, 91, 70, 32, 145, 208, 9, 50} + +// GetMaxTransactionsPerBlock invokes `getMaxTransactionsPerBlock` method on a +// native Policy contract. +func (c *Client) GetMaxTransactionsPerBlock() (int64, error) { + return c.invokeNativePolicyMethod("getMaxTransactionsPerBlock") +} + +// GetMaxBlockSize invokes `getMaxBlockSize` method on a native Policy contract. +func (c *Client) GetMaxBlockSize() (int64, error) { + return c.invokeNativePolicyMethod("getMaxBlockSize") +} + +// GetFeePerByte invokes `getFeePerByte` method on a native Policy contract. +func (c *Client) GetFeePerByte() (int64, error) { + return c.invokeNativePolicyMethod("getFeePerByte") +} + +func (c *Client) invokeNativePolicyMethod(operation string) (int64, error) { + result, err := c.InvokeFunction(PolicyContractHash.StringLE(), operation, []smartcontract.Parameter{}, nil) + if err != nil { + return 0, err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return 0, errors.New("invalid VM state") + } + + return topIntFromStack(result.Stack) +} + +// GetBlockedAccounts invokes `getBlockedAccounts` method on a native Policy contract. +func (c *Client) GetBlockedAccounts() (native.BlockedAccounts, error) { + result, err := c.InvokeFunction(PolicyContractHash.StringLE(), "getBlockedAccounts", []smartcontract.Parameter{}, nil) + if err != nil { + return nil, err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return nil, errors.New("invalid VM state") + } + + return topBlockedAccountsFromStack(result.Stack) +} + +func topBlockedAccountsFromStack(st []smartcontract.Parameter) (native.BlockedAccounts, error) { + index := len(st) - 1 // top stack element is last in the array + var ( + ba native.BlockedAccounts + err error + ) + switch typ := st[index].Type; typ { + case smartcontract.ArrayType: + data, ok := st[index].Value.([]smartcontract.Parameter) + if !ok { + return nil, errors.New("invalid Array item") + } + ba = make(native.BlockedAccounts, len(data)) + for i, account := range data { + ba[i], err = util.Uint160DecodeBytesLE(account.Value.([]byte)) + if err != nil { + return nil, err + } + } + default: + return nil, fmt.Errorf("invalid stack item type: %s", typ) + } + return ba, nil +} diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d4e9ce2cc..2d6fc2097 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -522,12 +522,10 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, acc *wallet.Account) tx.NetworkFee += netFee size += sizeDelta } - tx.NetworkFee += util.Fixed8(int64(size) * int64(c.GetFeePerByte())) + fee, err := c.GetFeePerByte() + if err != nil { + return err + } + tx.NetworkFee += util.Fixed8(int64(size) * fee) return nil } - -// GetFeePerByte returns transaction network fee per byte -func (c *Client) GetFeePerByte() util.Fixed8 { - // TODO: make it a part of policy contract - return util.Fixed8(1000) -} diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 1cc52f28b..97b66968f 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -13,6 +13,7 @@ import ( "github.com/gorilla/websocket" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/block" + "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/hash" @@ -318,6 +319,54 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getFeePerByte": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + return c.GetFeePerByte() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"state":"HALT","gas_consumed":"0.0200739","script":"10c00c0d676574466565506572427974650c149a61a46eec97b89306d7ce81f15b462091d0093241627d5b52","stack":[{"type":"Integer","value":"1000"}]}}`, + result: func(c *Client) interface{} { + return int64(1000) + }, + }, + }, + "getMaxTransacctionsPerBlock": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + return c.GetMaxTransactionsPerBlock() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"state":"HALT","gas_consumed":"0.0200739","script":"10c00c1a6765744d61785472616e73616374696f6e73506572426c6f636b0c149a61a46eec97b89306d7ce81f15b462091d0093241627d5b52","stack":[{"type":"Integer","value":"512"}]}}`, + result: func(c *Client) interface{} { + return int64(512) + }, + }, + }, + "getMaxBlockSize": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + return c.GetMaxBlockSize() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"state":"HALT","gas_consumed":"0.0200739","script":"10c00c0f6765744d6178426c6f636b53697a650c149a61a46eec97b89306d7ce81f15b462091d0093241627d5b52","stack":[{"type":"Integer","value":"262144"}]}}`, + result: func(c *Client) interface{} { + return int64(262144) + }, + }, + }, + "getBlockedAccounts": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + return c.GetBlockedAccounts() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"state":"HALT","gas_consumed":"0.0200739","script":"10c00c12676574426c6f636b65644163636f756e74730c149a61a46eec97b89306d7ce81f15b462091d0093241627d5b52","stack":[{"type":"Array","value":[]}]}}`, + result: func(c *Client) interface{} { + return native.BlockedAccounts{} + }, + }, + }, "getnep5balances": { { name: "positive", From ce402a70d231856c873d80189977221e82d2c26c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 17 Jun 2020 14:40:33 +0300 Subject: [PATCH 6/7] core: add policy check to (*Blockchain).verifyTx method If any of transaction's script hashes for verifying are included into Policy blocked accounts list, transaction is invalid. --- pkg/core/blockchain.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index a9658d6e9..80f06efb9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1123,6 +1123,22 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e if t.ValidUntilBlock <= height || t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement { return errors.Errorf("transaction has expired. ValidUntilBlock = %d, current height = %d", t.ValidUntilBlock, height) } + hashes, err := bc.GetScriptHashesForVerifying(t) + if err != nil { + return err + } + blockedAccounts, err := bc.contracts.Policy.GetBlockedAccountsInternal(bc.dao) + if err != nil { + return err + } + for _, h := range hashes { + i := sort.Search(len(blockedAccounts), func(i int) bool { + return !blockedAccounts[i].Less(h) + }) + if i != len(blockedAccounts) && blockedAccounts[i].Equals(h) { + return errors.Errorf("policy check failed") + } + } balance := bc.GetUtilityTokenBalance(t.Sender) need := t.SystemFee.Add(t.NetworkFee) if balance.LessThan(need) { From 9f11a55bd6bf78e9c719ad4fc442f5252e7ec34f Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 17 Jun 2020 15:29:56 +0300 Subject: [PATCH 7/7] core: add policy check to mempool After block was stored it's possible to have new FeePerByte constraint, so we should remove all transactions which do not meet this requirement. Also caching of FeePerByte was added in order not to re-verify transactions each time mempool needs to be updated. --- pkg/core/mempool/mem_pool.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pkg/core/mempool/mem_pool.go b/pkg/core/mempool/mem_pool.go index 5000fb4b5..7f767d6ab 100644 --- a/pkg/core/mempool/mem_pool.go +++ b/pkg/core/mempool/mem_pool.go @@ -46,7 +46,8 @@ type Pool struct { verifiedTxes items fees map[util.Uint160]utilityBalanceAndFees - capacity int + capacity int + feePerByte util.Fixed8 } func (p items) Len() int { return len(p) } @@ -218,12 +219,13 @@ func (mp *Pool) Remove(hash util.Uint256) { // drop part of the mempool that is now invalid after the block acceptance. func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) { mp.lock.Lock() + policyChanged := mp.loadPolicy(feer) // We can reuse already allocated slice // because items are iterated one-by-one in increasing order. newVerifiedTxes := mp.verifiedTxes[:0] mp.fees = make(map[util.Uint160]utilityBalanceAndFees) // it'd be nice to reuse existing map, but we can't easily clear it for _, itm := range mp.verifiedTxes { - if isOK(itm.txn) && mp.tryAddSendersFee(itm.txn, feer) { + if isOK(itm.txn) && mp.checkPolicy(itm.txn, policyChanged) && mp.tryAddSendersFee(itm.txn, feer) { newVerifiedTxes = append(newVerifiedTxes, itm) } else { delete(mp.verifiedMap, itm.txn.Hash()) @@ -233,6 +235,25 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) mp.lock.Unlock() } +// loadPolicy updates feePerByte field and returns whether policy has been +// changed. +func (mp *Pool) loadPolicy(feer Feer) bool { + newFeePerByte := feer.FeePerByte() + if newFeePerByte.GreaterThan(mp.feePerByte) { + mp.feePerByte = newFeePerByte + return true + } + return false +} + +// checkPolicy checks whether transaction fits policy. +func (mp *Pool) checkPolicy(tx *transaction.Transaction, policyChanged bool) bool { + if !policyChanged || tx.FeePerByte() >= mp.feePerByte { + return true + } + return false +} + // NewMemPool returns a new Pool struct. func NewMemPool(capacity int) Pool { return Pool{