neoneo-go/pkg/core/native_neo_test.go
Anna Shaleva be902afe9e core: do not allow NEP17 roundtrip in case of insufficient funds
NEP17 roundtrip is prohibited if from account doesn't have enough funds.

This commit fixes states diff in block 92057 where account
NfuwpaQ1A2xaeVbxWe8FRtaRgaMa8yF3YM initiates two NEO roundtrips with
amount exceeding the account's balance:

block 92057: value mismatch for key +////xTbYWBH3r5qhRKZAPFPHabKfb2vhQ==: QQMhAkwBIQOZZwEA vs QQMhAkwBIQN/ZwEA
block 92057: value mismatch for key +v///ws=: kqlddcitCg== vs tphddcitCg==
block 92057: value mismatch for key +v///xTbYWBH3r5qhRKZAPFPHabKfb2vhQ==: QQEhBUWyDu0W vs QQEhBWmhDu0W

C#'s applog (contains False and False on stack for both transfers):
```
{
   "id" : 1,
   "jsonrpc" : "2.0",
   "result" : {
      "executions" : [
         {
            "gasconsumed" : "11955500",
            "exception" : null,
            "stack" : [
               {
                  "value" : false,
                  "type" : "Boolean"
               },
               {
                  "value" : false,
                  "type" : "Boolean"
               }
            ],
            "vmstate" : "HALT",
            "trigger" : "Application",
            "notifications" : []
         }
      ],
      "txid" : "0x8e73a7e9a566a514813907272ad65fc965002c3b098eacc5bdda529af19d7688"
   }
}
```

Go's applog (both transfers succeeded and GAS minted):
```
{
   "result" : {
      "executions" : [
         {
            "gasconsumed" : "11955500",
            "trigger" : "Application",
            "stack" : [
               {
                  "type" : "Boolean",
                  "value" : true
               },
               {
                  "type" : "Boolean",
                  "value" : true
               }
            ],
            "vmstate" : "HALT",
            "notifications" : [
               {
                  "eventname" : "Transfer",
                  "contract" : "0xd2a4cff31913016155e38e474a2c06d08be276cf",
                  "state" : {
                     "value" : [
                        {
                           "type" : "Any"
                        },
                        {
                           "value" : "22FgR96+aoUSmQDxTx2myn29r4U=",
                           "type" : "ByteString"
                        },
                        {
                           "value" : "4316",
                           "type" : "Integer"
                        }
                     ],
                     "type" : "Array"
                  }
               },
               {
                  "state" : {
                     "type" : "Array",
                     "value" : [
                        {
                           "value" : "22FgR96+aoUSmQDxTx2myn29r4U=",
                           "type" : "ByteString"
                        },
                        {
                           "type" : "ByteString",
                           "value" : "22FgR96+aoUSmQDxTx2myn29r4U="
                        },
                        {
                           "type" : "Integer",
                           "value" : "1111111111"
                        }
                     ]
                  },
                  "contract" : "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5",
                  "eventname" : "Transfer"
               },
               {
                  "contract" : "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5",
                  "state" : {
                     "type" : "Array",
                     "value" : [
                        {
                           "value" : "22FgR96+aoUSmQDxTx2myn29r4U=",
                           "type" : "ByteString"
                        },
                        {
                           "type" : "ByteString",
                           "value" : "22FgR96+aoUSmQDxTx2myn29r4U="
                        },
                        {
                           "type" : "Integer",
                           "value" : "1111111"
                        }
                     ]
                  },
                  "eventname" : "Transfer"
               }
            ]
         }
      ],
      "txid" : "0x8e73a7e9a566a514813907272ad65fc965002c3b098eacc5bdda529af19d7688"
   },
   "id" : 1,
   "jsonrpc" : "2.0"
}
```
2021-06-09 13:36:50 +03:00

359 lines
12 KiB
Go

package core
import (
"math"
"math/big"
"sort"
"testing"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"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/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
func setSigner(tx *transaction.Transaction, h util.Uint160) {
tx.Signers = []transaction.Signer{{
Account: h,
Scopes: transaction.Global,
}}
}
func checkTxHalt(t *testing.T, bc *Blockchain, h util.Uint256) {
aer, err := bc.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
require.Equal(t, 1, len(aer))
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
}
func TestNEO_Vote(t *testing.T) {
bc := newTestChain(t)
neo := bc.contracts.NEO
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx)
ic.SpawnVM()
ic.Block = bc.newBlock(tx)
freq := testchain.ValidatorsCount + testchain.CommitteeSize()
advanceChain := func(t *testing.T) {
for i := 0; i < freq; i++ {
require.NoError(t, bc.AddBlock(bc.newBlock()))
ic.Block.Index++
}
}
standBySorted := bc.GetStandByValidators()
sort.Sort(standBySorted)
pubs, err := neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err)
require.Equal(t, standBySorted, pubs)
sz := testchain.CommitteeSize()
accs := make([]*wallet.Account, sz)
candidates := make(keys.PublicKeys, sz)
txs := make([]*transaction.Transaction, 0, len(accs))
for i := 0; i < sz; i++ {
priv, err := keys.NewPrivateKey()
require.NoError(t, err)
candidates[i] = priv.PublicKey()
accs[i], err = wallet.NewAccount()
require.NoError(t, err)
if i > 0 {
require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[i]))
}
to := accs[i].Contract.ScriptHash()
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, bc.contracts.NEO.Hash, "transfer", callflag.All,
neoOwner.BytesBE(), to.BytesBE(),
big.NewInt(int64(sz-i)*1000000).Int64(), nil)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
emit.AppCall(w.BinWriter, bc.contracts.GAS.Hash, "transfer", callflag.All,
neoOwner.BytesBE(), to.BytesBE(),
int64(1_000_000_000), nil)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
require.NoError(t, w.Err)
tx := transaction.New(w.Bytes(), 1000_000_000)
tx.ValidUntilBlock = bc.BlockHeight() + 1
setSigner(tx, testchain.MultisigScriptHash())
require.NoError(t, testchain.SignTx(bc, tx))
txs = append(txs, tx)
}
require.NoError(t, bc.AddBlock(bc.newBlock(txs...)))
for _, tx := range txs {
checkTxHalt(t, bc, tx.Hash())
}
transferBlock := bc.BlockHeight()
for i := 1; i < sz; i++ {
priv := accs[i].PrivateKey()
h := priv.GetScriptHash()
setSigner(tx, h)
ic.VM.Load(priv.PublicKey().GetVerificationScript())
require.NoError(t, neo.VoteInternal(ic, h, candidates[i]))
}
// We still haven't voted enough validators in.
pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err)
require.Equal(t, standBySorted, pubs)
advanceChain(t)
pubs = neo.GetNextBlockValidatorsInternal()
require.EqualValues(t, standBySorted, pubs)
// Register and give some value to the last validator.
require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[0]))
priv := accs[0].PrivateKey()
h := priv.GetScriptHash()
setSigner(tx, h)
ic.VM.Load(priv.PublicKey().GetVerificationScript())
require.NoError(t, neo.VoteInternal(ic, h, candidates[0]))
_, err = ic.DAO.Persist()
require.NoError(t, err)
advanceChain(t)
pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err)
sortedCandidates := candidates.Copy()[:testchain.Size()]
sort.Sort(sortedCandidates)
require.EqualValues(t, sortedCandidates, pubs)
pubs = neo.GetNextBlockValidatorsInternal()
require.EqualValues(t, sortedCandidates, pubs)
t.Run("check voter rewards", func(t *testing.T) {
gasBalance := make([]*big.Int, len(accs))
neoBalance := make([]*big.Int, len(accs))
txs := make([]*transaction.Transaction, 0, len(accs))
for i := range accs {
w := io.NewBufBinWriter()
h := accs[i].PrivateKey().GetScriptHash()
gasBalance[i] = bc.GetUtilityTokenBalance(h)
neoBalance[i], _ = bc.GetGoverningTokenBalance(h)
emit.AppCall(w.BinWriter, bc.contracts.NEO.Hash, "transfer", callflag.All,
h.BytesBE(), h.BytesBE(), int64(1), nil)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
require.NoError(t, w.Err)
tx := transaction.New(w.Bytes(), 0)
tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.NetworkFee = 2_000_000
tx.SystemFee = 11_000_000
setSigner(tx, h)
require.NoError(t, accs[i].SignTx(netmode.UnitTestNet, tx))
txs = append(txs, tx)
}
require.NoError(t, bc.AddBlock(bc.newBlock(txs...)))
for _, tx := range txs {
checkTxHalt(t, bc, tx.Hash())
}
// GAS increase consists of 2 parts: NEO holding + voting for committee nodes.
// Here we check that 2-nd part exists and is proportional to the amount of NEO given.
for i := range accs {
newGAS := bc.GetUtilityTokenBalance(accs[i].Contract.ScriptHash())
newGAS.Sub(newGAS, gasBalance[i])
gasForHold, err := bc.contracts.NEO.CalculateNEOHolderReward(bc.dao, neoBalance[i], transferBlock, bc.BlockHeight())
require.NoError(t, err)
newGAS.Sub(newGAS, gasForHold)
require.True(t, newGAS.Sign() > 0)
gasBalance[i] = newGAS
}
// First account voted later than the others.
require.Equal(t, -1, gasBalance[0].Cmp(gasBalance[1]))
for i := 2; i < testchain.ValidatorsCount; i++ {
require.Equal(t, 0, gasBalance[i].Cmp(gasBalance[1]))
}
require.Equal(t, 1, gasBalance[1].Cmp(gasBalance[testchain.ValidatorsCount]))
for i := testchain.ValidatorsCount; i < testchain.CommitteeSize(); i++ {
require.Equal(t, 0, gasBalance[i].Cmp(gasBalance[testchain.ValidatorsCount]))
}
})
require.NoError(t, neo.UnregisterCandidateInternal(ic, candidates[0]))
require.Error(t, neo.VoteInternal(ic, h, candidates[0]))
advanceChain(t)
pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err)
for i := range pubs {
require.NotEqual(t, candidates[0], pubs[i])
}
}
func TestNEO_SetGasPerBlock(t *testing.T) {
bc := newTestChain(t)
testGetSet(t, bc, bc.contracts.NEO.Hash, "GasPerBlock",
5*native.GASFactor, 0, 10*native.GASFactor)
}
func TestNEO_CalculateBonus(t *testing.T) {
bc := newTestChain(t)
neo := bc.contracts.NEO
tx := transaction.New([]byte{}, 0)
ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx)
ic.SpawnVM()
ic.VM.LoadScript([]byte{byte(opcode.RET)})
t.Run("Invalid", func(t *testing.T) {
_, err := neo.CalculateNEOHolderReward(ic.DAO, new(big.Int).SetInt64(-1), 0, 1)
require.Error(t, err)
})
t.Run("Zero", func(t *testing.T) {
res, err := neo.CalculateNEOHolderReward(ic.DAO, big.NewInt(0), 0, 100)
require.NoError(t, err)
require.EqualValues(t, 0, res.Int64())
})
t.Run("ManyBlocks", func(t *testing.T) {
setSigner(tx, neo.GetCommitteeAddress())
err := neo.SetGASPerBlock(ic, 10, big.NewInt(1*native.GASFactor))
require.NoError(t, err)
res, err := neo.CalculateNEOHolderReward(ic.DAO, big.NewInt(100), 5, 15)
require.NoError(t, err)
require.EqualValues(t, (100*5*5/10)+(100*5*1/10), res.Int64())
})
}
func TestNEO_GetAccountState(t *testing.T) {
bc := newTestChain(t)
acc, err := wallet.NewAccount()
require.NoError(t, err)
h := acc.Contract.ScriptHash()
t.Run("empty", func(t *testing.T) {
res, err := invokeContractMethod(bc, 1_0000000, bc.contracts.NEO.Hash, "getAccountState", h)
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
const amount = 123
transferTokenFromMultisigAccountCheckOK(t, bc, h, bc.GoverningTokenHash(), int64(amount))
t.Run("with funds", func(t *testing.T) {
bs := stackitem.NewStruct([]stackitem.Item{
stackitem.Make(123),
stackitem.Make(bc.BlockHeight()),
stackitem.Null{},
})
res, err := invokeContractMethod(bc, 1_0000000, bc.contracts.NEO.Hash, "getAccountState", h)
require.NoError(t, err)
checkResult(t, res, bs)
})
}
func TestNEO_CommitteeBountyOnPersist(t *testing.T) {
bc := newTestChain(t)
hs := make([]util.Uint160, testchain.CommitteeSize())
for i := range hs {
hs[i] = testchain.PrivateKeyByID(i).GetScriptHash()
}
const singleBounty = 50000000
bs := map[int]int64{0: singleBounty}
checkBalances := func() {
for i := 0; i < testchain.CommitteeSize(); i++ {
require.EqualValues(t, bs[i], bc.GetUtilityTokenBalance(hs[i]).Int64(), i)
}
}
for i := 0; i < testchain.CommitteeSize()*2; i++ {
require.NoError(t, bc.AddBlock(bc.newBlock()))
bs[(i+1)%testchain.CommitteeSize()] += singleBounty
checkBalances()
}
}
func TestNEO_TransferOnPayment(t *testing.T) {
bc := newTestChain(t)
cs, _ := getTestContractState(bc)
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
const amount = 2
tx := transferTokenFromMultisigAccount(t, bc, cs.Hash, bc.contracts.NEO.Hash, amount)
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer[0].VMState)
require.Len(t, aer[0].Events, 3) // transfer + GAS claim for sender + onPayment
e := aer[0].Events[2]
require.Equal(t, "LastPayment", e.Name)
arr := e.Item.Value().([]stackitem.Item)
require.Equal(t, bc.contracts.NEO.Hash.BytesBE(), arr[0].Value())
require.Equal(t, neoOwner.BytesBE(), arr[1].Value())
require.Equal(t, big.NewInt(amount), arr[2].Value())
tx = transferTokenFromMultisigAccount(t, bc, cs.Hash, bc.contracts.NEO.Hash, amount)
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer[0].VMState)
// Now we must also have GAS claim for contract and corresponding `onPayment`.
require.Len(t, aer[0].Events, 5)
e = aer[0].Events[2] // onPayment for GAS claim
require.Equal(t, "LastPayment", e.Name)
arr = e.Item.Value().([]stackitem.Item)
require.Equal(t, stackitem.Null{}, arr[1])
require.Equal(t, bc.contracts.GAS.Hash.BytesBE(), arr[0].Value())
e = aer[0].Events[4] // onPayment for NEO transfer
require.Equal(t, "LastPayment", e.Name)
arr = e.Item.Value().([]stackitem.Item)
require.Equal(t, bc.contracts.NEO.Hash.BytesBE(), arr[0].Value())
require.Equal(t, neoOwner.BytesBE(), arr[1].Value())
require.Equal(t, big.NewInt(amount), arr[2].Value())
}
func TestRegisterPrice(t *testing.T) {
bc := newTestChain(t)
testGetSet(t, bc, bc.contracts.NEO.Hash, "RegisterPrice",
native.DefaultRegisterPrice, 1, math.MaxInt64)
}
func TestNEO_Roundtrip(t *testing.T) {
bc := newTestChain(t)
initialBalance, initialHeight := bc.GetGoverningTokenBalance(neoOwner)
require.NotNil(t, initialBalance)
t.Run("bad: amount > initial balance", func(t *testing.T) {
tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.NEO.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 := bc.GetGoverningTokenBalance(neoOwner)
require.Equal(t, initialBalance, updatedBalance)
require.Equal(t, initialHeight, updatedHeight)
})
t.Run("good: amount == initial balance", func(t *testing.T) {
tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.NEO.Hash, initialBalance.Int64(), 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, 2) // roundtrip + GAS claim
// check balance wasn't changed and height was updated
updatedBalance, updatedHeight := bc.GetGoverningTokenBalance(neoOwner)
require.Equal(t, initialBalance, updatedBalance)
require.Equal(t, bc.BlockHeight(), updatedHeight)
})
}