diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 7aa75fdc6..0e602d36b 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -136,7 +136,9 @@ func TestNativeHelpersCompile(t *testing.T) { {"unclaimedGas", []string{u160, "123"}}, {"unregisterCandidate", []string{pub}}, }, nep17TestCases...)) - runNativeTestCases(t, cs.GAS.ContractMD, "gas", nep17TestCases) + runNativeTestCases(t, cs.GAS.ContractMD, "gas", append([]nativeTestCase{ + {"refuel", []string{u160, "123"}}, + }, nep17TestCases...)) runNativeTestCases(t, cs.Oracle.ContractMD, "oracle", []nativeTestCase{ {"getPrice", nil}, {"request", []string{`"url"`, "nil", `"callback"`, "nil", "123"}}, diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index f0080b476..a2b02b385 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -68,6 +68,7 @@ func TestSyscallExecution(t *testing.T) { "iterator.Create": {interopnames.SystemIteratorCreate, []string{pubs}, false}, "iterator.Next": {interopnames.SystemIteratorNext, []string{"iterator.Iterator{}"}, false}, "iterator.Value": {interopnames.SystemIteratorValue, []string{"iterator.Iterator{}"}, false}, + "runtime.BurnGas": {interopnames.SystemRuntimeBurnGas, []string{"1"}, true}, "runtime.CheckWitness": {interopnames.SystemRuntimeCheckWitness, []string{b}, false}, "runtime.GasLeft": {interopnames.SystemRuntimeGasLeft, nil, false}, "runtime.GetCallingScriptHash": {interopnames.SystemRuntimeGetCallingScriptHash, nil, false}, diff --git a/pkg/core/interop/interopnames/names.go b/pkg/core/interop/interopnames/names.go index 0ad602163..6d2da3bea 100644 --- a/pkg/core/interop/interopnames/names.go +++ b/pkg/core/interop/interopnames/names.go @@ -16,6 +16,7 @@ const ( SystemIteratorCreate = "System.Iterator.Create" SystemIteratorNext = "System.Iterator.Next" SystemIteratorValue = "System.Iterator.Value" + SystemRuntimeBurnGas = "System.Runtime.BurnGas" SystemRuntimeCheckWitness = "System.Runtime.CheckWitness" SystemRuntimeGasLeft = "System.Runtime.GasLeft" SystemRuntimeGetCallingScriptHash = "System.Runtime.GetCallingScriptHash" @@ -55,6 +56,7 @@ var names = []string{ SystemIteratorCreate, SystemIteratorNext, SystemIteratorValue, + SystemRuntimeBurnGas, SystemRuntimeCheckWitness, SystemRuntimeGasLeft, SystemRuntimeGetCallingScriptHash, diff --git a/pkg/core/interop/runtime/engine.go b/pkg/core/interop/runtime/engine.go index abd071678..8bb3f8e19 100644 --- a/pkg/core/interop/runtime/engine.go +++ b/pkg/core/interop/runtime/engine.go @@ -1,6 +1,7 @@ package runtime import ( + "errors" "fmt" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -99,3 +100,21 @@ func GetTime(ic *interop.Context) error { ic.VM.Estack().PushVal(ic.Block.Timestamp) return nil } + +// BurnGas burns GAS to benefit NEO ecosystem. +func BurnGas(ic *interop.Context) error { + gas := ic.VM.Estack().Pop().BigInt() + if !gas.IsInt64() { + return errors.New("invalid GAS value") + } + + g := gas.Int64() + if g <= 0 { + return errors.New("GAS must be positive") + } + + if !ic.VM.AddGas(g) { + return errors.New("GAS limit exceeded") + } + return nil +} diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index b682f204c..b7de6ef6d 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -23,6 +23,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -346,6 +347,13 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) { emit.Opcodes(w.BinWriter, opcode.CALLT, 1, 0, opcode.RET) callT2Off := w.Len() emit.Opcodes(w.BinWriter, opcode.CALLT, 0, 0, opcode.RET) + refuelOff := w.Len() + emit.Opcodes(w.BinWriter, opcode.PUSH2, opcode.PACK) + emit.AppCallNoArgs(w.BinWriter, bc.contracts.GAS.Hash, "refuel", callflag.States|callflag.AllowNotify) + emit.Opcodes(w.BinWriter, opcode.DROP) + burnGasOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeBurnGas) + emit.Opcodes(w.BinWriter, opcode.RET) script := w.Bytes() h := hash.Hash160(script) @@ -506,8 +514,26 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) { Offset: callT2Off, ReturnType: smartcontract.IntegerType, }, + { + Name: "burnGas", + Offset: burnGasOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("amount", smartcontract.IntegerType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "refuelGas", + Offset: refuelOff, + Parameters: []manifest.Parameter{ + manifest.NewParameter("account", smartcontract.Hash160Type), + manifest.NewParameter("gasRefuel", smartcontract.IntegerType), + manifest.NewParameter("gasBurn", smartcontract.IntegerType), + }, + ReturnType: smartcontract.VoidType, + }, } - m.Permissions = make([]manifest.Permission, 2) + m.Permissions = make([]manifest.Permission, 3) m.Permissions[0].Contract.Type = manifest.PermissionHash m.Permissions[0].Contract.Value = bc.contracts.NEO.Hash m.Permissions[0].Methods.Add("balanceOf") @@ -516,6 +542,10 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) { m.Permissions[1].Contract.Value = util.Uint160{} m.Permissions[1].Methods.Add("method") + m.Permissions[2].Contract.Type = manifest.PermissionHash + m.Permissions[2].Contract.Value = bc.contracts.GAS.Hash + m.Permissions[2].Methods.Add("refuel") + cs := &state.Contract{ ContractBase: state.ContractBase{ Hash: h, @@ -941,3 +971,37 @@ func TestLoadToken(t *testing.T) { checkFAULTState(t, aer) }) } + +func TestRuntimeBurnGas(t *testing.T) { + bc := newTestChain(t) + + cs, _ := getTestContractState(bc) + require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) + + const sysFee = 2_000000 + + t.Run("good", func(t *testing.T) { + aer, err := invokeContractMethod(bc, sysFee, cs.Hash, "burnGas", int64(1)) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState) + + t.Run("gas limit exceeded", func(t *testing.T) { + aer, err = invokeContractMethod(bc, aer.GasConsumed, cs.Hash, "burnGas", int64(2)) + require.NoError(t, err) + require.Equal(t, vm.FaultState, aer.VMState) + }) + }) + t.Run("too big integer", func(t *testing.T) { + gas := big.NewInt(math.MaxInt64) + gas.Add(gas, big.NewInt(1)) + + aer, err := invokeContractMethod(bc, sysFee, cs.Hash, "burnGas", gas) + require.NoError(t, err) + checkFAULTState(t, aer) + }) + t.Run("zero GAS", func(t *testing.T) { + aer, err := invokeContractMethod(bc, sysFee, cs.Hash, "burnGas", int64(0)) + require.NoError(t, err) + checkFAULTState(t, aer) + }) +} diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 6b4e88ac9..07f09cb9e 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -41,6 +41,7 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemIteratorCreate, Func: iterator.Create, Price: 1 << 4, ParamCount: 1}, {Name: interopnames.SystemIteratorNext, Func: iterator.Next, Price: 1 << 15, ParamCount: 1}, {Name: interopnames.SystemIteratorValue, Func: iterator.Value, Price: 1 << 4, ParamCount: 1}, + {Name: interopnames.SystemRuntimeBurnGas, Func: runtime.BurnGas, Price: 1 << 4, ParamCount: 1}, {Name: interopnames.SystemRuntimeCheckWitness, Func: runtime.CheckWitness, Price: 1 << 10, RequiredFlags: callflag.NoneFlag, ParamCount: 1}, {Name: interopnames.SystemRuntimeGasLeft, Func: runtime.GasLeft, Price: 1 << 4}, diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 84916686e..f0e0bed15 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -2,14 +2,19 @@ package native import ( "errors" + "fmt" "math/big" "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/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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" ) // GAS represents GAS native contract. @@ -38,6 +43,12 @@ func newGAS() *GAS { g.nep17TokenNative = *nep17 + desc := newDescriptor("refuel", smartcontract.VoidType, + manifest.NewParameter("account", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType)) + md := newMethodAndPrice(g.refuel, 1<<15, callflag.States|callflag.AllowNotify) + g.AddMethod(md, desc) + return g } @@ -68,6 +79,24 @@ func (g *GAS) balanceFromBytes(si *state.StorageItem) (*big.Int, error) { return &acc.Balance, err } +func (g *GAS) refuel(ic *interop.Context, args []stackitem.Item) stackitem.Item { + acc := toUint160(args[0]) + gas := toBigInt(args[1]) + + if !gas.IsInt64() || gas.Sign() == -1 { + panic("invalid GAS value") + } + + ok, err := runtime.CheckHashedWitness(ic, acc) + if !ok || err != nil { + panic(fmt.Errorf("%w: %v", ErrInvalidWitness, err)) + } + + g.burn(ic, acc, gas) + ic.VM.GasLimit += gas.Int64() + return stackitem.Null{} +} + // Initialize initializes GAS contract. func (g *GAS) Initialize(ic *interop.Context) error { if err := g.nep17TokenNative.Initialize(ic); err != nil { diff --git a/pkg/core/native_gas_test.go b/pkg/core/native_gas_test.go new file mode 100644 index 000000000..8b7e06e9d --- /dev/null +++ b/pkg/core/native_gas_test.go @@ -0,0 +1,70 @@ +package core + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func TestGAS_Refuel(t *testing.T) { + bc := newTestChain(t) + + cs, _ := getTestContractState(bc) + require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) + + const ( + sysFee = 10_000000 + burnFee = sysFee + 12345678 + ) + + accs := []*wallet.Account{ + newAccountWithGAS(t, bc), + newAccountWithGAS(t, bc), + } + + t.Run("good, refuel from self", func(t *testing.T) { + before0 := bc.GetUtilityTokenBalance(accs[0].Contract.ScriptHash()) + aer, err := invokeContractMethodGeneric(bc, sysFee, bc.contracts.GAS.Hash, "refuel", + accs[0], accs[0].Contract.ScriptHash(), int64(burnFee)) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState) + + after0 := bc.GetUtilityTokenBalance(accs[0].Contract.ScriptHash()) + tx, _, _ := bc.GetTransaction(aer.Container) + require.Equal(t, before0, new(big.Int).Add(after0, big.NewInt(tx.SystemFee+tx.NetworkFee+burnFee))) + }) + + t.Run("good, refuel from other", func(t *testing.T) { + before0 := bc.GetUtilityTokenBalance(accs[0].Contract.ScriptHash()) + before1 := bc.GetUtilityTokenBalance(accs[1].Contract.ScriptHash()) + aer, err := invokeContractMethodGeneric(bc, sysFee, cs.Hash, "refuelGas", + accs, accs[1].Contract.ScriptHash(), int64(burnFee), int64(burnFee)) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState) + + after0 := bc.GetUtilityTokenBalance(accs[0].Contract.ScriptHash()) + after1 := bc.GetUtilityTokenBalance(accs[1].Contract.ScriptHash()) + + tx, _, _ := bc.GetTransaction(aer.Container) + require.Equal(t, before0, new(big.Int).Add(after0, big.NewInt(tx.SystemFee+tx.NetworkFee))) + require.Equal(t, before1, new(big.Int).Add(after1, big.NewInt(burnFee))) + }) + + t.Run("bad, invalid witness", func(t *testing.T) { + aer, err := invokeContractMethodGeneric(bc, sysFee, cs.Hash, "refuelGas", + accs, random.Uint160(), int64(1), int64(1)) + require.NoError(t, err) + require.Equal(t, vm.FaultState, aer.VMState) + }) + + t.Run("bad, invalid GAS amount", func(t *testing.T) { + aer, err := invokeContractMethodGeneric(bc, sysFee, cs.Hash, "refuelGas", + accs, accs[0].Contract.ScriptHash(), int64(0), int64(1)) + require.NoError(t, err) + require.Equal(t, vm.FaultState, aer.VMState) + }) +} diff --git a/pkg/interop/native/gas/gas.go b/pkg/interop/native/gas/gas.go index 1d0cc9329..fbecafd12 100644 --- a/pkg/interop/native/gas/gas.go +++ b/pkg/interop/native/gas/gas.go @@ -37,3 +37,10 @@ func Transfer(from, to interop.Hash160, amount int, data interface{}) bool { return contract.Call(interop.Hash160(Hash), "transfer", contract.All, from, to, amount, data).(bool) } + +// Refuel makes some GAS from the provided account available +// for the current execution. It represents `refuel` method of GAS native contract. +func Refuel(from interop.Hash160, amount int) { + contract.Call(interop.Hash160(Hash), "refuel", + contract.States|contract.AllowNotify, from, amount) +} diff --git a/pkg/interop/runtime/runtime.go b/pkg/interop/runtime/runtime.go index 8a8b8cb16..174fc0a91 100644 --- a/pkg/interop/runtime/runtime.go +++ b/pkg/interop/runtime/runtime.go @@ -17,6 +17,11 @@ const ( Verification byte = 0x20 ) +// BurnGas burns provided amount of GAS. It uses `System.Runtime.BurnGas` syscall. +func BurnGas(gas int) { + neogointernal.Syscall1NoReturn("System.Runtime.BurnGas", gas) +} + // CheckWitness verifies if the given script hash (160-bit BE value in a 20 byte // slice) or key (compressed serialized 33-byte form) is one of the signers of // this invocation. It uses `System.Runtime.CheckWitness` syscall.