diff --git a/pkg/core/fee/opcode.go b/pkg/core/fee/opcode.go index 806ee1a8d..b90893cd0 100644 --- a/pkg/core/fee/opcode.go +++ b/pkg/core/fee/opcode.go @@ -8,12 +8,12 @@ import ( func Opcode(base int64, opcodes ...opcode.Opcode) int64 { var result int64 for _, op := range opcodes { - result += coefficients[op] + result += int64(coefficients[op]) } return result * base } -var coefficients = map[opcode.Opcode]int64{ +var coefficients = [256]uint16{ opcode.PUSHINT8: 1 << 0, opcode.PUSHINT16: 1 << 0, opcode.PUSHINT32: 1 << 0, diff --git a/pkg/core/fee/opcode_test.go b/pkg/core/fee/opcode_test.go new file mode 100644 index 000000000..39edf3e5b --- /dev/null +++ b/pkg/core/fee/opcode_test.go @@ -0,0 +1,19 @@ +package fee + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" +) + +const feeFactor = 30 + +// The most common Opcode() use case is to get price for single opcode. +func BenchmarkOpcode1(t *testing.B) { + // Just so that we don't always test the same opcode. + script := []opcode.Opcode{opcode.NOP, opcode.ADD, opcode.SYSCALL, opcode.APPEND} + l := len(script) + for n := 0; n < t.N; n++ { + _ = Opcode(feeFactor, script[n%l]) + } +} diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 496a22c60..3f1159a77 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -54,13 +54,17 @@ func newGAS(init int64) *GAS { return g } -func (g *GAS) increaseBalance(_ *interop.Context, _ util.Uint160, si *state.StorageItem, amount *big.Int) error { +func (g *GAS) increaseBalance(_ *interop.Context, _ util.Uint160, si *state.StorageItem, amount *big.Int, checkBal *big.Int) error { acc, err := state.NEP17BalanceFromBytes(*si) if err != nil { return err } if sign := amount.Sign(); sign == 0 { - return nil + // Requested self-transfer amount can be higher than actual balance. + if checkBal != nil && acc.Balance.Cmp(checkBal) < 0 { + err = errors.New("insufficient funds") + } + return err } else if sign == -1 && acc.Balance.Cmp(new(big.Int).Neg(amount)) == -1 { return errors.New("insufficient funds") } @@ -104,7 +108,8 @@ func (g *GAS) Initialize(ic *interop.Context) error { if err := g.nep17TokenNative.Initialize(ic); err != nil { return err } - if g.nep17TokenNative.getTotalSupply(ic.DAO).Sign() != 0 { + _, totalSupply := g.nep17TokenNative.getTotalSupply(ic.DAO) + if totalSupply.Sign() != 0 { return errors.New("already initialized") } h, err := getStandbyValidatorsHash(ic) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index e81b658c1..391525f1f 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -189,7 +189,8 @@ func (n *NEO) Initialize(ic *interop.Context) error { return err } - if n.nep17TokenNative.getTotalSupply(ic.DAO).Sign() != 0 { + _, totalSupply := n.nep17TokenNative.getTotalSupply(ic.DAO) + if totalSupply.Sign() != 0 { return errors.New("already initialized") } @@ -391,12 +392,13 @@ func (n *NEO) getGASPerVote(d dao.DAO, key []byte, index ...uint32) []big.Int { return reward } -func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int) error { +func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int, checkBal *big.Int) error { acc, err := state.NEOBalanceFromBytes(*si) if err != nil { return err } - if amount.Sign() == -1 && acc.Balance.Cmp(new(big.Int).Neg(amount)) == -1 { + if (amount.Sign() == -1 && acc.Balance.Cmp(new(big.Int).Neg(amount)) == -1) || + (amount.Sign() == 0 && checkBal != nil && acc.Balance.Cmp(checkBal) == -1) { return errors.New("insufficient funds") } if err := n.distributeGas(ic, h, acc); err != nil { @@ -976,7 +978,8 @@ func (n *NEO) computeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) ( votersCount := bigint.FromBytes(si) // votersCount / totalSupply must be >= 0.2 votersCount.Mul(votersCount, big.NewInt(effectiveVoterTurnout)) - voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d)) + _, totalSupply := n.getTotalSupply(d) + voterTurnout := votersCount.Div(votersCount, totalSupply) sbVals := bc.GetStandByCommittee() count := len(sbVals) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index d32d3b925..3b2695fa8 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -33,7 +33,7 @@ type nep17TokenNative struct { symbol string decimals int64 factor int64 - incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int) error + incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int, *big.Int) error balFromBytes func(item *state.StorageItem) (*big.Int, error) } @@ -95,19 +95,21 @@ func (c *nep17TokenNative) Decimals(_ *interop.Context, _ []stackitem.Item) stac } func (c *nep17TokenNative) TotalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(c.getTotalSupply(ic.DAO)) + _, supply := c.getTotalSupply(ic.DAO) + return stackitem.NewBigInteger(supply) } -func (c *nep17TokenNative) getTotalSupply(d dao.DAO) *big.Int { +func (c *nep17TokenNative) getTotalSupply(d dao.DAO) (state.StorageItem, *big.Int) { si := d.GetStorageItem(c.ID, totalSupplyKey) if si == nil { - return big.NewInt(0) + si = []byte{} } - return bigint.FromBytes(si) + return si, bigint.FromBytes(si) } -func (c *nep17TokenNative) saveTotalSupply(d dao.DAO, supply *big.Int) error { - return d.PutStorageItem(c.ID, totalSupplyKey, bigint.ToBytes(supply)) +func (c *nep17TokenNative) saveTotalSupply(d dao.DAO, si state.StorageItem, supply *big.Int) error { + si = state.StorageItem(bigint.ToPreallocatedBytes(supply, si)) + return d.PutStorageItem(c.ID, totalSupplyKey, si) } func (c *nep17TokenNative) Transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -173,29 +175,11 @@ func (c *nep17TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint16 return errors.New("insufficient funds") } si = state.StorageItem{} - } else if amount.Sign() == 0 && requiredBalance != nil { - // If amount == 0 then it's either a round trip or an empty transfer. In - // case of a round trip account's balance may still be less than actual - // transfer's amount, so we need to check it. Other cases are handled by - // `incBalance` method. - balance, err := c.balFromBytes(&si) - if err != nil { - return fmt.Errorf("failed to deserialise balance: %w", err) - } - if balance.Cmp(requiredBalance) < 0 { - // Firstly, need to put it back to storage as it affects dumps. - err = ic.DAO.PutStorageItem(c.ID, key, si) - if err != nil { - return err - } - // Finally, return an error. - return errors.New("insufficient funds") - } } - err := c.incBalance(ic, acc, &si, amount) + err := c.incBalance(ic, acc, &si, amount, requiredBalance) if err != nil { - if si != nil && amount.Sign() < 0 { + if si != nil && amount.Sign() <= 0 { _ = ic.DAO.PutStorageItem(c.ID, key, si) } return err @@ -288,7 +272,7 @@ func (c *nep17TokenNative) addTokens(ic *interop.Context, h util.Uint160, amount if si == nil { si = state.StorageItem{} } - if err := c.incBalance(ic, h, &si, amount); err != nil { + if err := c.incBalance(ic, h, &si, amount, nil); err != nil { panic(err) } var err error @@ -301,9 +285,9 @@ func (c *nep17TokenNative) addTokens(ic *interop.Context, h util.Uint160, amount panic(err) } - supply := c.getTotalSupply(ic.DAO) + buf, supply := c.getTotalSupply(ic.DAO) supply.Add(supply, amount) - err = c.saveTotalSupply(ic.DAO, supply) + err = c.saveTotalSupply(ic.DAO, buf, supply) if err != nil { panic(err) } diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go index 44e1adb50..5a59d1699 100644 --- a/pkg/core/state/native_state.go +++ b/pkg/core/state/native_state.go @@ -91,15 +91,18 @@ func (s *NEOBalance) Bytes() []byte { // ToStackItem implements stackitem.Convertible interface. It never returns an error. func (s *NEOBalance) ToStackItem() (stackitem.Item, error) { - resItem, _ := s.NEP17Balance.ToStackItem() - result := resItem.(*stackitem.Struct) - result.Append(stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight)))) + var voteItem stackitem.Item + if s.VoteTo != nil { - result.Append(stackitem.NewByteArray(s.VoteTo.Bytes())) + voteItem = stackitem.NewByteArray(s.VoteTo.Bytes()) } else { - result.Append(stackitem.Null{}) + voteItem = stackitem.Null{} } - return result, nil + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(&s.Balance), + stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight))), + voteItem, + }), nil } // FromStackItem converts stackitem.Item to NEOBalance. diff --git a/pkg/vm/opcode/isvalid_test.go b/pkg/vm/opcode/isvalid_test.go new file mode 100644 index 000000000..2361c4ee8 --- /dev/null +++ b/pkg/vm/opcode/isvalid_test.go @@ -0,0 +1,15 @@ +package opcode + +import ( + "testing" +) + +// IsValid() is called for every VM instruction. +func BenchmarkIsValid(t *testing.B) { + // Just so that we don't always test the same opcode. + script := []Opcode{NOP, ADD, SYSCALL, APPEND, 0xff, 0xf0} + l := len(script) + for n := 0; n < t.N; n++ { + _ = IsValid(script[n%l]) + } +} diff --git a/pkg/vm/opcode/opcode.go b/pkg/vm/opcode/opcode.go index 3d24b7ea7..93b62e5cd 100644 --- a/pkg/vm/opcode/opcode.go +++ b/pkg/vm/opcode/opcode.go @@ -222,8 +222,16 @@ const ( CONVERT Opcode = 0xDB ) +var validCodes [256]bool + +func init() { + // We rely on stringer here, it has a map anyway. + for op := range _Opcode_map { + validCodes[int(op)] = true + } +} + // IsValid returns true if the opcode passed is valid (defined in the VM). func IsValid(op Opcode) bool { - _, ok := _Opcode_map[op] // We rely on stringer here, it has a map anyway. - return ok + return validCodes[int(op)] } diff --git a/pkg/vm/ref_counter.go b/pkg/vm/ref_counter.go index 0d176cadf..182177f6c 100644 --- a/pkg/vm/ref_counter.go +++ b/pkg/vm/ref_counter.go @@ -18,6 +18,9 @@ func newRefCounter() *refCounter { // Add adds an item to the reference counter. func (r *refCounter) Add(item stackitem.Item) { + if r == nil { + return + } r.size++ switch item.(type) { @@ -41,6 +44,9 @@ func (r *refCounter) Add(item stackitem.Item) { // Remove removes item from the reference counter. func (r *refCounter) Remove(item stackitem.Item) { + if r == nil { + return + } r.size-- switch item.(type) { diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index 1b1130f53..fca28215b 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -158,13 +158,16 @@ type Stack struct { // NewStack returns a new stack name by the given name. func NewStack(n string) *Stack { + return newStack(n, newRefCounter()) +} + +func newStack(n string, refc *refCounter) *Stack { s := &Stack{ name: n, + refs: refc, } s.top.next = &s.top s.top.prev = &s.top - s.len = 0 - s.refs = newRefCounter() return s } diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 1ff7176e0..be47455a7 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -99,7 +99,7 @@ func New() *VM { func NewWithTrigger(t trigger.Type) *VM { vm := &VM{ state: NoneState, - istack: NewStack("invocation"), + istack: newStack("invocation", nil), refs: newRefCounter(), trigger: t, @@ -107,17 +107,10 @@ func NewWithTrigger(t trigger.Type) *VM { Invocations: make(map[util.Uint160]int), } - vm.estack = vm.newItemStack("evaluation") + vm.estack = newStack("evaluation", vm.refs) return vm } -func (v *VM) newItemStack(n string) *Stack { - s := NewStack(n) - s.refs = v.refs - - return s -} - // SetPriceGetter registers the given PriceGetterFunc in v. // f accepts vm's Context, current instruction and instruction parameter. func (v *VM) SetPriceGetter(f func(opcode.Opcode, []byte) int64) { @@ -288,9 +281,9 @@ func (v *VM) LoadScript(b []byte) { func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) { v.checkInvocationStackSize() ctx := NewContextWithParams(b, 0, -1, 0) - v.estack = v.newItemStack("estack") + v.estack = newStack("evaluation", v.refs) ctx.estack = v.estack - ctx.tryStack = NewStack("exception") + ctx.tryStack = newStack("exception", nil) ctx.callFlag = f ctx.static = newSlot(v.refs) ctx.callingScriptHash = v.GetCurrentScriptHash() @@ -1527,7 +1520,7 @@ func (v *VM) call(ctx *Context, offset int) { newCtx.RetCount = -1 newCtx.local = nil newCtx.arguments = nil - newCtx.tryStack = NewStack("exception") + newCtx.tryStack = newStack("exception", nil) newCtx.NEF = ctx.NEF v.istack.PushVal(newCtx) v.Jump(newCtx, offset)