diff --git a/pkg/core/native/native_test/gas_test.go b/pkg/core/native/native_test/gas_test.go new file mode 100644 index 000000000..27205d6d1 --- /dev/null +++ b/pkg/core/native/native_test/gas_test.go @@ -0,0 +1,139 @@ +package native_test + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func newGasClient(t *testing.T) *neotest.ContractInvoker { + return newNativeClient(t, nativenames.Gas) +} + +func TestGAS_Roundtrip(t *testing.T) { + c := newGasClient(t) + e := c.Executor + gasInvoker := c.WithSigners(c.NewAccount(t)) + owner := gasInvoker.Signers[0].ScriptHash() + + getUtilityTokenBalance := func(acc util.Uint160) (*big.Int, uint32) { + lub, err := e.Chain.GetTokenLastUpdated(acc) + require.NoError(t, err) + return e.Chain.GetUtilityTokenBalance(acc), lub[e.NativeID(t, nativenames.Gas)] + } + + initialBalance, _ := getUtilityTokenBalance(owner) + require.NotNil(t, initialBalance) + + t.Run("bad: amount > initial balance", func(t *testing.T) { + h := gasInvoker.Invoke(t, false, "transfer", owner, owner, initialBalance.Int64()+1, nil) + tx, height := e.GetTransaction(t, h) + require.Equal(t, 0, len(e.GetTxExecResult(t, h).Events)) // no events (failed transfer) + // check balance and height were not changed + updatedBalance, updatedHeight := getUtilityTokenBalance(owner) + initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee)) + require.Equal(t, initialBalance, updatedBalance) + require.Equal(t, height, updatedHeight) + }) + + t.Run("good: amount < initial balance", func(t *testing.T) { + h := gasInvoker.Invoke(t, true, "transfer", owner, owner, initialBalance.Int64()-1_0000_0000, nil) + tx, height := e.GetTransaction(t, h) + require.Equal(t, 1, len(e.GetTxExecResult(t, h).Events)) // roundtrip + // check balance wasn't changed and height was updated + updatedBalance, updatedHeight := getUtilityTokenBalance(owner) + initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee)) + require.Equal(t, initialBalance, updatedBalance) + require.Equal(t, height, updatedHeight) + }) +} + +func TestGAS_RewardWithP2PSigExtensionsEnabled(t *testing.T) { + const ( + nNotaries = 2 + nKeys = 4 + ) + + bc, validator, committee := chain.NewMultiWithCustomConfig(t, func(cfg *config.ProtocolConfiguration) { + cfg.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validator, committee) + gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas)) + notaryHash := e.NativeHash(t, nativenames.Notary) + + // transfer funds to committee + e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)).Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.CommitteeHash, 1000_0000_0000, nil) + + // set Notary nodes and check their balance + notaryNodes := make([]*keys.PrivateKey, nNotaries) + notaryNodesPublicKeys := make([]interface{}, nNotaries) + var err error + for i := range notaryNodes { + notaryNodes[i], err = keys.NewPrivateKey() + require.NoError(t, err) + notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey().Bytes() + } + e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation)).Invoke(t, stackitem.Null{}, "designateAsRole", int(roles.P2PNotary), notaryNodesPublicKeys) + for _, notaryNode := range notaryNodes { + e.CheckGASBalance(t, notaryNode.GetScriptHash(), big.NewInt(0)) + } + + // deposit GAS for `signer` with lock until the next block + depositAmount := 100_0000 + (2+int64(nKeys))*transaction.NotaryServiceFeePerKey // sysfee + netfee of the next transaction + gasCommitteeInvoker.Invoke(t, true, "transfer", e.CommitteeHash, notaryHash, depositAmount, []interface{}{e.CommitteeHash, e.Chain.BlockHeight() + 1}) + + // save initial GAS total supply + getGASTS := func(t *testing.T) int64 { + stack, err := gasCommitteeInvoker.TestInvoke(t, "totalSupply") + require.NoError(t, err) + return stack.Pop().Value().(*big.Int).Int64() + } + tsInitial := getGASTS(t) + + // send transaction with Notary contract as a sender + tx := transaction.New([]byte{byte(opcode.PUSH1)}, 1_000_000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}}) + tx.NetworkFee = (2 + int64(nKeys)) * transaction.NotaryServiceFeePerKey + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: e.CommitteeHash, + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notaryNodes[0].SignHashable(uint32(e.Chain.GetConfig().Magic), tx)...), + }, + { + InvocationScript: e.Committee.SignHashable(uint32(e.Chain.GetConfig().Magic), tx), + VerificationScript: e.Committee.Script(), + }, + } + e.AddNewBlock(t, tx) + + // check balance of notaries + e.CheckGASBalance(t, notaryHash, big.NewInt(int64(depositAmount-tx.SystemFee-tx.NetworkFee))) + for _, notaryNode := range notaryNodes { + e.CheckGASBalance(t, notaryNode.GetScriptHash(), big.NewInt(int64(transaction.NotaryServiceFeePerKey*(nKeys+1)/nNotaries))) + } + tsUpdated := getGASTS(t) + tsExpected := tsInitial + 5000_0000 - tx.SystemFee + require.Equal(t, tsExpected, tsUpdated) +} diff --git a/pkg/core/native_gas_test.go b/pkg/core/native_gas_test.go deleted file mode 100644 index 34c57caed..000000000 --- a/pkg/core/native_gas_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package core - -import ( - "math/big" - "testing" - - "github.com/nspcc-dev/neo-go/internal/testchain" - "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" - "github.com/nspcc-dev/neo-go/pkg/vm/opcode" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGAS_Roundtrip(t *testing.T) { - bc := newTestChain(t) - - getUtilityTokenBalance := func(bc *Blockchain, acc util.Uint160) (*big.Int, uint32) { - lub, err := bc.GetTokenLastUpdated(acc) - require.NoError(t, err) - return bc.GetUtilityTokenBalance(acc), lub[bc.contracts.GAS.ID] - } - - initialBalance, _ := getUtilityTokenBalance(bc, neoOwner) - require.NotNil(t, initialBalance) - - t.Run("bad: amount > initial balance", func(t *testing.T) { - tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.GAS.Hash, initialBalance.Int64()+1, false) - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) // transfer without assert => HALT state - checkResult(t, &aer[0], stackitem.NewBool(false)) - require.Len(t, aer[0].Events, 0) // failed transfer => no events - // check balance and height were not changed - updatedBalance, updatedHeight := getUtilityTokenBalance(bc, neoOwner) - initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee)) - require.Equal(t, initialBalance, updatedBalance) - require.Equal(t, bc.BlockHeight(), updatedHeight) - }) - - t.Run("good: amount < initial balance", func(t *testing.T) { - amount := initialBalance.Int64() - 10_00000000 - tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.GAS.Hash, amount, false) - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) - checkResult(t, &aer[0], stackitem.NewBool(true)) - require.Len(t, aer[0].Events, 1) // roundtrip - // check balance wasn't changed and height was updated - updatedBalance, updatedHeight := getUtilityTokenBalance(bc, neoOwner) - initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee)) - require.Equal(t, initialBalance, updatedBalance) - require.Equal(t, bc.BlockHeight(), updatedHeight) - }) -} - -func TestGAS_RewardWithP2PSigExtensionsEnabled(t *testing.T) { - chain := newTestChain(t) - notaryHash := chain.contracts.Notary.Hash - gasHash := chain.contracts.GAS.Hash - signer := testchain.MultisigScriptHash() - var err error - - const ( - nNotaries = 2 - nKeys = 4 - ) - - // set Notary nodes and check their balance - notaryNodes := make([]*keys.PrivateKey, nNotaries) - notaryNodesPublicKeys := make(keys.PublicKeys, nNotaries) - for i := range notaryNodes { - notaryNodes[i], err = keys.NewPrivateKey() - require.NoError(t, err) - notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey() - } - chain.setNodesByRole(t, true, noderoles.P2PNotary, notaryNodesPublicKeys) - for _, notaryNode := range notaryNodesPublicKeys { - checkBalanceOf(t, chain, notaryNode.GetScriptHash(), 0) - } - // deposit GAS for `signer` with lock until the next block - depositAmount := 100_0000 + (2+int64(nKeys))*transaction.NotaryServiceFeePerKey // sysfee + netfee of the next transaction - transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, depositAmount, signer, int64(chain.BlockHeight()+1)) - res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, vm.HaltState, res[0].VMState) - require.Equal(t, 0, len(res[0].Stack)) - - // save initial validators balance - balances := make(map[int]int64, testchain.ValidatorsCount) - for i := 0; i < testchain.ValidatorsCount; i++ { - balances[i] = chain.GetUtilityTokenBalance(testchain.PrivateKeyByID(i).GetScriptHash()).Int64() - } - ic := interop.NewContext(trigger.Application, chain, chain.dao, nil, nil, nil, nil, chain.log) - tsInitial := chain.contracts.GAS.TotalSupply(ic, nil).Value().(*big.Int).Int64() - - // send transaction with Notary contract as a sender - tx := chain.newTestTx(util.Uint160{}, []byte{byte(opcode.PUSH1)}) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}}) - tx.NetworkFee = (2 + int64(nKeys)) * transaction.NotaryServiceFeePerKey - tx.Signers = []transaction.Signer{ - { - Account: notaryHash, - Scopes: transaction.None, - }, - { - Account: signer, - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notaryNodes[0].SignHashable(uint32(testchain.Network()), tx)...), - }, - { - InvocationScript: testchain.Sign(tx), - VerificationScript: testchain.MultisigVerificationScript(), - }, - } - b := chain.newBlock(tx) - require.NoError(t, chain.AddBlock(b)) - checkBalanceOf(t, chain, notaryHash, int(depositAmount-tx.SystemFee-tx.NetworkFee)) - singleReward := transaction.NotaryServiceFeePerKey * (nKeys + 1) / nNotaries - for _, notaryNode := range notaryNodesPublicKeys { - checkBalanceOf(t, chain, notaryNode.GetScriptHash(), singleReward) - } - for i := 0; i < testchain.ValidatorsCount; i++ { - newBalance := chain.GetUtilityTokenBalance(testchain.PrivateKeyByID(i).GetScriptHash()).Int64() - expectedBalance := balances[i] - if i == int(b.Index)%testchain.CommitteeSize() { - // committee reward - expectedBalance += 5000_0000 - } - if testchain.IDToOrder(i) == int(b.PrimaryIndex) { - // primary reward - expectedBalance += tx.NetworkFee - int64(singleReward*nNotaries) - } - assert.Equal(t, expectedBalance, newBalance, i) - } - tsUpdated := chain.contracts.GAS.TotalSupply(ic, nil).Value().(*big.Int).Int64() - tsExpected := tsInitial + 5000_0000 - tx.SystemFee - require.Equal(t, tsExpected, tsUpdated) -}