diff --git a/pkg/core/basic_chain_test.go b/pkg/core/basic_chain_test.go index d9e8cbfc3..480db1eb8 100644 --- a/pkg/core/basic_chain_test.go +++ b/pkg/core/basic_chain_test.go @@ -8,6 +8,7 @@ import ( "path" "path/filepath" "testing" + "time" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" @@ -32,9 +33,15 @@ const ( // It is also used to retrieve smart contracts that should be deployed to // Basic chain. basicChainPrefix = "../rpc/server/testdata/" + // bcPersistInterval is the time period Blockchain persists changes to the + // underlying storage. + bcPersistInterval = time.Second ) -var notaryModulePath = filepath.Join("..", "services", "notary") +var ( + notaryModulePath = filepath.Join("..", "services", "notary") + pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") +) // TestCreateBasicChain generates "../rpc/testdata/testblocks.acc" file which // contains data for RPC unit tests. It also is a nice integration test. diff --git a/pkg/core/bench_test.go b/pkg/core/bench_test.go index 8bbb6b772..d747a6401 100644 --- a/pkg/core/bench_test.go +++ b/pkg/core/bench_test.go @@ -1,34 +1,33 @@ -package core +package core_test import ( "fmt" "testing" "github.com/nspcc-dev/neo-go/internal/random" - "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/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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/wallet" "github.com/stretchr/testify/require" ) -func BenchmarkVerifyWitness(t *testing.B) { - bc := newTestChain(t) - acc, err := wallet.NewAccount() - require.NoError(t, err) - - tx := bc.newTestTx(acc.Contract.ScriptHash(), []byte{byte(opcode.PUSH1)}) - require.NoError(t, acc.SignTx(netmode.UnitTestNet, tx)) +func BenchmarkBlockchain_VerifyWitness(t *testing.B) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + tx := e.NewTx(t, []neotest.Signer{acc}, e.NativeHash(t, nativenames.Gas), "transfer", acc.ScriptHash(), acc.Script(), 1, nil) t.ResetTimer() for n := 0; n < t.N; n++ { - _, _ = bc.VerifyWitness(tx.Signers[0].Account, tx, &tx.Scripts[0], 100000000) + _, err := bc.VerifyWitness(tx.Signers[0].Account, tx, &tx.Scripts[0], 100000000) + require.NoError(t, err) } } @@ -57,38 +56,35 @@ func BenchmarkBlockchain_ForEachNEP17Transfer(t *testing.B) { func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBlock, nBlocksToTake int) { var ( - nonce uint32 = 1 - chainHeight = 2_100 // constant chain height to be able to compare paging results - transfersPerBlock = state.TokenTransferBatchSize/4 + // 4 blocks per batch + chainHeight = 2_100 // constant chain height to be able to compare paging results + transfersPerBlock = state.TokenTransferBatchSize/4 + // 4 blocks per batch state.TokenTransferBatchSize/32 // shift ) - bc := newTestChainWithCustomCfgAndStore(t, ps, nil) - gasHash := bc.contracts.GAS.Hash + bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true) + + e := neotest.NewExecutor(t, bc, validators, committee) + gasHash := e.NativeHash(t, nativenames.Gas) + acc := random.Uint160() + from := e.Validator.ScriptHash() for j := 0; j < chainHeight; j++ { w := io.NewBufBinWriter() for i := 0; i < transfersPerBlock; i++ { - emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, testchain.MultisigScriptHash(), acc, 1, nil) + emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, from, acc, 1, nil) emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) } + require.NoError(t, w.Err) script := w.Bytes() tx := transaction.New(script, int64(1100_0000*transfersPerBlock)) + tx.NetworkFee = 1_0000_000 tx.ValidUntilBlock = bc.BlockHeight() + 1 - tx.Nonce = nonce - nonce++ - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.CalledByEntry, - AllowedContracts: nil, - AllowedGroups: nil, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - b := bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, tx.Hash()) + tx.Nonce = neotest.Nonce() + tx.Signers = []transaction.Signer{{Account: from, Scopes: transaction.CalledByEntry}} + require.NoError(t, validators.SignTx(netmode.UnitTestNet, tx)) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) } newestB, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight()) - startFromBlock + 1)) diff --git a/pkg/core/blockchain_core_test.go b/pkg/core/blockchain_core_test.go index b06623738..4a68fb16c 100644 --- a/pkg/core/blockchain_core_test.go +++ b/pkg/core/blockchain_core_test.go @@ -4,43 +4,20 @@ import ( "encoding/binary" "errors" "fmt" - "math/big" - "math/rand" - "path/filepath" "strings" "testing" "time" - "github.com/nspcc-dev/neo-go/internal/contracts" - "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" - "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/fee" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/core/mempool" - "github.com/nspcc-dev/neo-go/pkg/core/native" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativeprices" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/crypto/hash" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions" "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/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util/slice" - "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/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -72,44 +49,6 @@ func TestVerifyHeader(t *testing.T) { }) } -func TestAddHeaders(t *testing.T) { - bc := newTestChain(t) - lastBlock := bc.topBlock.Load().(*block.Block) - h1 := newBlock(bc.config, 1, lastBlock.Hash()).Header - h2 := newBlock(bc.config, 2, h1.Hash()).Header - h3 := newBlock(bc.config, 3, h2.Hash()).Header - - require.NoError(t, bc.AddHeaders()) - require.NoError(t, bc.AddHeaders(&h1, &h2)) - require.NoError(t, bc.AddHeaders(&h2, &h3)) - - assert.Equal(t, h3.Index, bc.HeaderHeight()) - assert.Equal(t, uint32(0), bc.BlockHeight()) - assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) - - // Add them again, they should not be added. - require.NoError(t, bc.AddHeaders(&h3, &h2, &h1)) - - assert.Equal(t, h3.Index, bc.HeaderHeight()) - assert.Equal(t, uint32(0), bc.BlockHeight()) - assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) - - h4 := newBlock(bc.config, 4, h3.Hash().Reverse()).Header - h5 := newBlock(bc.config, 5, h4.Hash()).Header - - assert.Error(t, bc.AddHeaders(&h4, &h5)) - assert.Equal(t, h3.Index, bc.HeaderHeight()) - assert.Equal(t, uint32(0), bc.BlockHeight()) - assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) - - h6 := newBlock(bc.config, 4, h3.Hash()).Header - h6.Script.InvocationScript = nil - assert.Error(t, bc.AddHeaders(&h6)) - assert.Equal(t, h3.Index, bc.HeaderHeight()) - assert.Equal(t, uint32(0), bc.BlockHeight()) - assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) -} - func TestAddBlock(t *testing.T) { const size = 3 bc := newTestChain(t) @@ -136,1371 +75,6 @@ func TestAddBlock(t *testing.T) { assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) } -func TestAddBlockStateRoot(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true - }) - - sr, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight()) - require.NoError(t, err) - - tx := newNEP17Transfer(bc.contracts.NEO.Hash, neoOwner, util.Uint160{}, 1) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, tx) - require.NoError(t, testchain.SignTx(bc, tx)) - - lastBlock := bc.topBlock.Load().(*block.Block) - b := newBlock(bc.config, lastBlock.Index+1, lastBlock.Hash(), tx) - err = bc.AddBlock(b) - require.True(t, errors.Is(err, ErrHdrStateRootSetting), "got: %v", err) - - u := sr.Root - u[0] ^= 0xFF - b = newBlockWithState(bc.config, lastBlock.Index+1, lastBlock.Hash(), &u, tx) - err = bc.AddBlock(b) - require.True(t, errors.Is(err, ErrHdrInvalidStateRoot), "got: %v", err) - - b = bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b)) -} - -func TestAddHeadersStateRoot(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.StateRootInHeader = true - }) - - r := bc.stateRoot.CurrentLocalStateRoot() - h1 := bc.newBlock().Header - - // invalid stateroot - h1.PrevStateRoot[0] ^= 0xFF - require.True(t, errors.Is(bc.AddHeaders(&h1), ErrHdrInvalidStateRoot)) - - // valid stateroot - h1.PrevStateRoot = r - require.NoError(t, bc.AddHeaders(&h1)) - - // unable to verify stateroot (stateroot is computed for block #0 only => can - // verify stateroot of header #1 only) => just store the header - h2 := newBlockWithState(bc.config, 2, h1.Hash(), nil).Header - require.NoError(t, bc.AddHeaders(&h2)) -} - -func TestAddBadBlock(t *testing.T) { - bc := newTestChain(t) - // It has ValidUntilBlock == 0, which is wrong - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - b1 := bc.newBlock(tx) - - require.Error(t, bc.AddBlock(b1)) - bc.config.VerifyTransactions = false - require.NoError(t, bc.AddBlock(b1)) - - b2 := bc.newBlock() - b2.PrevHash = util.Uint256{} - - require.Error(t, bc.AddBlock(b2)) - bc.config.VerifyBlocks = false - require.NoError(t, bc.AddBlock(b2)) - - tx = transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = 128 - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.PoolTx(tx)) - bc.config.VerifyTransactions = true - bc.config.VerifyBlocks = true - b3 := bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b3)) -} - -func TestGetHeader(t *testing.T) { - bc := newTestChain(t) - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, tx) - assert.Nil(t, testchain.SignTx(bc, tx)) - block := bc.newBlock(tx) - err := bc.AddBlock(block) - assert.Nil(t, err) - - hash := block.Hash() - header, err := bc.GetHeader(hash) - require.NoError(t, err) - assert.Equal(t, &block.Header, header) - - b2 := bc.newBlock() - _, err = bc.GetHeader(b2.Hash()) - assert.Error(t, err) -} - -func TestGetBlock(t *testing.T) { - bc := newTestChain(t) - blocks, err := bc.genBlocks(100) - require.NoError(t, err) - - for i := 0; i < len(blocks); i++ { - block, err := bc.GetBlock(blocks[i].Hash()) - require.NoErrorf(t, err, "can't get block %d: %s", i, err) - assert.Equal(t, blocks[i].Index, block.Index) - assert.Equal(t, blocks[i].Hash(), block.Hash()) - } - - t.Run("store only header", func(t *testing.T) { - t.Run("non-empty block", func(t *testing.T) { - tx, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, - random.Uint160(), 1, 1, 1000) - require.NoError(t, err) - b := bc.newBlock(tx) - require.NoError(t, bc.AddHeaders(&b.Header)) - - _, err = bc.GetBlock(b.Hash()) - require.Error(t, err) - - _, err = bc.GetHeader(b.Hash()) - require.NoError(t, err) - - require.NoError(t, bc.AddBlock(b)) - - _, err = bc.GetBlock(b.Hash()) - require.NoError(t, err) - }) - t.Run("empty block", func(t *testing.T) { - b := bc.newBlock() - require.NoError(t, bc.AddHeaders(&b.Header)) - - _, err = bc.GetBlock(b.Hash()) - require.NoError(t, err) - }) - }) -} - -func (bc *Blockchain) newTestTx(h util.Uint160, script []byte) *transaction.Transaction { - tx := transaction.New(script, 1_000_000) - tx.Nonce = rand.Uint32() - tx.ValidUntilBlock = 100 - tx.Signers = []transaction.Signer{{ - Account: h, - Scopes: transaction.CalledByEntry, - }} - tx.NetworkFee = int64(io.GetVarSize(tx)+200 /* witness */) * bc.FeePerByte() - tx.NetworkFee += 1_000_000 // verification cost - return tx -} - -func TestVerifyTx(t *testing.T) { - bc := newTestChain(t) - - accs := make([]*wallet.Account, 5) - for i := range accs { - var err error - accs[i], err = wallet.NewAccount() - require.NoError(t, err) - } - - notaryServiceFeePerKey := bc.contracts.Notary.GetNotaryServiceFeePerKey(bc.dao) - - oracleAcc := accs[2] - oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()} - require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs)) - - neoHash := bc.contracts.NEO.Hash - gasHash := bc.contracts.GAS.Hash - w := io.NewBufBinWriter() - for _, sc := range []util.Uint160{neoHash, gasHash} { - for _, a := range accs { - amount := int64(1_000_000) - if sc.Equals(gasHash) { - amount = 1_000_000_000 - } - emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, - neoOwner, a.Contract.ScriptHash(), amount, nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - } - } - emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All, - neoOwner, testchain.CommitteeScriptHash(), int64(1_000_000_000), nil) - emit.Opcodes(w.BinWriter, opcode.ASSERT) - require.NoError(t, w.Err) - - txMove := bc.newTestTx(neoOwner, w.Bytes()) - txMove.SystemFee = 1_000_000_000 - require.NoError(t, testchain.SignTx(bc, txMove)) - b := bc.newBlock(txMove) - require.NoError(t, bc.AddBlock(b)) - - aer, err := bc.GetAppExecResults(txMove.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - require.Equal(t, aer[0].VMState, vm.HaltState) - - res, err := invokeContractMethodGeneric(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", true, accs[1].PrivateKey().GetScriptHash().BytesBE()) - require.NoError(t, err) - checkResult(t, res, stackitem.NewBool(true)) - - checkErr := func(t *testing.T, expectedErr error, tx *transaction.Transaction) { - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, expectedErr), "expected: %v, got: %v", expectedErr, err) - } - - testScript := []byte{byte(opcode.PUSH1)} - h := accs[0].PrivateKey().GetScriptHash() - t.Run("Expired", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.ValidUntilBlock = 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrTxExpired, tx) - }) - t.Run("BlockedAccount", func(t *testing.T) { - tx := bc.newTestTx(accs[1].PrivateKey().GetScriptHash(), testScript) - require.NoError(t, accs[1].SignTx(netmode.UnitTestNet, tx)) - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, ErrPolicy)) - }) - t.Run("InsufficientGas", func(t *testing.T) { - balance := bc.GetUtilityTokenBalance(h) - tx := bc.newTestTx(h, testScript) - tx.SystemFee = balance.Int64() + 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInsufficientFunds, tx) - }) - t.Run("TooBigTx", func(t *testing.T) { - script := make([]byte, transaction.MaxTransactionSize) - tx := bc.newTestTx(h, script) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrTxTooBig, tx) - }) - t.Run("NetworkFee", func(t *testing.T) { - t.Run("SmallNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.NetworkFee = 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrTxSmallNetworkFee, tx) - }) - t.Run("AlmostEnoughNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) - expectedSize := io.GetVarSize(tx) + calcultedScriptSize - calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = calculatedNetFee - 1 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.Equal(t, expectedSize, io.GetVarSize(tx)) - checkErr(t, ErrVerificationFailed, tx) - }) - t.Run("EnoughNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) - expectedSize := io.GetVarSize(tx) + calcultedScriptSize - calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = calculatedNetFee - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.Equal(t, expectedSize, io.GetVarSize(tx)) - require.NoError(t, bc.VerifyTx(tx)) - }) - t.Run("CalculateNetworkFee, signature script", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - expectedSize := io.GetVarSize(tx) - verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) - expectedSize += calculatedScriptSize - expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = expectedNetFee - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - actualSize := io.GetVarSize(tx) - require.Equal(t, expectedSize, actualSize) - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx) - gasConsumed, err := bc.verifyHashAgainstScript(h, &tx.Scripts[0], interopCtx, -1) - require.NoError(t, err) - require.Equal(t, verificationNetFee, gasConsumed) - require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) - }) - t.Run("CalculateNetworkFee, multisignature script", func(t *testing.T) { - multisigAcc := accs[4] - pKeys := keys.PublicKeys{multisigAcc.PrivateKey().PublicKey()} - require.NoError(t, multisigAcc.ConvertMultisig(1, pKeys)) - multisigHash := hash.Hash160(multisigAcc.Contract.Script) - tx := bc.newTestTx(multisigHash, testScript) - verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), multisigAcc.Contract.Script) - expectedSize := io.GetVarSize(tx) + calculatedScriptSize - expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() - tx.NetworkFee = expectedNetFee - require.NoError(t, multisigAcc.SignTx(netmode.UnitTestNet, tx)) - actualSize := io.GetVarSize(tx) - require.Equal(t, expectedSize, actualSize) - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx) - gasConsumed, err := bc.verifyHashAgainstScript(multisigHash, &tx.Scripts[0], interopCtx, -1) - require.NoError(t, err) - require.Equal(t, verificationNetFee, gasConsumed) - require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) - }) - }) - t.Run("InvalidTxScript", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Script = append(tx.Script, 0xff) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidScript, tx) - }) - t.Run("InvalidVerificationScript", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verif := []byte{byte(opcode.JMP), 3, 0xff, byte(opcode.PUSHT)} - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: hash.Hash160(verif), - Scopes: transaction.Global, - }) - tx.NetworkFee += 1000000 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{ - InvocationScript: []byte{}, - VerificationScript: verif, - }) - checkErr(t, ErrInvalidVerification, tx) - }) - t.Run("InvalidInvocationScript", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - verif := []byte{byte(opcode.PUSHT)} - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: hash.Hash160(verif), - Scopes: transaction.Global, - }) - tx.NetworkFee += 1000000 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{ - InvocationScript: []byte{byte(opcode.JMP), 3, 0xff}, - VerificationScript: verif, - }) - checkErr(t, ErrInvalidInvocation, tx) - }) - t.Run("Conflict", func(t *testing.T) { - balance := bc.GetUtilityTokenBalance(h).Int64() - tx := bc.newTestTx(h, testScript) - tx.NetworkFee = balance / 2 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, bc.PoolTx(tx)) - - tx2 := bc.newTestTx(h, testScript) - tx2.NetworkFee = balance / 2 - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) - err := bc.PoolTx(tx2) - require.True(t, errors.Is(err, ErrMemPoolConflict)) - }) - t.Run("InvalidWitnessHash", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts[0].VerificationScript = []byte{byte(opcode.PUSHT)} - checkErr(t, ErrWitnessHashMismatch, tx) - }) - t.Run("InvalidWitnessSignature", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10] - checkErr(t, ErrVerificationFailed, tx) - }) - t.Run("InsufficientNetworkFeeForSecondWitness", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: accs[3].PrivateKey().GetScriptHash(), - Scopes: transaction.Global, - }) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, accs[3].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrVerificationFailed, tx) - }) - t.Run("OldTX", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - b := bc.newBlock(tx) - require.NoError(t, bc.AddBlock(b)) - - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, ErrAlreadyExists)) - }) - t.Run("MemPooledTX", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, bc.PoolTx(tx)) - - err := bc.PoolTx(tx) - require.True(t, errors.Is(err, ErrAlreadyExists)) - }) - t.Run("MemPoolOOM", func(t *testing.T) { - bc.memPool = mempool.New(1, 0, false) - tx1 := bc.newTestTx(h, testScript) - tx1.NetworkFee += 10000 // Give it more priority. - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx1)) - require.NoError(t, bc.PoolTx(tx1)) - - tx2 := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) - err := bc.PoolTx(tx2) - require.True(t, errors.Is(err, ErrOOM)) - }) - t.Run("Attribute", func(t *testing.T) { - t.Run("InvalidHighPriority", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("ValidHighPriority", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - require.NoError(t, bc.VerifyTx(tx)) - }) - t.Run("Oracle", func(t *testing.T) { - orc := bc.contracts.Oracle - req := &state.OracleRequest{GasForResponse: 1000_0000} - require.NoError(t, orc.PutRequestInternal(1, req, bc.dao)) - - oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs) - require.NoError(t, err) - oracleHash := hash.Hash160(oracleScript) - - // We need to create new transaction, - // because hashes are cached after signing. - getOracleTx := func(t *testing.T) *transaction.Transaction { - tx := bc.newTestTx(h, orc.GetOracleResponseScript()) - resp := &transaction.OracleResponse{ - ID: 1, - Code: transaction.Success, - Result: []byte{1, 2, 3}, - } - tx.Attributes = []transaction.Attribute{{ - Type: transaction.OracleResponseT, - Value: resp, - }} - tx.NetworkFee += 4_000_000 // multisig check - tx.SystemFee = int64(req.GasForResponse - uint64(tx.NetworkFee)) - tx.Signers = []transaction.Signer{{ - Account: oracleHash, - Scopes: transaction.None, - }} - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), oracleScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - return tx - } - - t.Run("NoOracleNodes", func(t *testing.T) { - tx := getOracleTx(t) - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - - txSetOracle := transaction.New([]byte{byte(opcode.RET)}, 0) // it's a hack, so we don't need a real script - setSigner(txSetOracle, testchain.CommitteeScriptHash()) - txSetOracle.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(txSetOracle), - VerificationScript: testchain.CommitteeVerificationScript(), - }} - bl := block.New(bc.config.StateRootInHeader) - bl.Index = bc.BlockHeight() + 1 - ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetOracle) - ic.SpawnVM() - ic.VM.LoadScript([]byte{byte(opcode.RET)}) - require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, noderoles.Oracle, oraclePubs)) - _, err = ic.DAO.Persist() - require.NoError(t, err) - - t.Run("Valid", func(t *testing.T) { - tx := getOracleTx(t) - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - require.NoError(t, bc.VerifyTx(tx)) - - t.Run("NativeVerify", func(t *testing.T) { - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: bc.contracts.Oracle.Hash, - Scopes: transaction.None, - }) - tx.Scripts = append(tx.Scripts, transaction.Witness{}) - t.Run("NonZeroVerification", func(t *testing.T) { - w := io.NewBufBinWriter() - emit.Opcodes(w.BinWriter, opcode.ABORT) - emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE()) - emit.Int(w.BinWriter, 0) - emit.String(w.BinWriter, orc.Manifest.Name) - tx.Scripts[len(tx.Scripts)-1].VerificationScript = w.Bytes() - err := bc.VerifyTx(tx) - require.True(t, errors.Is(err, ErrNativeContractWitness), "got: %v", err) - }) - t.Run("Good", func(t *testing.T) { - tx.Scripts[len(tx.Scripts)-1].VerificationScript = nil - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - t.Run("InvalidRequestID", func(t *testing.T) { - tx := getOracleTx(t) - tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2 - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("InvalidScope", func(t *testing.T) { - tx := getOracleTx(t) - tx.Signers[0].Scopes = transaction.Global - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("InvalidScript", func(t *testing.T) { - tx := getOracleTx(t) - tx.Script = append(tx.Script, byte(opcode.NOP)) - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("InvalidSigner", func(t *testing.T) { - tx := getOracleTx(t) - tx.Signers[0].Account = accs[0].Contract.ScriptHash() - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - t.Run("SmallFee", func(t *testing.T) { - tx := getOracleTx(t) - tx.SystemFee = 0 - require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) - checkErr(t, ErrInvalidAttribute, tx) - }) - }) - t.Run("NotValidBefore", func(t *testing.T) { - getNVBTx := func(height uint32) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height}}) - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - return tx - } - t.Run("Disabled", func(t *testing.T) { - tx := getNVBTx(bc.blockHeight + 1) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("Enabled", func(t *testing.T) { - bc.config.P2PSigExtensions = true - t.Run("NotYetValid", func(t *testing.T) { - tx := getNVBTx(bc.blockHeight + 1) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) - }) - t.Run("positive", func(t *testing.T) { - tx := getNVBTx(bc.blockHeight) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - t.Run("Reserved", func(t *testing.T) { - getReservedTx := func(attrType transaction.AttrType) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: attrType, Value: &transaction.Reserved{Value: []byte{1, 2, 3}}}) - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - return tx - } - t.Run("Disabled", func(t *testing.T) { - tx := getReservedTx(transaction.ReservedLowerBound + 3) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("Enabled", func(t *testing.T) { - bc.config.ReservedAttributes = true - tx := getReservedTx(transaction.ReservedLowerBound + 3) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - t.Run("Conflicts", func(t *testing.T) { - getConflictsTx := func(hashes ...util.Uint256) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = make([]transaction.Attribute, len(hashes)) - for i, h := range hashes { - tx.Attributes[i] = transaction.Attribute{ - Type: transaction.ConflictsT, - Value: &transaction.Conflicts{ - Hash: h, - }, - } - } - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }} - rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }} - return tx - } - t.Run("disabled", func(t *testing.T) { - bc.config.P2PSigExtensions = false - tx := getConflictsTx(util.Uint256{1, 2, 3}) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("enabled", func(t *testing.T) { - bc.config.P2PSigExtensions = true - t.Run("dummy on-chain conflict", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) - conflicting := transaction.New([]byte{byte(opcode.RET)}, 1) - conflicting.Attributes = []transaction.Attribute{ - { - Type: transaction.ConflictsT, - Value: &transaction.Conflicts{ - Hash: tx.Hash(), - }, - }, - } - require.NoError(t, bc.dao.StoreAsTransaction(conflicting, bc.blockHeight, nil)) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrHasConflicts)) - }) - t.Run("attribute on-chain conflict", func(t *testing.T) { - tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = 4242 - tx.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }} - require.NoError(t, testchain.SignTx(bc, tx)) - b := bc.newBlock(tx) - - require.NoError(t, bc.AddBlock(b)) - txConflict := getConflictsTx(tx.Hash()) - require.Error(t, bc.VerifyTx(txConflict)) - }) - t.Run("positive", func(t *testing.T) { - tx := getConflictsTx(random.Uint256()) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - t.Run("NotaryAssisted", func(t *testing.T) { - notary, err := wallet.NewAccount() - require.NoError(t, err) - txSetNotary := transaction.New([]byte{byte(opcode.RET)}, 0) - setSigner(txSetNotary, testchain.CommitteeScriptHash()) - txSetNotary.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(txSetNotary), - VerificationScript: testchain.CommitteeVerificationScript(), - }} - bl := block.New(false) - bl.Index = bc.BlockHeight() + 1 - ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetNotary) - ic.SpawnVM() - ic.VM.LoadScript([]byte{byte(opcode.RET)}) - require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, noderoles.P2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()})) - _, err = ic.DAO.Persist() - require.NoError(t, err) - getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ - NKeys: signaturesCount, - }}) - tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute - tx.NetworkFee += 4_000_000 // multisig check - tx.Signers = []transaction.Signer{{ - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - rawScript := testchain.CommitteeVerificationScript() - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) - tx.NetworkFee += netFee - tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: rawScript, - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - return tx - } - t.Run("Disabled", func(t *testing.T) { - bc.config.P2PSigExtensions = false - tx := getNotaryAssistedTx(0, 0) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) - }) - t.Run("Enabled, insufficient network fee", func(t *testing.T) { - bc.config.P2PSigExtensions = true - tx := getNotaryAssistedTx(1, 0) - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("Test verify", func(t *testing.T) { - bc.config.P2PSigExtensions = true - t.Run("no NotaryAssisted attribute", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Attributes = []transaction.Attribute{} - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("no deposit", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("bad Notary signer scope", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.CalledByEntry, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("not signed by Notary", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("bad Notary node witness", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.None, - }, - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - acc, err := keys.NewPrivateKey() - require.NoError(t, err) - tx.Scripts = []transaction.Witness{ - { - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }, - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("missing payer", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - tx.Signers = []transaction.Signer{ - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - } - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...), - }, - } - require.Error(t, bc.VerifyTx(tx)) - }) - t.Run("positive", func(t *testing.T) { - tx := getNotaryAssistedTx(1, (1+1)*notaryServiceFeePerKey) - require.NoError(t, bc.VerifyTx(tx)) - }) - }) - }) - }) - t.Run("Partially-filled transaction", func(t *testing.T) { - bc.config.P2PSigExtensions = true - getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction { - tx := bc.newTestTx(h, testScript) - tx.ValidUntilBlock = validUntil - tx.Attributes = []transaction.Attribute{ - { - Type: transaction.NotValidBeforeT, - Value: &transaction.NotValidBefore{Height: nvb}, - }, - { - Type: transaction.NotaryAssistedT, - Value: &transaction.NotaryAssisted{NKeys: 0}, - }, - } - tx.Signers = []transaction.Signer{ - { - Account: bc.contracts.Notary.Hash, - Scopes: transaction.None, - }, - { - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }, - } - size := io.GetVarSize(tx) - netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), testchain.MultisigVerificationScript()) - tx.NetworkFee = netFee + // multisig witness verification price - int64(size)*bc.FeePerByte() + // fee for unsigned size - int64(sizeDelta)*bc.FeePerByte() + // fee for multisig size - 66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script) - 2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation) - notaryServiceFeePerKey + // fee for Notary attribute - fee.Opcode(bc.GetBaseExecFee(), // Notary verification script - opcode.PUSHDATA1, opcode.RET, // invocation script - opcode.PUSH0, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call - nativeprices.NotaryVerificationPrice*bc.GetBaseExecFee() // Notary witness verification price - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), - VerificationScript: []byte{}, - }, - { - InvocationScript: testchain.Sign(tx), - VerificationScript: testchain.MultisigVerificationScript(), - }, - } - return tx - } - - mp := mempool.New(10, 1, false) - verificationF := func(tx *transaction.Transaction, data interface{}) error { - if data.(int) > 5 { - return errors.New("bad data") - } - return nil - } - t.Run("failed pre-verification", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) - require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity. - }) - t.Run("GasLimitExceeded during witness verification", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) - tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx - tx.Scripts = []transaction.Witness{ - { - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), - VerificationScript: []byte{}, - }, - { - InvocationScript: testchain.Sign(tx), - VerificationScript: testchain.MultisigVerificationScript(), - }, - } - require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) - }) - t.Run("bad NVB: too big", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1, bc.blockHeight+1) - require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute)) - }) - t.Run("bad ValidUntilBlock: too small", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1) - require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute)) - }) - t.Run("good", func(t *testing.T) { - tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) - require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) - }) - }) -} - -func TestVerifyHashAgainstScript(t *testing.T) { - bc := newTestChain(t) - - cs, csInvalid := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, csInvalid)) - - gas := bc.contracts.Policy.GetMaxVerificationGas(ic.DAO) - t.Run("Contract", func(t *testing.T) { - t.Run("Missing", func(t *testing.T) { - newH := cs.Hash - newH[0] = ^newH[0] - w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(newH, w, ic, gas) - require.True(t, errors.Is(err, ErrUnknownVerificationContract)) - }) - t.Run("Invalid", func(t *testing.T) { - w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(csInvalid.Hash, w, ic, gas) - require.True(t, errors.Is(err, ErrInvalidVerificationContract)) - }) - t.Run("ValidSignature", func(t *testing.T) { - w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas) - require.NoError(t, err) - }) - t.Run("InvalidSignature", func(t *testing.T) { - w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH3)}} - _, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) - }) - }) - t.Run("NotEnoughGas", func(t *testing.T) { - verif := []byte{byte(opcode.PUSH1)} - w := &transaction.Witness{ - InvocationScript: []byte{byte(opcode.NOP)}, - VerificationScript: verif, - } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, 1) - require.True(t, errors.Is(err, ErrVerificationFailed)) - }) - t.Run("NoResult", func(t *testing.T) { - verif := []byte{byte(opcode.DROP)} - w := &transaction.Witness{ - InvocationScript: []byte{byte(opcode.PUSH1)}, - VerificationScript: verif, - } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) - }) - t.Run("BadResult", func(t *testing.T) { - verif := make([]byte, 66) - verif[0] = byte(opcode.PUSHDATA1) - verif[1] = 64 - w := &transaction.Witness{ - InvocationScript: []byte{byte(opcode.NOP)}, - VerificationScript: verif, - } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) - }) - t.Run("TooManyResults", func(t *testing.T) { - verif := []byte{byte(opcode.NOP)} - w := &transaction.Witness{ - InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.PUSH1)}, - VerificationScript: verif, - } - _, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas) - require.True(t, errors.Is(err, ErrVerificationFailed)) - }) -} - -func TestIsTxStillRelevant(t *testing.T) { - bc := newTestChain(t) - - mp := bc.GetMemPool() - newTx := func(t *testing.T) *transaction.Transaction { - tx := transaction.New([]byte{byte(opcode.RET)}, 100) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - tx.Signers = []transaction.Signer{{ - Account: neoOwner, - Scopes: transaction.CalledByEntry, - }} - return tx - } - - t.Run("small ValidUntilBlock", func(t *testing.T) { - tx := newTx(t) - require.NoError(t, testchain.SignTx(bc, tx)) - - require.True(t, bc.IsTxStillRelevant(tx, nil, false)) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.False(t, bc.IsTxStillRelevant(tx, nil, false)) - }) - - t.Run("tx is already persisted", func(t *testing.T) { - tx := newTx(t) - tx.ValidUntilBlock = bc.BlockHeight() + 2 - require.NoError(t, testchain.SignTx(bc, tx)) - - require.True(t, bc.IsTxStillRelevant(tx, nil, false)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - require.False(t, bc.IsTxStillRelevant(tx, nil, false)) - }) - - t.Run("tx with Conflicts attribute", func(t *testing.T) { - tx1 := newTx(t) - require.NoError(t, testchain.SignTx(bc, tx1)) - - tx2 := newTx(t) - tx2.Attributes = []transaction.Attribute{{ - Type: transaction.ConflictsT, - Value: &transaction.Conflicts{Hash: tx1.Hash()}, - }} - require.NoError(t, testchain.SignTx(bc, tx2)) - - require.True(t, bc.IsTxStillRelevant(tx1, mp, false)) - require.NoError(t, bc.verifyAndPoolTx(tx2, mp, bc)) - require.False(t, bc.IsTxStillRelevant(tx1, mp, false)) - }) - t.Run("NotValidBefore", func(t *testing.T) { - tx3 := newTx(t) - tx3.Attributes = []transaction.Attribute{{ - Type: transaction.NotValidBeforeT, - Value: &transaction.NotValidBefore{Height: bc.BlockHeight() + 1}, - }} - tx3.ValidUntilBlock = bc.BlockHeight() + 2 - require.NoError(t, testchain.SignTx(bc, tx3)) - - require.False(t, bc.IsTxStillRelevant(tx3, nil, false)) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.True(t, bc.IsTxStillRelevant(tx3, nil, false)) - }) - t.Run("contract witness check fails", func(t *testing.T) { - src := fmt.Sprintf(`package verify - import ( - "github.com/nspcc-dev/neo-go/pkg/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/interop/util" - ) - func Verify() bool { - addr := util.FromAddress("`+address.Uint160ToString(bc.contracts.Ledger.Hash)+`") - currentHeight := contract.Call(addr, "currentIndex", contract.ReadStates) - return currentHeight.(int) < %d - }`, bc.BlockHeight()+2) // deploy + next block - txDeploy, h, _, err := testchain.NewDeployTx(bc, "TestVerify.go", neoOwner, strings.NewReader(src), nil) - require.NoError(t, err) - txDeploy.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, txDeploy) - require.NoError(t, testchain.SignTx(bc, txDeploy)) - require.NoError(t, bc.AddBlock(bc.newBlock(txDeploy))) - - tx := newTx(t) - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: h, - Scopes: transaction.None, - }) - tx.NetworkFee += 10_000_000 - require.NoError(t, testchain.SignTx(bc, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{}) - - require.True(t, bc.IsTxStillRelevant(tx, mp, false)) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.False(t, bc.IsTxStillRelevant(tx, mp, false)) - }) -} - -func TestMemPoolRemoval(t *testing.T) { - const added = 16 - const notAdded = 32 - bc := newTestChain(t) - addedTxes := make([]*transaction.Transaction, added) - notAddedTxes := make([]*transaction.Transaction, notAdded) - for i := range addedTxes { - addedTxes[i] = bc.newTestTx(testchain.MultisigScriptHash(), []byte{byte(opcode.PUSH1)}) - require.NoError(t, testchain.SignTx(bc, addedTxes[i])) - require.NoError(t, bc.PoolTx(addedTxes[i])) - } - for i := range notAddedTxes { - notAddedTxes[i] = bc.newTestTx(testchain.MultisigScriptHash(), []byte{byte(opcode.PUSH1)}) - require.NoError(t, testchain.SignTx(bc, notAddedTxes[i])) - require.NoError(t, bc.PoolTx(notAddedTxes[i])) - } - b := bc.newBlock(addedTxes...) - require.NoError(t, bc.AddBlock(b)) - mempool := bc.GetMemPool() - for _, tx := range addedTxes { - require.False(t, mempool.ContainsKey(tx.Hash())) - } - for _, tx := range notAddedTxes { - require.True(t, mempool.ContainsKey(tx.Hash())) - } -} - -func TestHasBlock(t *testing.T) { - bc := newTestChain(t) - blocks, err := bc.genBlocks(50) - require.NoError(t, err) - - for i := 0; i < len(blocks); i++ { - assert.True(t, bc.HasBlock(blocks[i].Hash())) - } - newBlock := bc.newBlock() - assert.False(t, bc.HasBlock(newBlock.Hash())) -} - -func TestGetTransaction(t *testing.T) { - bc := newTestChain(t) - tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx1.ValidUntilBlock = 16 - tx1.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.CalledByEntry, - }} - tx2 := transaction.New([]byte{byte(opcode.PUSH2)}, 0) - tx2.ValidUntilBlock = 16 - tx2.Signers = []transaction.Signer{{ - Account: testchain.MultisigScriptHash(), - Scopes: transaction.CalledByEntry, - }} - require.NoError(t, testchain.SignTx(bc, tx1, tx2)) - b1 := bc.newBlock(tx1) - - assert.Nil(t, bc.AddBlock(b1)) - block := bc.newBlock(tx2) - txSize := io.GetVarSize(tx2) - assert.Nil(t, bc.AddBlock(block)) - - tx, height, err := bc.GetTransaction(block.Transactions[0].Hash()) - require.Nil(t, err) - assert.Equal(t, block.Index, height) - assert.Equal(t, txSize, tx.Size()) - assert.Equal(t, block.Transactions[0], tx) - assert.Equal(t, 1, io.GetVarSize(tx.Attributes)) - assert.Equal(t, 1, io.GetVarSize(tx.Scripts)) -} - -func TestGetClaimable(t *testing.T) { - bc := newTestChain(t) - - _, err := bc.genBlocks(10) - require.NoError(t, err) - - t.Run("first generation period", func(t *testing.T) { - amount, err := bc.CalculateClaimable(neoOwner, 1) - require.NoError(t, err) - require.EqualValues(t, big.NewInt(5*native.GASFactor/10), amount) - }) -} - -func TestClose(t *testing.T) { - defer func() { - r := recover() - assert.NotNil(t, r) - }() - bc := initTestChain(t, nil, nil) - go bc.Run() - _, err := bc.genBlocks(10) - require.NoError(t, err) - bc.Close() - // It's a hack, but we use internal knowledge of MemoryStore - // implementation which makes it completely unusable (up to panicing) - // after Close(). - bc.dao.Store.Put([]byte{0}, []byte{1}) - - // This should never be executed. - assert.Nil(t, t) -} - -func TestSubscriptions(t *testing.T) { - // We use buffering here as a substitute for reader goroutines, events - // get queued up and we read them one by one here. - const chBufSize = 16 - blockCh := make(chan *block.Block, chBufSize) - txCh := make(chan *transaction.Transaction, chBufSize) - notificationCh := make(chan *subscriptions.NotificationEvent, chBufSize) - executionCh := make(chan *state.AppExecResult, chBufSize) - - bc := newTestChain(t) - bc.SubscribeForBlocks(blockCh) - bc.SubscribeForTransactions(txCh) - bc.SubscribeForNotifications(notificationCh) - bc.SubscribeForExecutions(executionCh) - - assert.Empty(t, notificationCh) - assert.Empty(t, executionCh) - assert.Empty(t, blockCh) - assert.Empty(t, txCh) - - blocks, err := bc.genBlocks(1) - require.NoError(t, err) - require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond) - assert.Len(t, notificationCh, 1) // validator bounty - assert.Len(t, executionCh, 2) - assert.Empty(t, txCh) - - b := <-blockCh - assert.Equal(t, blocks[0], b) - assert.Empty(t, blockCh) - - aer := <-executionCh - assert.Equal(t, b.Hash(), aer.Container) - aer = <-executionCh - assert.Equal(t, b.Hash(), aer.Container) - - notif := <-notificationCh - require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash) - - script := io.NewBufBinWriter() - emit.Bytes(script.BinWriter, []byte("yay!")) - emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) - require.NoError(t, script.Err) - txGood1 := transaction.New(script.Bytes(), 0) - txGood1.Signers = []transaction.Signer{{Account: neoOwner}} - txGood1.Nonce = 1 - txGood1.ValidUntilBlock = 1024 - require.NoError(t, testchain.SignTx(bc, txGood1)) - - // Reset() reuses the script buffer and we need to keep scripts. - script = io.NewBufBinWriter() - emit.Bytes(script.BinWriter, []byte("nay!")) - emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) - emit.Opcodes(script.BinWriter, opcode.THROW) - require.NoError(t, script.Err) - txBad := transaction.New(script.Bytes(), 0) - txBad.Signers = []transaction.Signer{{Account: neoOwner}} - txBad.Nonce = 2 - txBad.ValidUntilBlock = 1024 - require.NoError(t, testchain.SignTx(bc, txBad)) - - script = io.NewBufBinWriter() - emit.Bytes(script.BinWriter, []byte("yay! yay! yay!")) - emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) - require.NoError(t, script.Err) - txGood2 := transaction.New(script.Bytes(), 0) - txGood2.Signers = []transaction.Signer{{Account: neoOwner}} - txGood2.Nonce = 3 - txGood2.ValidUntilBlock = 1024 - require.NoError(t, testchain.SignTx(bc, txGood2)) - - invBlock := newBlock(bc.config, bc.BlockHeight()+1, bc.CurrentHeaderHash(), txGood1, txBad, txGood2) - require.NoError(t, bc.AddBlock(invBlock)) - - require.Eventually(t, func() bool { - return len(blockCh) != 0 && len(txCh) != 0 && - len(notificationCh) != 0 && len(executionCh) != 0 - }, time.Second, 10*time.Millisecond) - - b = <-blockCh - require.Equal(t, invBlock, b) - assert.Empty(t, blockCh) - - exec := <-executionCh - require.Equal(t, b.Hash(), exec.Container) - require.Equal(t, exec.VMState, vm.HaltState) - - // 3 burn events for every tx and 1 mint for primary node - require.True(t, len(notificationCh) >= 4) - for i := 0; i < 4; i++ { - notif := <-notificationCh - require.Equal(t, bc.contracts.GAS.Hash, notif.ScriptHash) - } - - // Follow in-block transaction order. - for _, txExpected := range invBlock.Transactions { - tx := <-txCh - require.Equal(t, txExpected, tx) - exec := <-executionCh - require.Equal(t, tx.Hash(), exec.Container) - if exec.VMState == vm.HaltState { - notif := <-notificationCh - require.Equal(t, hash.Hash160(tx.Script), notif.ScriptHash) - } - } - assert.Empty(t, txCh) - assert.Len(t, notificationCh, 1) - assert.Len(t, executionCh, 1) - - notif = <-notificationCh - require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash) - - exec = <-executionCh - require.Equal(t, b.Hash(), exec.Container) - require.Equal(t, exec.VMState, vm.HaltState) - - bc.UnsubscribeFromBlocks(blockCh) - bc.UnsubscribeFromTransactions(txCh) - bc.UnsubscribeFromNotifications(notificationCh) - bc.UnsubscribeFromExecutions(executionCh) - - // Ensure that new blocks are processed correctly after unsubscription. - _, err = bc.genBlocks(2 * chBufSize) - require.NoError(t, err) -} - func TestRemoveOldTransfers(t *testing.T) { // Creating proper number of transfers/blocks takes unneccessary time, so emulate // some DB with stale entries. @@ -1562,182 +136,6 @@ func TestRemoveOldTransfers(t *testing.T) { } } -func TestRemoveUntraceable(t *testing.T) { - check := func(t *testing.T, bc *Blockchain, tHash, bHash, sHash util.Uint256, errorExpected bool) { - _, _, err := bc.GetTransaction(tHash) - if errorExpected { - require.Error(t, err) - } else { - require.NoError(t, err) - } - _, err = bc.GetAppExecResults(tHash, trigger.Application) - if errorExpected { - require.Error(t, err) - } else { - require.NoError(t, err) - } - _, err = bc.GetBlock(bHash) - if errorExpected { - require.Error(t, err) - } else { - require.NoError(t, err) - } - _, err = bc.GetHeader(bHash) - require.NoError(t, err) - if !sHash.Equals(util.Uint256{}) { - sm := bc.GetStateModule() - _, err = sm.GetState(sHash, []byte{0xfb, 0xff, 0xff, 0xff, 0x0e}) // NEO committee key. - if errorExpected { - require.Error(t, err) - } else { - require.NoError(t, err) - } - } - } - t.Run("P2PStateExchangeExtensions off", func(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.MaxTraceableBlocks = 2 - c.ProtocolConfiguration.GarbageCollectionPeriod = 2 - c.ProtocolConfiguration.RemoveUntraceableBlocks = true - }) - - tx1, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - b1 := bc.newBlock(tx1) - require.NoError(t, bc.AddBlock(b1)) - tx1Height := bc.BlockHeight() - sRoot, err := bc.GetStateModule().GetStateRoot(tx1Height) - require.NoError(t, err) - - tx2, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - require.NoError(t, bc.AddBlock(bc.newBlock(tx2))) - - _, h1, err := bc.GetTransaction(tx1.Hash()) - require.NoError(t, err) - require.Equal(t, tx1Height, h1) - - check(t, bc, tx1.Hash(), b1.Hash(), sRoot.Root, false) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - // Don't wait for Run(). - _, err = bc.persist(true) - require.NoError(t, err) - bc.tryRunGC(0) - check(t, bc, tx1.Hash(), b1.Hash(), sRoot.Root, true) - }) - t.Run("P2PStateExchangeExtensions on", func(t *testing.T) { - bc := newTestChainWithCustomCfg(t, func(c *config.Config) { - c.ProtocolConfiguration.MaxTraceableBlocks = 2 - c.ProtocolConfiguration.GarbageCollectionPeriod = 2 - c.ProtocolConfiguration.RemoveUntraceableBlocks = true - c.ProtocolConfiguration.P2PStateExchangeExtensions = true - c.ProtocolConfiguration.StateSyncInterval = 2 - c.ProtocolConfiguration.StateRootInHeader = true - }) - - tx1, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - b1 := bc.newBlock(tx1) - require.NoError(t, bc.AddBlock(b1)) - tx1Height := bc.BlockHeight() - sRoot, err := bc.GetStateModule().GetStateRoot(tx1Height) - require.NoError(t, err) - - tx2, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1) - require.NoError(t, err) - b2 := bc.newBlock(tx2) - require.NoError(t, bc.AddBlock(b2)) - tx2Height := bc.BlockHeight() - - _, h1, err := bc.GetTransaction(tx1.Hash()) - require.NoError(t, err) - require.Equal(t, tx1Height, h1) - - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - require.NoError(t, bc.AddBlock(bc.newBlock())) - - check(t, bc, tx1.Hash(), b1.Hash(), sRoot.Root, false) - check(t, bc, tx2.Hash(), b2.Hash(), sRoot.Root, false) - - require.NoError(t, bc.AddBlock(bc.newBlock())) - - check(t, bc, tx1.Hash(), b1.Hash(), util.Uint256{}, true) - check(t, bc, tx2.Hash(), b2.Hash(), util.Uint256{}, false) - _, h2, err := bc.GetTransaction(tx2.Hash()) - require.NoError(t, err) - require.Equal(t, tx2Height, h2) - }) -} - -func TestInvalidNotification(t *testing.T) { - bc := newTestChain(t) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "invalidStack1") - require.NoError(t, err) - require.Equal(t, 1, len(aer.Stack)) - require.Nil(t, aer.Stack[0]) - - aer, err = invokeContractMethod(bc, 1_00000000, cs.Hash, "invalidStack2") - require.NoError(t, err) - require.Equal(t, 1, len(aer.Stack)) - require.Equal(t, stackitem.InteropT, aer.Stack[0].Type()) -} - -// Test that deletion of non-existent doesn't result in error in tx or block addition. -func TestMPTDeleteNoKey(t *testing.T) { - bc := newTestChain(t) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "delValue", "non-existent-key") - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState) -} - -// Test that UpdateHistory is added to ProtocolConfiguration for all native contracts -// for all default configurations. If UpdateHistory is not added to config, then -// native contract is disabled. It's easy to forget about config while adding new -// native contract. -func TestConfigNativeUpdateHistory(t *testing.T) { - var prefixPath = filepath.Join("..", "..", "config") - check := func(t *testing.T, cfgFileSuffix interface{}) { - cfgPath := filepath.Join(prefixPath, fmt.Sprintf("protocol.%s.yml", cfgFileSuffix)) - cfg, err := config.LoadFile(cfgPath) - require.NoError(t, err, fmt.Errorf("failed to load %s", cfgPath)) - natives := native.NewContracts(cfg.ProtocolConfiguration) - assert.Equal(t, len(natives.Contracts), - len(cfg.ProtocolConfiguration.NativeUpdateHistories), - fmt.Errorf("protocol configuration file %s: extra or missing NativeUpdateHistory in NativeActivations section", cfgPath)) - for _, c := range natives.Contracts { - assert.NotNil(t, cfg.ProtocolConfiguration.NativeUpdateHistories[c.Metadata().Name], - fmt.Errorf("protocol configuration file %s: configuration for %s native contract is missing in NativeActivations section; "+ - "edit the test if the contract should be disabled", cfgPath, c.Metadata().Name)) - } - } - testCases := []interface{}{ - netmode.MainNet, - netmode.PrivNet, - netmode.TestNet, - netmode.UnitTestNet, - "privnet.docker.one", - "privnet.docker.two", - "privnet.docker.three", - "privnet.docker.four", - "privnet.docker.single", - "unit_testnet.single", - } - for _, tc := range testCases { - check(t, tc) - } -} - func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) { var ( stateSyncInterval = 4 diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index 7caa409ef..e09be9a04 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -4,20 +4,47 @@ import ( "encoding/binary" "errors" "fmt" + "math/big" + "path/filepath" "strings" "testing" + "time" + "github.com/nspcc-dev/neo-go/internal/contracts" + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/fee" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativeprices" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "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/assert" "github.com/stretchr/testify/require" ) @@ -336,3 +363,1614 @@ func TestBlockchain_StartFromExistingDB(t *testing.T) { require.NoError(t, err) }) } + +func TestBlockchain_AddHeaders(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + newHeader := func(t *testing.T, index uint32, prevHash util.Uint256, timestamp uint64) *block.Header { + b := e.NewUnsignedBlock(t) + b.Index = index + b.PrevHash = prevHash + b.PrevStateRoot = util.Uint256{} + b.Timestamp = timestamp + e.SignBlock(b) + return &b.Header + } + b1 := e.NewUnsignedBlock(t) + h1 := &e.SignBlock(b1).Header + h2 := newHeader(t, h1.Index+1, h1.Hash(), h1.Timestamp+1) + h3 := newHeader(t, h2.Index+1, h2.Hash(), h2.Timestamp+1) + + require.NoError(t, bc.AddHeaders()) + require.NoError(t, bc.AddHeaders(h1, h2)) + require.NoError(t, bc.AddHeaders(h2, h3)) + + assert.Equal(t, h3.Index, bc.HeaderHeight()) + assert.Equal(t, uint32(0), bc.BlockHeight()) + assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) + + // Add them again, they should not be added. + require.NoError(t, bc.AddHeaders(h3, h2, h1)) + + assert.Equal(t, h3.Index, bc.HeaderHeight()) + assert.Equal(t, uint32(0), bc.BlockHeight()) + assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) + + h4Bad := newHeader(t, h3.Index+1, h3.Hash().Reverse(), h3.Timestamp+1) + h5Bad := newHeader(t, h4Bad.Index+1, h4Bad.Hash(), h4Bad.Timestamp+1) + + assert.Error(t, bc.AddHeaders(h4Bad, h5Bad)) + assert.Equal(t, h3.Index, bc.HeaderHeight()) + assert.Equal(t, uint32(0), bc.BlockHeight()) + assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) + + h4Bad2 := newHeader(t, h3.Index+1, h3.Hash().Reverse(), h3.Timestamp+1) + h4Bad2.Script.InvocationScript = []byte{} + assert.Error(t, bc.AddHeaders(h4Bad2)) + assert.Equal(t, h3.Index, bc.HeaderHeight()) + assert.Equal(t, uint32(0), bc.BlockHeight()) + assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) +} + +func TestBlockchain_AddBlockStateRoot(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + sr, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight()) + require.NoError(t, err) + + b := e.NewUnsignedBlock(t) + b.StateRootEnabled = false + b.PrevStateRoot = util.Uint256{} + e.SignBlock(b) + err = bc.AddBlock(b) + require.True(t, errors.Is(err, core.ErrHdrStateRootSetting), "got: %v", err) + + u := sr.Root + u[0] ^= 0xFF + b = e.NewUnsignedBlock(t) + b.PrevStateRoot = u + e.SignBlock(b) + err = bc.AddBlock(b) + require.True(t, errors.Is(err, core.ErrHdrInvalidStateRoot), "got: %v", err) + + b = e.NewUnsignedBlock(t) + e.SignBlock(b) + require.NoError(t, bc.AddBlock(b)) +} + +func TestBlockchain_AddHeadersStateRoot(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.StateRootInHeader = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + b := e.NewUnsignedBlock(t) + e.SignBlock(b) + h1 := b.Header + r := bc.GetStateModule().CurrentLocalStateRoot() + + // invalid stateroot + h1.PrevStateRoot[0] ^= 0xFF + require.True(t, errors.Is(bc.AddHeaders(&h1), core.ErrHdrInvalidStateRoot)) + + // valid stateroot + h1.PrevStateRoot = r + require.NoError(t, bc.AddHeaders(&h1)) + + // unable to verify stateroot (stateroot is computed for block #0 only => can + // verify stateroot of header #1 only) => just store the header + b = e.NewUnsignedBlock(t) + b.PrevHash = h1.Hash() + b.Timestamp = h1.Timestamp + 1 + b.PrevStateRoot = util.Uint256{} + b.Index = h1.Index + 1 + e.SignBlock(b) + require.NoError(t, bc.AddHeaders(&b.Header)) +} + +func TestBlockchain_AddBadBlock(t *testing.T) { + check := func(t *testing.T, b *block.Block, cfg func(c *config.ProtocolConfiguration)) { + bc, _ := chain.NewSingleWithCustomConfig(t, cfg) + err := bc.AddBlock(b) + if cfg == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + neoHash := e.NativeHash(t, nativenames.Neo) + + tx := e.NewUnsignedTx(t, neoHash, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + tx.ValidUntilBlock = 0 // Intentionally make the transaction invalid. + e.SignTx(t, tx, -1, acc) + b := e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + check(t, b, nil) + check(t, b, func(c *config.ProtocolConfiguration) { + c.VerifyBlocks = false + }) + + b = e.NewUnsignedBlock(t) + b.PrevHash = util.Uint256{} // Intentionally make block invalid. + e.SignBlock(b) + check(t, b, nil) + check(t, b, func(c *config.ProtocolConfiguration) { + c.VerifyBlocks = false + }) + + tx = e.NewUnsignedTx(t, neoHash, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) // Check the good tx. + e.SignTx(t, tx, -1, acc) + b = e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + check(t, b, func(c *config.ProtocolConfiguration) { + c.VerifyTransactions = true + c.VerifyBlocks = true + }) +} + +func TestBlockchain_GetHeader(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + block := e.AddNewBlock(t) + hash := block.Hash() + header, err := bc.GetHeader(hash) + require.NoError(t, err) + assert.Equal(t, &block.Header, header) + + b2 := e.NewUnsignedBlock(t) + _, err = bc.GetHeader(b2.Hash()) + assert.Error(t, err) +} + +func TestBlockchain_GetBlock(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + blocks := e.GenerateNewBlocks(t, 10) + neoValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Neo)) + + for i := 0; i < len(blocks); i++ { + block, err := bc.GetBlock(blocks[i].Hash()) + require.NoErrorf(t, err, "can't get block %d: %s", i, err) + assert.Equal(t, blocks[i].Index, block.Index) + assert.Equal(t, blocks[i].Hash(), block.Hash()) + } + + t.Run("store only header", func(t *testing.T) { + t.Run("non-empty block", func(t *testing.T) { + tx := neoValidatorInvoker.PrepareInvoke(t, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + b := e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + require.NoError(t, bc.AddHeaders(&b.Header)) + + _, err := bc.GetBlock(b.Hash()) + require.Error(t, err) + + _, err = bc.GetHeader(b.Hash()) + require.NoError(t, err) + + require.NoError(t, bc.AddBlock(b)) + + _, err = bc.GetBlock(b.Hash()) + require.NoError(t, err) + }) + t.Run("empty block", func(t *testing.T) { + b := e.NewUnsignedBlock(t) + e.SignBlock(b) + + require.NoError(t, bc.AddHeaders(&b.Header)) + + _, err := bc.GetBlock(b.Hash()) + require.NoError(t, err) + }) + }) +} + +func TestBlockchain_VerifyHashAgainstScript(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + cs, csInvalid := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + c1 := &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + } + c2 := &neotest.Contract{ + Hash: csInvalid.Hash, + NEF: &csInvalid.NEF, + Manifest: &csInvalid.Manifest, + } + e.DeployContract(t, c1, nil) + e.DeployContract(t, c2, nil) + + gas := bc.GetMaxVerificationGAS() + t.Run("Contract", func(t *testing.T) { + t.Run("Missing", func(t *testing.T) { + newH := cs.Hash + newH[0] = ^newH[0] + w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} + _, err := bc.VerifyWitness(newH, nil, w, gas) + require.True(t, errors.Is(err, core.ErrUnknownVerificationContract)) + }) + t.Run("Invalid", func(t *testing.T) { + w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} + _, err := bc.VerifyWitness(csInvalid.Hash, nil, w, gas) + require.True(t, errors.Is(err, core.ErrInvalidVerificationContract)) + }) + t.Run("ValidSignature", func(t *testing.T) { + w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} + _, err := bc.VerifyWitness(cs.Hash, nil, w, gas) + require.NoError(t, err) + }) + t.Run("InvalidSignature", func(t *testing.T) { + w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH3)}} + _, err := bc.VerifyWitness(cs.Hash, nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) + }) + }) + t.Run("NotEnoughGas", func(t *testing.T) { + verif := []byte{byte(opcode.PUSH1)} + w := &transaction.Witness{ + InvocationScript: []byte{byte(opcode.NOP)}, + VerificationScript: verif, + } + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, 1) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) + }) + t.Run("NoResult", func(t *testing.T) { + verif := []byte{byte(opcode.DROP)} + w := &transaction.Witness{ + InvocationScript: []byte{byte(opcode.PUSH1)}, + VerificationScript: verif, + } + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) + }) + t.Run("BadResult", func(t *testing.T) { + verif := make([]byte, 66) + verif[0] = byte(opcode.PUSHDATA1) + verif[1] = 64 + w := &transaction.Witness{ + InvocationScript: []byte{byte(opcode.NOP)}, + VerificationScript: verif, + } + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) + }) + t.Run("TooManyResults", func(t *testing.T) { + verif := []byte{byte(opcode.NOP)} + w := &transaction.Witness{ + InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.PUSH1)}, + VerificationScript: verif, + } + _, err := bc.VerifyWitness(hash.Hash160(verif), nil, w, gas) + require.True(t, errors.Is(err, core.ErrVerificationFailed)) + }) +} + +func TestBlockchain_IsTxStillRelevant(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + mp := bc.GetMemPool() + + t.Run("small ValidUntilBlock", func(t *testing.T) { + tx := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, bc.BlockHeight()+1) + + require.True(t, bc.IsTxStillRelevant(tx, nil, false)) + e.AddNewBlock(t) + require.False(t, bc.IsTxStillRelevant(tx, nil, false)) + }) + + t.Run("tx is already persisted", func(t *testing.T) { + tx := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, bc.BlockHeight()+2) + + require.True(t, bc.IsTxStillRelevant(tx, nil, false)) + e.AddNewBlock(t, tx) + require.False(t, bc.IsTxStillRelevant(tx, nil, false)) + }) + + t.Run("tx with Conflicts attribute", func(t *testing.T) { + tx1 := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, bc.BlockHeight()+5) + + tx2 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + tx2.Nonce = neotest.Nonce() + tx2.ValidUntilBlock = e.Chain.BlockHeight() + 5 + tx2.Attributes = []transaction.Attribute{{ + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{Hash: tx1.Hash()}, + }} + e.SignTx(t, tx2, -1, acc) + + require.True(t, bc.IsTxStillRelevant(tx1, mp, false)) + require.NoError(t, bc.PoolTx(tx2)) + require.False(t, bc.IsTxStillRelevant(tx1, mp, false)) + }) + t.Run("NotValidBefore", func(t *testing.T) { + tx3 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + tx3.Nonce = neotest.Nonce() + tx3.Attributes = []transaction.Attribute{{ + Type: transaction.NotValidBeforeT, + Value: &transaction.NotValidBefore{Height: bc.BlockHeight() + 1}, + }} + tx3.ValidUntilBlock = bc.BlockHeight() + 2 + e.SignTx(t, tx3, -1, acc) + + require.False(t, bc.IsTxStillRelevant(tx3, nil, false)) + e.AddNewBlock(t) + require.True(t, bc.IsTxStillRelevant(tx3, nil, false)) + }) + t.Run("contract witness check fails", func(t *testing.T) { + src := fmt.Sprintf(`package verify + import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/util" + ) + func Verify() bool { + addr := util.FromAddress("`+address.Uint160ToString(e.NativeHash(t, nativenames.Ledger))+`") + currentHeight := contract.Call(addr, "currentIndex", contract.ReadStates) + return currentHeight.(int) < %d + }`, bc.BlockHeight()+2) // deploy + next block + c := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(src), &compiler.Options{ + Name: "verification_contract", + }) + e.DeployContract(t, c, nil) + + tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = bc.BlockHeight() + 2 + tx.Signers = []transaction.Signer{ + { + Account: c.Hash, + Scopes: transaction.None, + }, + } + tx.NetworkFee += 10_000_000 + tx.Scripts = []transaction.Witness{{}} + + require.True(t, bc.IsTxStillRelevant(tx, mp, false)) + e.AddNewBlock(t) + require.False(t, bc.IsTxStillRelevant(tx, mp, false)) + }) +} + +func TestBlockchain_MemPoolRemoval(t *testing.T) { + const added = 16 + const notAdded = 32 + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + addedTxes := make([]*transaction.Transaction, added) + notAddedTxes := make([]*transaction.Transaction, notAdded) + for i := range addedTxes { + addedTxes[i] = e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, 100) + require.NoError(t, bc.PoolTx(addedTxes[i])) + } + for i := range notAddedTxes { + notAddedTxes[i] = e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}, 100) + require.NoError(t, bc.PoolTx(notAddedTxes[i])) + } + mempool := bc.GetMemPool() + e.AddNewBlock(t, addedTxes...) + for _, tx := range addedTxes { + require.False(t, mempool.ContainsKey(tx.Hash())) + } + for _, tx := range notAddedTxes { + require.True(t, mempool.ContainsKey(tx.Hash())) + } +} + +func TestBlockchain_HasBlock(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + blocks := e.GenerateNewBlocks(t, 10) + + for i := 0; i < len(blocks); i++ { + assert.True(t, bc.HasBlock(blocks[i].Hash())) + } + newBlock := e.NewUnsignedBlock(t) + assert.False(t, bc.HasBlock(newBlock.Hash())) +} + +func TestBlockchain_GetTransaction(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + tx1 := e.PrepareInvocation(t, []byte{byte(opcode.PUSH1)}, []neotest.Signer{acc}) + e.AddNewBlock(t, tx1) + + tx2 := e.PrepareInvocation(t, []byte{byte(opcode.PUSH2)}, []neotest.Signer{acc}) + tx2Size := io.GetVarSize(tx2) + b := e.AddNewBlock(t, tx2) + + tx, height, err := bc.GetTransaction(tx2.Hash()) + require.Nil(t, err) + assert.Equal(t, b.Index, height) + assert.Equal(t, tx2Size, tx.Size()) + assert.Equal(t, b.Transactions[0], tx) +} + +func TestBlockchain_GetClaimable(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + e.GenerateNewBlocks(t, 10) + + t.Run("first generation period", func(t *testing.T) { + amount, err := bc.CalculateClaimable(acc.ScriptHash(), 1) + require.NoError(t, err) + require.EqualValues(t, big.NewInt(5*native.GASFactor/10), amount) + }) +} + +func TestBlockchain_Close(t *testing.T) { + st := storage.NewMemoryStore() + bc, acc := chain.NewSingleWithCustomConfigAndStore(t, nil, st, false) + e := neotest.NewExecutor(t, bc, acc, acc) + go bc.Run() + e.GenerateNewBlocks(t, 10) + bc.Close() + // It's a hack, but we use internal knowledge of MemoryStore + // implementation which makes it completely unusable (up to panicing) + // after Close(). + require.Panics(t, func() { + _ = st.PutChangeSet(map[string][]byte{"0": {1}}, nil) + }) +} + +func TestBlockchain_Subscriptions(t *testing.T) { + // We use buffering here as a substitute for reader goroutines, events + // get queued up and we read them one by one here. + const chBufSize = 16 + blockCh := make(chan *block.Block, chBufSize) + txCh := make(chan *transaction.Transaction, chBufSize) + notificationCh := make(chan *subscriptions.NotificationEvent, chBufSize) + executionCh := make(chan *state.AppExecResult, chBufSize) + + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + nativeGASHash := e.NativeHash(t, nativenames.Gas) + bc.SubscribeForBlocks(blockCh) + bc.SubscribeForTransactions(txCh) + bc.SubscribeForNotifications(notificationCh) + bc.SubscribeForExecutions(executionCh) + + assert.Empty(t, notificationCh) + assert.Empty(t, executionCh) + assert.Empty(t, blockCh) + assert.Empty(t, txCh) + + generatedB := e.AddNewBlock(t) + require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond) + assert.Len(t, notificationCh, 1) // validator bounty + assert.Len(t, executionCh, 2) + assert.Empty(t, txCh) + + b := <-blockCh + assert.Equal(t, generatedB, b) + assert.Empty(t, blockCh) + + aer := <-executionCh + assert.Equal(t, b.Hash(), aer.Container) + aer = <-executionCh + assert.Equal(t, b.Hash(), aer.Container) + + notif := <-notificationCh + require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash) + + script := io.NewBufBinWriter() + emit.Bytes(script.BinWriter, []byte("yay!")) + emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) + require.NoError(t, script.Err) + txGood1 := e.PrepareInvocation(t, script.Bytes(), []neotest.Signer{acc}) + + // Reset() reuses the script buffer and we need to keep scripts. + script = io.NewBufBinWriter() + emit.Bytes(script.BinWriter, []byte("nay!")) + emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) + emit.Opcodes(script.BinWriter, opcode.THROW) + require.NoError(t, script.Err) + txBad := e.PrepareInvocation(t, script.Bytes(), []neotest.Signer{acc}) + + script = io.NewBufBinWriter() + emit.Bytes(script.BinWriter, []byte("yay! yay! yay!")) + emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify) + require.NoError(t, script.Err) + txGood2 := e.PrepareInvocation(t, script.Bytes(), []neotest.Signer{acc}) + + invBlock := e.AddNewBlock(t, txGood1, txBad, txGood2) + + require.Eventually(t, func() bool { + return len(blockCh) != 0 && len(txCh) != 0 && + len(notificationCh) != 0 && len(executionCh) != 0 + }, time.Second, 10*time.Millisecond) + + b = <-blockCh + require.Equal(t, invBlock, b) + assert.Empty(t, blockCh) + + exec := <-executionCh + require.Equal(t, b.Hash(), exec.Container) + require.Equal(t, exec.VMState, vm.HaltState) + + // 3 burn events for every tx and 1 mint for primary node + require.True(t, len(notificationCh) >= 4) + for i := 0; i < 4; i++ { + notif := <-notificationCh + require.Equal(t, nativeGASHash, notif.ScriptHash) + } + + // Follow in-block transaction order. + for _, txExpected := range invBlock.Transactions { + tx := <-txCh + require.Equal(t, txExpected, tx) + exec := <-executionCh + require.Equal(t, tx.Hash(), exec.Container) + if exec.VMState == vm.HaltState { + notif := <-notificationCh + require.Equal(t, hash.Hash160(tx.Script), notif.ScriptHash) + } + } + assert.Empty(t, txCh) + assert.Len(t, notificationCh, 1) + assert.Len(t, executionCh, 1) + + notif = <-notificationCh + require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash) + + exec = <-executionCh + require.Equal(t, b.Hash(), exec.Container) + require.Equal(t, exec.VMState, vm.HaltState) + + bc.UnsubscribeFromBlocks(blockCh) + bc.UnsubscribeFromTransactions(txCh) + bc.UnsubscribeFromNotifications(notificationCh) + bc.UnsubscribeFromExecutions(executionCh) + + // Ensure that new blocks are processed correctly after unsubscription. + e.GenerateNewBlocks(t, 2*chBufSize) +} + +func TestBlockchain_RemoveUntraceable(t *testing.T) { + neoCommitteeKey := []byte{0xfb, 0xff, 0xff, 0xff, 0x0e} + check := func(t *testing.T, bc *core.Blockchain, tHash, bHash, sHash util.Uint256, errorExpected bool) { + _, _, err := bc.GetTransaction(tHash) + if errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + } + _, err = bc.GetAppExecResults(tHash, trigger.Application) + if errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + } + _, err = bc.GetBlock(bHash) + if errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + } + _, err = bc.GetHeader(bHash) + require.NoError(t, err) + if !sHash.Equals(util.Uint256{}) { + sm := bc.GetStateModule() + _, err = sm.GetState(sHash, neoCommitteeKey) + if errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + } + t.Run("P2PStateExchangeExtensions off", func(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.MaxTraceableBlocks = 2 + c.GarbageCollectionPeriod = 2 + c.RemoveUntraceableBlocks = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + neoValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Neo)) + + tx1Hash := neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + tx1Height := bc.BlockHeight() + b1 := e.TopBlock(t) + sRoot, err := bc.GetStateModule().GetStateRoot(tx1Height) + require.NoError(t, err) + + neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + + _, h1, err := bc.GetTransaction(tx1Hash) + require.NoError(t, err) + require.Equal(t, tx1Height, h1) + + check(t, bc, tx1Hash, b1.Hash(), sRoot.Root, false) + e.GenerateNewBlocks(t, 4) + + sm := bc.GetStateModule() + require.Eventually(t, func() bool { + _, err = sm.GetState(sRoot.Root, neoCommitteeKey) + return err != nil + }, 2*bcPersistInterval, 10*time.Millisecond) + check(t, bc, tx1Hash, b1.Hash(), sRoot.Root, true) + }) + t.Run("P2PStateExchangeExtensions on", func(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.MaxTraceableBlocks = 2 + c.GarbageCollectionPeriod = 2 + c.RemoveUntraceableBlocks = true + c.P2PStateExchangeExtensions = true + c.StateSyncInterval = 2 + c.StateRootInHeader = true + }) + e := neotest.NewExecutor(t, bc, acc, acc) + neoValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Neo)) + + tx1Hash := neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + tx1Height := bc.BlockHeight() + b1 := e.TopBlock(t) + sRoot, err := bc.GetStateModule().GetStateRoot(tx1Height) + require.NoError(t, err) + + tx2Hash := neoValidatorInvoker.Invoke(t, true, "transfer", acc.ScriptHash(), util.Uint160{1, 2, 3}, 1, nil) + tx2Height := bc.BlockHeight() + b2 := e.TopBlock(t) + + _, h1, err := bc.GetTransaction(tx1Hash) + require.NoError(t, err) + require.Equal(t, tx1Height, h1) + + e.GenerateNewBlocks(t, 3) + + check(t, bc, tx1Hash, b1.Hash(), sRoot.Root, false) + check(t, bc, tx2Hash, b2.Hash(), sRoot.Root, false) + + e.AddNewBlock(t) + + check(t, bc, tx1Hash, b1.Hash(), util.Uint256{}, true) + check(t, bc, tx2Hash, b2.Hash(), util.Uint256{}, false) + _, h2, err := bc.GetTransaction(tx2Hash) + require.NoError(t, err) + require.Equal(t, tx2Height, h2) + }) +} + +func TestBlockchain_InvalidNotification(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + + cValidatorInvoker := e.ValidatorInvoker(cs.Hash) + cValidatorInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0]) + }, "invalidStack1") + cValidatorInvoker.Invoke(t, stackitem.NewInterop(nil), "invalidStack2") +} + +// Test that deletion of non-existent doesn't result in error in tx or block addition. +func TestBlockchain_MPTDeleteNoKey(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + + cValidatorInvoker := e.ValidatorInvoker(cs.Hash) + cValidatorInvoker.Invoke(t, stackitem.Null{}, "delValue", "non-existent-key") +} + +// Test that UpdateHistory is added to ProtocolConfiguration for all native contracts +// for all default configurations. If UpdateHistory is not added to config, then +// native contract is disabled. It's easy to forget about config while adding new +// native contract. +func TestConfigNativeUpdateHistory(t *testing.T) { + var prefixPath = filepath.Join("..", "..", "config") + check := func(t *testing.T, cfgFileSuffix interface{}) { + cfgPath := filepath.Join(prefixPath, fmt.Sprintf("protocol.%s.yml", cfgFileSuffix)) + cfg, err := config.LoadFile(cfgPath) + require.NoError(t, err, fmt.Errorf("failed to load %s", cfgPath)) + natives := native.NewContracts(cfg.ProtocolConfiguration) + assert.Equal(t, len(natives.Contracts), + len(cfg.ProtocolConfiguration.NativeUpdateHistories), + fmt.Errorf("protocol configuration file %s: extra or missing NativeUpdateHistory in NativeActivations section", cfgPath)) + for _, c := range natives.Contracts { + assert.NotNil(t, cfg.ProtocolConfiguration.NativeUpdateHistories[c.Metadata().Name], + fmt.Errorf("protocol configuration file %s: configuration for %s native contract is missing in NativeActivations section; "+ + "edit the test if the contract should be disabled", cfgPath, c.Metadata().Name)) + } + } + testCases := []interface{}{ + netmode.MainNet, + netmode.PrivNet, + netmode.TestNet, + netmode.UnitTestNet, + "privnet.docker.one", + "privnet.docker.two", + "privnet.docker.three", + "privnet.docker.four", + "privnet.docker.single", + "unit_testnet.single", + } + for _, tc := range testCases { + check(t, tc) + } +} + +func TestBlockchain_VerifyTx(t *testing.T) { + bc, validator, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + c.ReservedAttributes = true + }) + e := neotest.NewExecutor(t, bc, validator, committee) + + accs := make([]*wallet.Account, 5) + for i := range accs { + var err error + accs[i], err = wallet.NewAccount() + require.NoError(t, err) + } + + notaryServiceFeePerKey := bc.GetNotaryServiceFeePerKey() + + oracleAcc := accs[2] + oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()} + require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs)) + + neoHash := e.NativeHash(t, nativenames.Neo) + gasHash := e.NativeHash(t, nativenames.Gas) + policyHash := e.NativeHash(t, nativenames.Policy) + designateHash := e.NativeHash(t, nativenames.Designation) + notaryHash := e.NativeHash(t, nativenames.Notary) + oracleHash := e.NativeHash(t, nativenames.Oracle) + + neoValidatorsInvoker := e.ValidatorInvoker(neoHash) + gasValidatorsInvoker := e.ValidatorInvoker(gasHash) + policySuperInvoker := e.NewInvoker(policyHash, validator, committee) + designateSuperInvoker := e.NewInvoker(designateHash, validator, committee) + neoOwner := validator.ScriptHash() + + neoAmount := int64(1_000_000) + gasAmount := int64(1_000_000_000) + txs := make([]*transaction.Transaction, 0, 2*len(accs)+2) + for _, a := range accs { + txs = append(txs, neoValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, a.Contract.ScriptHash(), neoAmount, nil)) + txs = append(txs, gasValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, a.Contract.ScriptHash(), gasAmount, nil)) + } + txs = append(txs, neoValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, committee.ScriptHash(), neoAmount, nil)) + txs = append(txs, gasValidatorsInvoker.PrepareInvoke(t, "transfer", neoOwner, committee.ScriptHash(), gasAmount, nil)) + e.AddNewBlock(t, txs...) + for _, tx := range txs { + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) + } + policySuperInvoker.Invoke(t, true, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) + + checkErr := func(t *testing.T, expectedErr error, tx *transaction.Transaction) { + err := bc.VerifyTx(tx) + require.True(t, errors.Is(err, expectedErr), "expected: %v, got: %v", expectedErr, err) + } + + testScript := []byte{byte(opcode.PUSH1)} + newTestTx := func(t *testing.T, signer util.Uint160, script []byte) *transaction.Transaction { + tx := transaction.New(script, 1_000_000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 5 + tx.Signers = []transaction.Signer{{ + Account: signer, + Scopes: transaction.CalledByEntry, + }} + tx.NetworkFee = int64(io.GetVarSize(tx)+200 /* witness */) * bc.FeePerByte() + tx.NetworkFee += 1_000_000 // verification cost + return tx + } + + h := accs[0].PrivateKey().GetScriptHash() + t.Run("Expired", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.ValidUntilBlock = 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrTxExpired, tx) + }) + t.Run("BlockedAccount", func(t *testing.T) { + tx := newTestTx(t, accs[1].PrivateKey().GetScriptHash(), testScript) + require.NoError(t, accs[1].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrPolicy, tx) + }) + t.Run("InsufficientGas", func(t *testing.T) { + balance := bc.GetUtilityTokenBalance(h) + tx := newTestTx(t, h, testScript) + tx.SystemFee = balance.Int64() + 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInsufficientFunds, tx) + }) + t.Run("TooBigTx", func(t *testing.T) { + script := make([]byte, transaction.MaxTransactionSize) + tx := newTestTx(t, h, script) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrTxTooBig, tx) + }) + t.Run("NetworkFee", func(t *testing.T) { + t.Run("SmallNetworkFee", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.NetworkFee = 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrTxSmallNetworkFee, tx) + }) + t.Run("AlmostEnoughNetworkFee", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) + expectedSize := io.GetVarSize(tx) + calcultedScriptSize + calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = calculatedNetFee - 1 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.Equal(t, expectedSize, io.GetVarSize(tx)) + checkErr(t, core.ErrVerificationFailed, tx) + }) + t.Run("EnoughNetworkFee", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) + expectedSize := io.GetVarSize(tx) + calcultedScriptSize + calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = calculatedNetFee + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.Equal(t, expectedSize, io.GetVarSize(tx)) + require.NoError(t, bc.VerifyTx(tx)) + }) + t.Run("CalculateNetworkFee, signature script", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + expectedSize := io.GetVarSize(tx) + verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script) + expectedSize += calculatedScriptSize + expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = expectedNetFee + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + actualSize := io.GetVarSize(tx) + require.Equal(t, expectedSize, actualSize) + gasConsumed, err := bc.VerifyWitness(h, tx, &tx.Scripts[0], -1) + require.NoError(t, err) + require.Equal(t, verificationNetFee, gasConsumed) + require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) + }) + t.Run("CalculateNetworkFee, multisignature script", func(t *testing.T) { + multisigAcc := accs[4] + pKeys := keys.PublicKeys{multisigAcc.PrivateKey().PublicKey()} + require.NoError(t, multisigAcc.ConvertMultisig(1, pKeys)) + multisigHash := hash.Hash160(multisigAcc.Contract.Script) + tx := newTestTx(t, multisigHash, testScript) + verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), multisigAcc.Contract.Script) + expectedSize := io.GetVarSize(tx) + calculatedScriptSize + expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = expectedNetFee + require.NoError(t, multisigAcc.SignTx(netmode.UnitTestNet, tx)) + actualSize := io.GetVarSize(tx) + require.Equal(t, expectedSize, actualSize) + gasConsumed, err := bc.VerifyWitness(multisigHash, tx, &tx.Scripts[0], -1) + require.NoError(t, err) + require.Equal(t, verificationNetFee, gasConsumed) + require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) + }) + }) + t.Run("InvalidTxScript", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Script = append(tx.Script, 0xff) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidScript, tx) + }) + t.Run("InvalidVerificationScript", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verif := []byte{byte(opcode.JMP), 3, 0xff, byte(opcode.PUSHT)} + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: hash.Hash160(verif), + Scopes: transaction.Global, + }) + tx.NetworkFee += 1000000 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts = append(tx.Scripts, transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: verif, + }) + checkErr(t, core.ErrInvalidVerification, tx) + }) + t.Run("InvalidInvocationScript", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + verif := []byte{byte(opcode.PUSHT)} + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: hash.Hash160(verif), + Scopes: transaction.Global, + }) + tx.NetworkFee += 1000000 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts = append(tx.Scripts, transaction.Witness{ + InvocationScript: []byte{byte(opcode.JMP), 3, 0xff}, + VerificationScript: verif, + }) + checkErr(t, core.ErrInvalidInvocation, tx) + }) + t.Run("Conflict", func(t *testing.T) { + balance := bc.GetUtilityTokenBalance(h).Int64() + tx := newTestTx(t, h, testScript) + tx.NetworkFee = balance / 2 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, bc.PoolTx(tx)) + + tx2 := newTestTx(t, h, testScript) + tx2.NetworkFee = balance / 2 + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) + err := bc.PoolTx(tx2) + require.True(t, errors.Is(err, core.ErrMemPoolConflict)) + }) + t.Run("InvalidWitnessHash", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts[0].VerificationScript = []byte{byte(opcode.PUSHT)} + checkErr(t, core.ErrWitnessHashMismatch, tx) + }) + t.Run("InvalidWitnessSignature", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10] + checkErr(t, core.ErrVerificationFailed, tx) + }) + t.Run("InsufficientNetworkFeeForSecondWitness", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: accs[3].PrivateKey().GetScriptHash(), + Scopes: transaction.Global, + }) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, accs[3].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrVerificationFailed, tx) + }) + t.Run("OldTX", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + e.AddNewBlock(t, tx) + + checkErr(t, core.ErrAlreadyExists, tx) + }) + t.Run("MemPooledTX", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, bc.PoolTx(tx)) + + err := bc.PoolTx(tx) + require.True(t, errors.Is(err, core.ErrAlreadyExists)) + }) + t.Run("MemPoolOOM", func(t *testing.T) { + mp := mempool.New(1, 0, false) + tx1 := newTestTx(t, h, testScript) + tx1.NetworkFee += 10000 // Give it more priority. + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx1)) + require.NoError(t, bc.PoolTx(tx1, mp)) + + tx2 := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2)) + err := bc.PoolTx(tx2, mp) + require.True(t, errors.Is(err, core.ErrOOM)) + }) + t.Run("Attribute", func(t *testing.T) { + t.Run("InvalidHighPriority", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("ValidHighPriority", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: committee.ScriptHash(), + Scopes: transaction.None, + }} + rawScript := committee.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + require.NoError(t, bc.VerifyTx(tx)) + }) + t.Run("Oracle", func(t *testing.T) { + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + cInvoker := e.ValidatorInvoker(cs.Hash) + + const gasForResponse int64 = 10_000_000 + putOracleRequest(t, cInvoker, "https://get.1234", new(string), "handle", []byte{}, gasForResponse) + + oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs) + require.NoError(t, err) + oracleMultisigHash := hash.Hash160(oracleScript) + + respScript := native.CreateOracleResponseScript(oracleHash) + + // We need to create new transaction, + // because hashes are cached after signing. + getOracleTx := func(t *testing.T) *transaction.Transaction { + tx := transaction.New(respScript, 1000_0000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = bc.BlockHeight() + 1 + resp := &transaction.OracleResponse{ + ID: 0, + Code: transaction.Success, + Result: []byte{1, 2, 3}, + } + tx.Attributes = []transaction.Attribute{{ + Type: transaction.OracleResponseT, + Value: resp, + }} + tx.NetworkFee += 4_000_000 // multisig check + tx.SystemFee = gasForResponse - tx.NetworkFee + tx.Signers = []transaction.Signer{{ + Account: oracleMultisigHash, + Scopes: transaction.None, + }} + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), oracleScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + return tx + } + + t.Run("NoOracleNodes", func(t *testing.T) { + tx := getOracleTx(t) + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + + keys := make([]interface{}, 0, len(oraclePubs)) + for _, p := range oraclePubs { + keys = append(keys, p.Bytes()) + } + designateSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), keys) + + t.Run("Valid", func(t *testing.T) { + tx := getOracleTx(t) + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + require.NoError(t, bc.VerifyTx(tx)) + + t.Run("NativeVerify", func(t *testing.T) { + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: oracleHash, + Scopes: transaction.None, + }) + tx.Scripts = append(tx.Scripts, transaction.Witness{}) + t.Run("NonZeroVerification", func(t *testing.T) { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.ABORT) + emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE()) + emit.Int(w.BinWriter, 0) + emit.String(w.BinWriter, nativenames.Oracle) + tx.Scripts[len(tx.Scripts)-1].VerificationScript = w.Bytes() + err := bc.VerifyTx(tx) + require.True(t, errors.Is(err, core.ErrNativeContractWitness), "got: %v", err) + }) + t.Run("Good", func(t *testing.T) { + tx.Scripts[len(tx.Scripts)-1].VerificationScript = nil + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + t.Run("InvalidRequestID", func(t *testing.T) { + tx := getOracleTx(t) + tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2 + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("InvalidScope", func(t *testing.T) { + tx := getOracleTx(t) + tx.Signers[0].Scopes = transaction.Global + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("InvalidScript", func(t *testing.T) { + tx := getOracleTx(t) + tx.Script = append(tx.Script, byte(opcode.NOP)) + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("InvalidSigner", func(t *testing.T) { + tx := getOracleTx(t) + tx.Signers[0].Account = accs[0].Contract.ScriptHash() + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + t.Run("SmallFee", func(t *testing.T) { + tx := getOracleTx(t) + tx.SystemFee = 0 + require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx)) + checkErr(t, core.ErrInvalidAttribute, tx) + }) + }) + t.Run("NotValidBefore", func(t *testing.T) { + getNVBTx := func(e *neotest.Executor, height uint32) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height}}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.Validator.ScriptHash(), + Scopes: transaction.None, + }} + size := io.GetVarSize(tx) + rawScript := e.Validator.Script() + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: e.Validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := getNVBTx(eBad, bcBad.BlockHeight()) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: NotValidBefore attribute was found, but P2PSigExtensions are disabled")) + }) + t.Run("Enabled", func(t *testing.T) { + t.Run("NotYetValid", func(t *testing.T) { + tx := getNVBTx(e, bc.BlockHeight()+1) + require.True(t, errors.Is(bc.VerifyTx(tx), core.ErrInvalidAttribute)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNVBTx(e, bc.BlockHeight()) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + t.Run("Reserved", func(t *testing.T) { + getReservedTx := func(e *neotest.Executor, attrType transaction.AttrType) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: attrType, Value: &transaction.Reserved{Value: []byte{1, 2, 3}}}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.Validator.ScriptHash(), + Scopes: transaction.None, + }} + rawScript := e.Validator.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: e.Validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := getReservedTx(eBad, transaction.ReservedLowerBound+3) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: attribute of reserved type was found, but ReservedAttributes are disabled")) + }) + t.Run("Enabled", func(t *testing.T) { + tx := getReservedTx(e, transaction.ReservedLowerBound+3) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + t.Run("Conflicts", func(t *testing.T) { + getConflictsTx := func(e *neotest.Executor, hashes ...util.Uint256) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = make([]transaction.Attribute, len(hashes)) + for i, h := range hashes { + tx.Attributes[i] = transaction.Attribute{ + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{ + Hash: h, + }, + } + } + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.Validator.ScriptHash(), + Scopes: transaction.None, + }} + rawScript := e.Validator.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{{ + InvocationScript: e.Validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }} + return tx + } + t.Run("disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := getConflictsTx(eBad, util.Uint256{1, 2, 3}) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: Conflicts attribute was found, but P2PSigExtensions are disabled")) + }) + t.Run("enabled", func(t *testing.T) { + t.Run("dummy on-chain conflict", func(t *testing.T) { + tx := newTestTx(t, h, testScript) + require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) + conflicting := transaction.New([]byte{byte(opcode.RET)}, 1000_0000) + conflicting.ValidUntilBlock = bc.BlockHeight() + 1 + conflicting.Signers = []transaction.Signer{ + { + Account: validator.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + } + conflicting.Attributes = []transaction.Attribute{ + { + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{ + Hash: tx.Hash(), + }, + }, + } + conflicting.NetworkFee = 1000_0000 + require.NoError(t, validator.SignTx(netmode.UnitTestNet, conflicting)) + e.AddNewBlock(t, conflicting) + require.True(t, errors.Is(bc.VerifyTx(tx), core.ErrHasConflicts)) + }) + t.Run("attribute on-chain conflict", func(t *testing.T) { + tx := neoValidatorsInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoOwner, neoOwner, 1, nil) + txConflict := getConflictsTx(e, tx) + require.Error(t, bc.VerifyTx(txConflict)) + }) + t.Run("positive", func(t *testing.T) { + tx := getConflictsTx(e, random.Uint256()) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + t.Run("NotaryAssisted", func(t *testing.T) { + notary, err := wallet.NewAccount() + require.NoError(t, err) + designateSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.P2PNotary), []interface{}{notary.PrivateKey().PublicKey().Bytes()}) + txSetNotary := transaction.New([]byte{byte(opcode.RET)}, 0) + txSetNotary.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.Global, + }, + } + txSetNotary.Scripts = []transaction.Witness{{ + InvocationScript: e.Committee.SignHashable(uint32(netmode.UnitTestNet), txSetNotary), + VerificationScript: e.Committee.Script(), + }} + + getNotaryAssistedTx := func(e *neotest.Executor, signaturesCount uint8, serviceFee int64) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ + NKeys: signaturesCount, + }}) + tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: e.CommitteeHash, + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + rawScript := committee.Script() + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(e.Chain.GetBaseExecFee(), rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * e.Chain.FeePerByte() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: rawScript, + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + return tx + } + t.Run("Disabled", func(t *testing.T) { + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = false + c.ReservedAttributes = false + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + tx := transaction.New(testScript, 1_000_000) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 5 + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}) + tx.NetworkFee = 1_0000_0000 + eBad.SignTx(t, tx, 1_0000_0000, eBad.Committee) + err := bcBad.VerifyTx(tx) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "invalid attribute: NotaryAssisted attribute was found, but P2PSigExtensions are disabled")) + }) + t.Run("Enabled, insufficient network fee", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, 0) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Test verify", func(t *testing.T) { + t.Run("no NotaryAssisted attribute", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Attributes = []transaction.Attribute{} + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("no deposit", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary signer scope", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.CalledByEntry, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("not signed by Notary", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary node witness", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: committee.ScriptHash(), + Scopes: transaction.None, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + acc, err := keys.NewPrivateKey() + require.NoError(t, err) + tx.Scripts = []transaction.Witness{ + { + InvocationScript: committee.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: committee.Script(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("missing payer", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + } + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), tx)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNotaryAssistedTx(e, 1, (1+1)*notaryServiceFeePerKey) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) + }) + t.Run("Partially-filled transaction", func(t *testing.T) { + getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction { + tx := newTestTx(t, h, testScript) + tx.ValidUntilBlock = validUntil + tx.Attributes = []transaction.Attribute{ + { + Type: transaction.NotValidBeforeT, + Value: &transaction.NotValidBefore{Height: nvb}, + }, + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: 0}, + }, + } + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: validator.ScriptHash(), + Scopes: transaction.None, + }, + } + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), validator.Script()) + tx.NetworkFee = netFee + // multisig witness verification price + int64(size)*bc.FeePerByte() + // fee for unsigned size + int64(sizeDelta)*bc.FeePerByte() + // fee for multisig size + 66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script) + 2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation) + notaryServiceFeePerKey + // fee for Notary attribute + fee.Opcode(bc.GetBaseExecFee(), // Notary verification script + opcode.PUSHDATA1, opcode.RET, // invocation script + opcode.PUSH0, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call + nativeprices.NotaryVerificationPrice*bc.GetBaseExecFee() // Notary witness verification price + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), + VerificationScript: []byte{}, + }, + { + InvocationScript: validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: validator.Script(), + }, + } + return tx + } + + mp := mempool.New(10, 1, false) + verificationF := func(tx *transaction.Transaction, data interface{}) error { + if data.(int) > 5 { + return errors.New("bad data") + } + return nil + } + t.Run("failed pre-verification", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+1) + require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity. + }) + t.Run("GasLimitExceeded during witness verification", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+1) + tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), + VerificationScript: []byte{}, + }, + { + InvocationScript: validator.SignHashable(uint32(netmode.UnitTestNet), tx), + VerificationScript: validator.Script(), + }, + } + require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) + }) + t.Run("bad NVB: too big", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight()+bc.GetMaxNotValidBeforeDelta()+1, bc.BlockHeight()+1) + require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), core.ErrInvalidAttribute)) + }) + t.Run("bad ValidUntilBlock: too small", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+bc.GetMaxNotValidBeforeDelta()+1) + require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), core.ErrInvalidAttribute)) + }) + t.Run("good", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+1) + require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) + }) + }) +} + +func TestBlockchain_Bug1728(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + src := `package example + import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + func init() { if true { } else { } } + func _deploy(_ interface{}, isUpdate bool) { + runtime.Log("Deploy") + }` + c := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(src), &compiler.Options{Name: "TestContract"}) + managementInvoker.DeployContract(t, c, nil) +} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index e7f1cf38f..63134688a 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -1,42 +1,21 @@ package core import ( - "encoding/json" - "fmt" - "math/big" - "path/filepath" - "strings" "testing" "time" "github.com/nspcc-dev/neo-go/internal/testchain" - "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" - "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" - "github.com/nspcc-dev/neo-go/pkg/core/fee" - "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/io" "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/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" "go.uber.org/zap" "go.uber.org/zap/zaptest" ) -// multisig address which possess all NEO. -var neoOwner = testchain.MultisigScriptHash() - // newTestChain should be called before newBlock invocation to properly setup // global state. func newTestChain(t testing.TB) *Blockchain { @@ -54,24 +33,6 @@ func newTestChainWithCustomCfgAndStore(t testing.TB, st storage.Store, f func(*c return chain } -func newLevelDBForTesting(t testing.TB) storage.Store { - dbPath := t.TempDir() - dbOptions := storage.LevelDBOptions{ - DataDirectoryPath: dbPath, - } - newLevelStore, err := storage.NewLevelDBStore(dbOptions) - require.Nil(t, err, "NewLevelDBStore error") - return newLevelStore -} - -func newBoltStoreForTesting(t testing.TB) storage.Store { - d := t.TempDir() - dbPath := filepath.Join(d, "test_bolt_db") - boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: dbPath}) - require.NoError(t, err) - return boltDBStore -} - func initTestChain(t testing.TB, st storage.Store, f func(*config.Config)) *Blockchain { chain, err := initTestChainNoCheck(t, st, f) require.NoError(t, err) @@ -165,246 +126,3 @@ func (bc *Blockchain) genBlocks(n int) ([]*block.Block, error) { } return blocks, nil } - -func TestBug1728(t *testing.T) { - src := `package example - import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - func init() { if true { } else { } } - func _deploy(_ interface{}, isUpdate bool) { - runtime.Log("Deploy") - }` - nf, di, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil) - require.NoError(t, err) - m, err := di.ConvertToManifest(&compiler.Options{Name: "TestContract"}) - require.NoError(t, err) - - rawManifest, err := json.Marshal(m) - require.NoError(t, err) - rawNef, err := nf.Bytes() - require.NoError(t, err) - - bc := newTestChain(t) - - aer, err := invokeContractMethod(bc, 10000000000, - bc.contracts.Management.Hash, "deploy", rawNef, rawManifest) - require.NoError(t, err) - require.Equal(t, aer.VMState, vm.HaltState) -} - -func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { - return newNEP17TransferWithAssert(sc, from, to, amount, true, additionalArgs...) -} - -func newNEP17TransferWithAssert(sc, from, to util.Uint160, amount int64, needAssert bool, additionalArgs ...interface{}) *transaction.Transaction { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs) - if needAssert { - emit.Opcodes(w.BinWriter, opcode.ASSERT) - } - if w.Err != nil { - panic(fmt.Errorf("failed to create NEP-17 transfer transaction: %w", w.Err)) - } - - script := w.Bytes() - return transaction.New(script, 11000000) -} - -func addSigners(sender util.Uint160, txs ...*transaction.Transaction) { - for _, tx := range txs { - tx.Signers = []transaction.Signer{{ - Account: sender, - Scopes: transaction.Global, - AllowedContracts: nil, - AllowedGroups: nil, - }} - } -} - -// Signer can be either bool or *wallet.Account. -// In the first case `true` means sign by committee, `false` means sign by validators. -func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64, - hash util.Uint160, method string, signer interface{}, args ...interface{}) (*transaction.Transaction, error) { - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, hash, method, callflag.All, args...) - if w.Err != nil { - return nil, w.Err - } - script := w.Bytes() - tx := transaction.New(script, 0) - tx.ValidUntilBlock = chain.blockHeight + 1 - var err error - switch s := signer.(type) { - case bool: - if s { - addSigners(testchain.CommitteeScriptHash(), tx) - setTxSystemFee(chain, sysfee, tx) - err = testchain.SignTxCommittee(chain, tx) - } else { - addSigners(neoOwner, tx) - setTxSystemFee(chain, sysfee, tx) - err = testchain.SignTx(chain, tx) - } - case *wallet.Account: - signTxWithAccounts(chain, sysfee, tx, s) - case []*wallet.Account: - signTxWithAccounts(chain, sysfee, tx, s...) - default: - panic("invalid signer") - } - if err != nil { - return nil, err - } - return tx, nil -} - -func setTxSystemFee(bc *Blockchain, sysFee int64, tx *transaction.Transaction) { - if sysFee >= 0 { - tx.SystemFee = sysFee - return - } - - lastBlock := bc.topBlock.Load().(*block.Block) - b := &block.Block{ - Header: block.Header{ - Index: lastBlock.Index + 1, - Timestamp: lastBlock.Timestamp + 1000, - }, - Transactions: []*transaction.Transaction{tx}, - } - - ttx := *tx // prevent setting 'hash' field - ic := bc.GetTestVM(trigger.Application, &ttx, b) - defer ic.Finalize() - - ic.VM.LoadWithFlags(tx.Script, callflag.All) - _ = ic.VM.Run() - tx.SystemFee = ic.VM.GasConsumed() -} - -func signTxWithAccounts(chain *Blockchain, sysFee int64, tx *transaction.Transaction, accs ...*wallet.Account) { - scope := transaction.CalledByEntry - for _, acc := range accs { - accH, _ := address.StringToUint160(acc.Address) - tx.Signers = append(tx.Signers, transaction.Signer{ - Account: accH, - Scopes: scope, - }) - scope = transaction.Global - } - setTxSystemFee(chain, sysFee, tx) - size := io.GetVarSize(tx) - for _, acc := range accs { - if acc.Contract.Deployed { - // don't need precise calculation for tests - tx.NetworkFee += 1000_0000 - continue - } - netFee, sizeDelta := fee.Calculate(chain.GetBaseExecFee(), acc.Contract.Script) - size += sizeDelta - tx.NetworkFee += netFee - } - tx.NetworkFee += int64(size) * chain.FeePerByte() - - for _, acc := range accs { - if err := acc.SignTx(testchain.Network(), tx); err != nil { - panic(err) - } - } -} - -func persistBlock(chain *Blockchain, txs ...*transaction.Transaction) ([]*state.AppExecResult, error) { - b := chain.newBlock(txs...) - err := chain.AddBlock(b) - if err != nil { - return nil, err - } - - aers := make([]*state.AppExecResult, len(txs)) - for i, tx := range txs { - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) - if err != nil { - return nil, err - } - aers[i] = &res[0] - } - return aers, nil -} - -func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { - return invokeContractMethodGeneric(chain, sysfee, hash, method, false, args...) -} - -func invokeContractMethodGeneric(chain *Blockchain, sysfee int64, hash util.Uint160, method string, - signer interface{}, args ...interface{}) (*state.AppExecResult, error) { - tx, err := prepareContractMethodInvokeGeneric(chain, sysfee, hash, - method, signer, args...) - if err != nil { - return nil, err - } - aers, err := persistBlock(chain, tx) - if err != nil { - return nil, err - } - return aers[0], nil -} - -func transferTokenFromMultisigAccountCheckOK(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) { - transferTx := transferTokenFromMultisigAccount(t, chain, to, tokenHash, amount, additionalArgs...) - 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)) -} - -func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { - return transferTokenFromMultisigAccountWithAssert(t, chain, to, tokenHash, amount, true, additionalArgs...) -} - -func transferTokenFromMultisigAccountWithAssert(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, needAssert bool, additionalArgs ...interface{}) *transaction.Transaction { - transferTx := newNEP17TransferWithAssert(tokenHash, testchain.MultisigScriptHash(), to, amount, needAssert, additionalArgs...) - transferTx.SystemFee = 100000000 - transferTx.ValidUntilBlock = chain.BlockHeight() + 1 - addSigners(neoOwner, transferTx) - require.NoError(t, testchain.SignTx(chain, transferTx)) - b := chain.newBlock(transferTx) - require.NoError(t, chain.AddBlock(b)) - return transferTx -} - -func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { - require.Equal(t, vm.HaltState, result.VMState, result.FaultException) - require.Equal(t, 1, len(result.Stack)) - require.Equal(t, expected, result.Stack[0]) -} - -func checkTxHalt(t testing.TB, 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 checkFAULTState(t *testing.T, result *state.AppExecResult) { - require.Equal(t, vm.FaultState, result.VMState) -} - -func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) { - balance := chain.GetUtilityTokenBalance(addr) - require.Equal(t, int64(expected), balance.Int64()) -} - -type NotaryFeerStub struct { - bc blockchainer.Blockchainer -} - -func (f NotaryFeerStub) FeePerByte() int64 { return f.bc.FeePerByte() } -func (f NotaryFeerStub) GetUtilityTokenBalance(acc util.Uint160) *big.Int { - return f.bc.GetNotaryBalance(acc) -} -func (f NotaryFeerStub) BlockHeight() uint32 { return f.bc.BlockHeight() } -func (f NotaryFeerStub) P2PSigExtensionsEnabled() bool { return f.bc.P2PSigExtensionsEnabled() } -func NewNotaryFeerStub(bc blockchainer.Blockchainer) NotaryFeerStub { - return NotaryFeerStub{ - bc: bc, - } -} diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_core_test.go similarity index 84% rename from pkg/core/interop_system_test.go rename to pkg/core/interop_system_core_test.go index f46b04e2b..5224c0e4c 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_core_test.go @@ -3,20 +3,18 @@ package core import ( "errors" "fmt" - "math" "math/big" + "path/filepath" "testing" "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" "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/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" @@ -27,7 +25,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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" "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/smartcontract/nef" @@ -40,6 +37,8 @@ import ( "github.com/stretchr/testify/require" ) +var pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") + // Tests are taken from // https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs func TestRuntimeGetRandomCompatibility(t *testing.T) { @@ -69,29 +68,6 @@ func TestRuntimeGetRandomCompatibility(t *testing.T) { require.Equal(t, "217172703763162599519098299724476526911", ic.VM.Estack().Pop().BigInt().String()) } -func TestRuntimeGetRandomDifferentTransactions(t *testing.T) { - bc := newTestChain(t) - b, _ := bc.GetBlock(bc.GetHeaderHash(0)) - - tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - ic1 := bc.newInteropContext(trigger.Application, bc.dao.GetWrapped(), b, tx1) - ic1.VM = vm.New() - ic1.VM.LoadScript(tx1.Script) - - tx2 := transaction.New([]byte{byte(opcode.PUSH2)}, 0) - ic2 := bc.newInteropContext(trigger.Application, bc.dao.GetWrapped(), b, tx2) - ic2.VM = vm.New() - ic2.VM.LoadScript(tx2.Script) - - require.NoError(t, runtime.GetRandom(ic1)) - require.NoError(t, runtime.GetRandom(ic2)) - require.NotEqual(t, ic1.VM.Estack().Pop().BigInt(), ic2.VM.Estack().Pop().BigInt()) - - require.NoError(t, runtime.GetRandom(ic1)) - require.NoError(t, runtime.GetRandom(ic2)) - require.NotEqual(t, ic1.VM.Estack().Pop().BigInt(), ic2.VM.Estack().Pop().BigInt()) -} - func getSharpTestTx(sender util.Uint160) *transaction.Transaction { tx := transaction.New([]byte{byte(opcode.PUSH2)}, 0) tx.Nonce = 0 @@ -113,81 +89,6 @@ func getSharpTestGenesis(t *testing.T) *block.Block { return b } -func TestContractCreateAccount(t *testing.T) { - v, ic, _ := createVM(t) - t.Run("Good", func(t *testing.T) { - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - pub := priv.PublicKey() - v.Estack().PushVal(pub.Bytes()) - require.NoError(t, contractCreateStandardAccount(ic)) - - value := v.Estack().Pop().Bytes() - u, err := util.Uint160DecodeBytesBE(value) - require.NoError(t, err) - require.Equal(t, pub.GetScriptHash(), u) - }) - t.Run("InvalidKey", func(t *testing.T) { - v.Estack().PushVal([]byte{1, 2, 3}) - require.Error(t, contractCreateStandardAccount(ic)) - }) -} - -func TestContractCreateMultisigAccount(t *testing.T) { - v, ic, _ := createVM(t) - t.Run("Good", func(t *testing.T) { - m, n := 3, 5 - pubs := make(keys.PublicKeys, n) - arr := make([]stackitem.Item, n) - for i := range pubs { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - pubs[i] = pk.PublicKey() - arr[i] = stackitem.Make(pubs[i].Bytes()) - } - v.Estack().PushVal(stackitem.Make(arr)) - v.Estack().PushVal(m) - require.NoError(t, contractCreateMultisigAccount(ic)) - - expected, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) - require.NoError(t, err) - value := v.Estack().Pop().Bytes() - u, err := util.Uint160DecodeBytesBE(value) - require.NoError(t, err) - require.Equal(t, hash.Hash160(expected), u) - }) - t.Run("InvalidKey", func(t *testing.T) { - v.Estack().PushVal(stackitem.Make([]stackitem.Item{stackitem.Make([]byte{1, 2, 3})})) - v.Estack().PushVal(1) - require.Error(t, contractCreateMultisigAccount(ic)) - }) - t.Run("Invalid m", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - v.Estack().PushVal(stackitem.Make([]stackitem.Item{stackitem.Make(pk.PublicKey().Bytes())})) - v.Estack().PushVal(2) - require.Error(t, contractCreateMultisigAccount(ic)) - }) - t.Run("m overflows int64", func(t *testing.T) { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - v.Estack().PushVal(stackitem.Make([]stackitem.Item{stackitem.Make(pk.PublicKey().Bytes())})) - m := big.NewInt(math.MaxInt64) - m.Add(m, big.NewInt(1)) - v.Estack().PushVal(stackitem.NewBigInteger(m)) - require.Error(t, contractCreateMultisigAccount(ic)) - }) -} - -func TestRuntimeGasLeft(t *testing.T) { - v, ic, _ := createVM(t) - - v.GasLimit = 100 - v.AddGas(58) - require.NoError(t, runtime.GasLeft(ic)) - require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) -} - func TestRuntimeGetNotifications(t *testing.T) { v, ic, _ := createVM(t) @@ -1067,80 +968,3 @@ func TestRuntimeCheckWitness(t *testing.T) { }) }) } - -func TestLoadToken(t *testing.T) { - bc := newTestChain(t) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - - t.Run("good", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "callT0", neoOwner.BytesBE()) - require.NoError(t, err) - realBalance, _ := bc.GetGoverningTokenBalance(neoOwner) - checkResult(t, aer, stackitem.Make(realBalance.Int64()+1)) - }) - t.Run("invalid param count", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "callT2") - require.NoError(t, err) - checkFAULTState(t, aer) - }) - t.Run("invalid contract", func(t *testing.T) { - aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "callT1") - require.NoError(t, err) - checkFAULTState(t, aer) - }) -} - -func TestRuntimeGetNetwork(t *testing.T) { - bc := newTestChain(t) - - w := io.NewBufBinWriter() - emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetNetwork) - require.NoError(t, w.Err) - - tx := transaction.New(w.Bytes(), 10_000) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(neoOwner, tx) - require.NoError(t, testchain.SignTx(bc, tx)) - - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - checkResult(t, &aer[0], stackitem.Make(uint32(bc.config.Magic))) -} - -func TestRuntimeBurnGas(t *testing.T) { - bc := newTestChain(t) - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - 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/interop_system_neotest_test.go b/pkg/core/interop_system_neotest_test.go new file mode 100644 index 000000000..f2ac39226 --- /dev/null +++ b/pkg/core/interop_system_neotest_test.go @@ -0,0 +1,245 @@ +package core_test + +import ( + "encoding/json" + "math" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/internal/contracts" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "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/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "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/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestSystemRuntimeGetRandom_DifferentTransactions(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + w := io.NewBufBinWriter() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetRandom) + require.NoError(t, w.Err) + script := w.Bytes() + + tx1 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + tx2 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx1, tx2) + e.CheckHalt(t, tx1.Hash()) + e.CheckHalt(t, tx2.Hash()) + + res1 := e.GetTxExecResult(t, tx1.Hash()) + res2 := e.GetTxExecResult(t, tx2.Hash()) + + r1, err := res1.Stack[0].TryInteger() + require.NoError(t, err) + r2, err := res2.Stack[0].TryInteger() + require.NoError(t, err) + require.NotEqual(t, r1, r2) +} + +func TestSystemContractCreateStandardAccount(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + t.Run("Good", func(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + emit.Bytes(w.BinWriter, pub.Bytes()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + script := w.Bytes() + + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + + res := e.GetTxExecResult(t, tx.Hash()) + value := res.Stack[0].Value().([]byte) + u, err := util.Uint160DecodeBytesBE(value) + require.NoError(t, err) + require.Equal(t, pub.GetScriptHash(), u) + }) + t.Run("InvalidKey", func(t *testing.T) { + w.Reset() + emit.Bytes(w.BinWriter, []byte{1, 2, 3}) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount) + require.NoError(t, w.Err) + script := w.Bytes() + + tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "invalid prefix 1") + }) +} + +func TestSystemContractCreateMultisigAccount(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + createScript := func(t *testing.T, pubs []interface{}, m int) []byte { + w.Reset() + emit.Array(w.BinWriter, pubs...) + emit.Int(w.BinWriter, int64(m)) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + return w.Bytes() + } + t.Run("Good", func(t *testing.T) { + m, n := 3, 5 + pubs := make(keys.PublicKeys, n) + arr := make([]interface{}, n) + for i := range pubs { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + pubs[i] = pk.PublicKey() + arr[i] = pubs[i].Bytes() + } + script := createScript(t, arr, m) + + txH := e.InvokeScript(t, script, []neotest.Signer{acc}) + e.CheckHalt(t, txH) + res := e.GetTxExecResult(t, txH) + value := res.Stack[0].Value().([]byte) + u, err := util.Uint160DecodeBytesBE(value) + require.NoError(t, err) + expected, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) + require.NoError(t, err) + require.Equal(t, hash.Hash160(expected), u) + }) + t.Run("InvalidKey", func(t *testing.T) { + script := createScript(t, []interface{}{[]byte{1, 2, 3}}, 1) + e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "invalid prefix 1") + }) + t.Run("Invalid m", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + script := createScript(t, []interface{}{pk.PublicKey().Bytes()}, 2) + e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "length of the signatures (2) is higher then the number of public keys") + }) + t.Run("m overflows int32", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + m := big.NewInt(math.MaxInt32) + m.Add(m, big.NewInt(1)) + w.Reset() + emit.Array(w.BinWriter, pk.Bytes()) + emit.BigInt(w.BinWriter, m) + emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount) + require.NoError(t, w.Err) + e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32") + }) +} + +func TestSystemRuntimeGasLeft(t *testing.T) { + const runtimeGasLeftPrice = 1 << 4 + + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + gasLimit := 1100 + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft) + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft) + require.NoError(t, w.Err) + tx := transaction.New(w.Bytes(), int64(gasLimit)) + tx.Nonce = neotest.Nonce() + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + e.SignTx(t, tx, int64(gasLimit), acc) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + res := e.GetTxExecResult(t, tx.Hash()) + l1 := res.Stack[0].Value().(*big.Int) + l2 := res.Stack[1].Value().(*big.Int) + + require.Equal(t, int64(gasLimit-runtimeGasLeftPrice*interop.DefaultBaseExecFee), l1.Int64()) + require.Equal(t, int64(gasLimit-2*runtimeGasLeftPrice*interop.DefaultBaseExecFee), l2.Int64()) +} + +func TestLoadToken(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) + + t.Run("good", func(t *testing.T) { + realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash()) + cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash()) + }) + t.Run("invalid param count", func(t *testing.T) { + cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash()) + }) + t.Run("invalid contract", func(t *testing.T) { + cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1") + }) +} + +func TestSystemRuntimeGetNetwork(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + w := io.NewBufBinWriter() + + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetNetwork) + require.NoError(t, w.Err) + e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(bc.GetConfig().Magic)))) +} + +func TestSystemRuntimeBurnGas(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash()) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) + + t.Run("good", func(t *testing.T) { + h := cInvoker.Invoke(t, stackitem.Null{}, "burnGas", int64(1)) + res := e.GetTxExecResult(t, h) + + t.Run("gas limit exceeded", func(t *testing.T) { + tx := e.NewUnsignedTx(t, cs.Hash, "burnGas", int64(2)) + e.SignTx(t, tx, res.GasConsumed, acc) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "GAS limit exceeded") + }) + }) + t.Run("too big integer", func(t *testing.T) { + gas := big.NewInt(math.MaxInt64) + gas.Add(gas, big.NewInt(1)) + + cInvoker.InvokeFail(t, "invalid GAS value", "burnGas", gas) + }) + t.Run("zero GAS", func(t *testing.T) { + cInvoker.InvokeFail(t, "GAS must be positive", "burnGas", int64(0)) + }) +} diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 6d7241f19..2be2c46b4 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -1,324 +1,164 @@ -package core +package core_test import ( - "errors" - "math/big" + "encoding/json" + "fmt" + "strings" "testing" "github.com/nspcc-dev/neo-go/internal/contracts" - "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/fee" - "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "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/core/storage" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "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/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/require" ) -type testNative struct { - meta interop.ContractMD - blocks chan uint32 -} - -func (tn *testNative) Initialize(_ *interop.Context) error { - return nil -} - -func (tn *testNative) Metadata() *interop.ContractMD { - return &tn.meta -} - -func (tn *testNative) OnPersist(ic *interop.Context) error { - select { - case tn.blocks <- ic.Block.Index: - return nil - default: - return errors.New("can't send index") - } -} - -func (tn *testNative) PostPersist(ic *interop.Context) error { - return nil -} - -var _ interop.Contract = (*testNative)(nil) - -// registerNative registers native contract in the blockchain. -func (bc *Blockchain) registerNative(c interop.Contract) { - bc.contracts.Contracts = append(bc.contracts.Contracts, c) - bc.config.NativeUpdateHistories[c.Metadata().Name] = c.Metadata().UpdateHistory -} - -const ( - testSumCPUFee = 1 << 15 // same as contract.Call - testSumStorageFee = 200 -) - -func newTestNative() *testNative { - cMD := interop.NewContractMD("Test.Native.Sum", 0) - cMD.UpdateHistory = []uint32{0} - tn := &testNative{ - meta: *cMD, - blocks: make(chan uint32, 1), - } - defer tn.meta.UpdateHash() - - desc := &manifest.Method{ - Name: "sum", - Parameters: []manifest.Parameter{ - manifest.NewParameter("addend1", smartcontract.IntegerType), - manifest.NewParameter("addend2", smartcontract.IntegerType), - }, - ReturnType: smartcontract.IntegerType, - Safe: true, - } - md := &interop.MethodAndPrice{ - Func: tn.sum, - CPUFee: testSumCPUFee, - StorageFee: testSumStorageFee, - RequiredFlags: callflag.NoneFlag, - } - tn.meta.AddMethod(md, desc) - - desc = &manifest.Method{ - Name: "callOtherContractNoReturn", - Parameters: []manifest.Parameter{ - manifest.NewParameter("contractHash", smartcontract.Hash160Type), - manifest.NewParameter("method", smartcontract.StringType), - manifest.NewParameter("arg", smartcontract.ArrayType), - }, - ReturnType: smartcontract.VoidType, - Safe: true, - } - md = &interop.MethodAndPrice{ - Func: tn.callOtherContractNoReturn, - CPUFee: testSumCPUFee, - RequiredFlags: callflag.NoneFlag} - tn.meta.AddMethod(md, desc) - - desc = &manifest.Method{ - Name: "callOtherContractWithReturn", - Parameters: []manifest.Parameter{ - manifest.NewParameter("contractHash", smartcontract.Hash160Type), - manifest.NewParameter("method", smartcontract.StringType), - manifest.NewParameter("arg", smartcontract.ArrayType), - }, - ReturnType: smartcontract.IntegerType, - } - md = &interop.MethodAndPrice{ - Func: tn.callOtherContractWithReturn, - CPUFee: testSumCPUFee, - RequiredFlags: callflag.NoneFlag} - tn.meta.AddMethod(md, desc) - - return tn -} - -func (tn *testNative) sum(_ *interop.Context, args []stackitem.Item) stackitem.Item { - s1, err := args[0].TryInteger() - if err != nil { - panic(err) - } - s2, err := args[1].TryInteger() - if err != nil { - panic(err) - } - return stackitem.NewBigInteger(s1.Add(s1, s2)) -} - -func toUint160(item stackitem.Item) util.Uint160 { - bs, err := item.TryBytes() - if err != nil { - panic(err) - } - u, err := util.Uint160DecodeBytesBE(bs) - if err != nil { - panic(err) - } - return u -} - -func (tn *testNative) call(ic *interop.Context, args []stackitem.Item, hasReturn bool) { - cs, err := ic.GetContract(toUint160(args[0])) - if err != nil { - panic(err) - } - bs, err := args[1].TryBytes() - if err != nil { - panic(err) - } - err = contract.CallFromNative(ic, tn.meta.Hash, cs, string(bs), args[2].Value().([]stackitem.Item), hasReturn) - if err != nil { - panic(err) - } -} - -func (tn *testNative) callOtherContractNoReturn(ic *interop.Context, args []stackitem.Item) stackitem.Item { - tn.call(ic, args, false) - return stackitem.Null{} -} - -func (tn *testNative) callOtherContractWithReturn(ic *interop.Context, args []stackitem.Item) stackitem.Item { - tn.call(ic, args, true) - bi := ic.VM.Estack().Pop().BigInt() - return stackitem.Make(bi.Add(bi, big.NewInt(1))) -} - func TestNativeContract_Invoke(t *testing.T) { - chain := newTestChain(t) + const ( + transferCPUFee = 1 << 17 + transferStorageFee = 50 + systemContractCallPrice = 1 << 15 + ) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + gasHash := e.NativeHash(t, nativenames.Gas) - tn := newTestNative() - chain.registerNative(tn) + baseExecFee := bc.GetBaseExecFee() + price := fee.Opcode(baseExecFee, opcode.SYSCALL, // System.Contract.Call + opcode.PUSHDATA1, // contract hash (20 byte) + opcode.PUSHDATA1, // method + opcode.PUSH15, // call flags + // `transfer` args: + opcode.PUSHDATA1, // from + opcode.PUSHDATA1, // to + opcode.PUSH1, // amount + opcode.PUSHNULL, // data + // end args + opcode.PUSH4, // amount of args + opcode.PACK, // pack args + ) + price += systemContractCallPrice*baseExecFee + // System.Contract.Call price + transferCPUFee*baseExecFee + // `transfer` itself + transferStorageFee*bc.GetStoragePrice() // `transfer` storage price - err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ - ContractBase: state.ContractBase{ - ID: 1, - NEF: tn.meta.NEF, - Hash: tn.meta.Hash, - Manifest: tn.meta.Manifest, - }, - }) - require.NoError(t, err) + tx := e.NewUnsignedTx(t, gasHash, "transfer", validator.ScriptHash(), validator.ScriptHash(), 1, nil) + e.SignTx(t, tx, -1, validator) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) - // System.Contract.Call + "sum" itself + opcodes for pushing arguments. - price := int64(testSumCPUFee * chain.GetBaseExecFee() * 2) - price += testSumStorageFee * chain.GetStoragePrice() - price += 3 * fee.Opcode(chain.GetBaseExecFee(), opcode.PUSHINT8) - price += 2 * fee.Opcode(chain.GetBaseExecFee(), opcode.SYSCALL, opcode.PUSHDATA1, opcode.PUSHINT8) - price += fee.Opcode(chain.GetBaseExecFee(), opcode.PACK) - res, err := invokeContractMethod(chain, price, tn.Metadata().Hash, "sum", int64(14), int64(28)) - require.NoError(t, err) - checkResult(t, res, stackitem.Make(42)) - _, err = chain.persist(false) - require.NoError(t, err) - - select { - case index := <-tn.blocks: - require.Equal(t, chain.blockHeight, index) - default: - require.Fail(t, "onPersist wasn't called") - } - - // Enough for Call and other opcodes, but not enough for "sum" call. - res, err = invokeContractMethod(chain, price-1, tn.Metadata().Hash, "sum", int64(14), int64(28)) - require.NoError(t, err) - checkFAULTState(t, res) + // Enough for Call and other opcodes, but not enough for "transfer" call. + tx = e.NewUnsignedTx(t, gasHash, "transfer", validator.ScriptHash(), validator.ScriptHash(), 1, nil) + e.SignTx(t, tx, price-1, validator) + e.AddNewBlock(t, tx) + e.CheckFault(t, tx.Hash(), "gas limit exceeded") } func TestNativeContract_InvokeInternal(t *testing.T) { - chain := newTestChain(t) - - tn := newTestNative() - chain.registerNative(tn) - - err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ - ContractBase: state.ContractBase{ - ID: 1, - NEF: tn.meta.NEF, - Manifest: tn.meta.Manifest, - }, - }) - require.NoError(t, err) - - d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) - ic := chain.newInteropContext(trigger.Application, d, nil, nil) - - sumOffset := 0 - for _, md := range tn.Metadata().Methods { - if md.MD.Name == "sum" { - sumOffset = md.MD.Offset - break - } - } + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + clState := bc.GetContractState(e.NativeHash(t, nativenames.CryptoLib)) + require.NotNil(t, clState) + md := clState.Manifest.ABI.GetMethod("ripemd160", 1) + require.NotNil(t, md) t.Run("fail, bad current script hash", func(t *testing.T) { + ic := bc.GetTestVM(trigger.Application, nil, nil) v := ic.SpawnVM() - v.LoadScriptWithHash(tn.Metadata().NEF.Script, util.Uint160{1, 2, 3}, callflag.All) - v.Estack().PushVal(14) - v.Estack().PushVal(28) - v.Context().Jump(sumOffset) + fakeH := util.Uint160{1, 2, 3} + v.LoadScriptWithHash(clState.NEF.Script, fakeH, callflag.All) + input := []byte{1, 2, 3, 4} + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) - // it's prohibited to call natives directly - require.Error(t, v.Run()) + // Bad current script hash + err := v.Run() + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s (version 0) not found", fakeH.StringLE())), err.Error()) }) t.Run("fail, bad NativeUpdateHistory height", func(t *testing.T) { - tn.Metadata().UpdateHistory = []uint32{chain.blockHeight + 1} + bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.NativeUpdateHistories = map[string][]uint32{ + nativenames.Policy: {0}, + nativenames.Neo: {0}, + nativenames.Gas: {0}, + nativenames.Designation: {0}, + nativenames.StdLib: {0}, + nativenames.Management: {0}, + nativenames.Oracle: {0}, + nativenames.Ledger: {0}, + nativenames.CryptoLib: {1}, + } + }) + eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) + + ic := bcBad.GetTestVM(trigger.Application, nil, nil) v := ic.SpawnVM() - v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) - v.Estack().PushVal(14) - v.Estack().PushVal(28) - v.Context().Jump(sumOffset) + v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history + input := []byte{1, 2, 3, 4} + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) - // it's prohibited to call natives before NativeUpdateHistory[0] height - require.Error(t, v.Run()) + // It's prohibited to call natives before NativeUpdateHistory[0] height. + err := v.Run() + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "native contract CryptoLib is active after height = 1")) - // set the value back to 0 - tn.Metadata().UpdateHistory = []uint32{0} + // Add new block => CryptoLib should be active now. + eBad.AddNewBlock(t) + ic = bcBad.GetTestVM(trigger.Application, nil, nil) + v = ic.SpawnVM() + v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) + + require.NoError(t, v.Run()) + value := v.Estack().Pop().Bytes() + require.Equal(t, hash.RipeMD160(input).BytesBE(), value) }) t.Run("success", func(t *testing.T) { + ic := bc.GetTestVM(trigger.Application, nil, nil) v := ic.SpawnVM() - v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) - v.Estack().PushVal(14) - v.Estack().PushVal(28) - v.Context().Jump(sumOffset) + v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) + input := []byte{1, 2, 3, 4} + v.Estack().PushVal(input) + v.Context().Jump(md.Offset) + require.NoError(t, v.Run()) - value := v.Estack().Pop().BigInt() - require.Equal(t, int64(42), value.Int64()) + value := v.Estack().Pop().Bytes() + require.Equal(t, hash.RipeMD160(input).BytesBE(), value) }) } func TestNativeContract_InvokeOtherContract(t *testing.T) { - chain := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + gasInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) - tn := newTestNative() - chain.registerNative(tn) - - err := chain.contracts.Management.PutContractState(chain.dao, &state.Contract{ - ContractBase: state.ContractBase{ - ID: 1, - Hash: tn.meta.Hash, - NEF: tn.meta.NEF, - Manifest: tn.meta.Manifest, - }, - }) + cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, validator.ScriptHash()) + cs.Hash = state.CreateContractHash(validator.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) // set proper hash + manifB, err := json.Marshal(cs.Manifest) require.NoError(t, err) + nefB, err := cs.NEF.Bytes() + require.NoError(t, err) + si, err := cs.ToStackItem() + require.NoError(t, err) + managementInvoker.Invoke(t, si, "deploy", nefB, manifB) - var drainTN = func(t *testing.T) { - select { - case <-tn.blocks: - default: - require.Fail(t, "testNative didn't send us block") - } - } - - cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test - require.NoError(t, chain.contracts.Management.PutContractState(chain.dao, cs)) - - baseFee := chain.GetBaseExecFee() t.Run("non-native, no return", func(t *testing.T) { - res, err := invokeContractMethod(chain, testSumCPUFee*baseFee*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.Hash, "justReturn", []interface{}{}) - require.NoError(t, err) - drainTN(t) - require.Equal(t, vm.HaltState, res.VMState, res.FaultException) - checkResult(t, res, stackitem.Null{}) // simple call is done with EnsureNotEmpty - }) - t.Run("non-native, with return", func(t *testing.T) { - res, err := invokeContractMethod(chain, testSumCPUFee*baseFee*4+10000, tn.Metadata().Hash, - "callOtherContractWithReturn", cs.Hash, "ret7", []interface{}{}) - require.NoError(t, err) - drainTN(t) - checkResult(t, res, stackitem.Make(8)) + // `onNEP17Payment` will be invoked on test contract from GAS contract. + gasInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), cs.Hash, 1, nil) }) } diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index d8a5cb44c..3dd68f05f 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -10,69 +10,11 @@ import ( "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/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/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/stretchr/testify/require" ) -func (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r noderoles.Role, nodes keys.PublicKeys) { - w := io.NewBufBinWriter() - for _, pub := range nodes { - emit.Bytes(w.BinWriter, pub.Bytes()) - } - emit.Int(w.BinWriter, int64(len(nodes))) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.Int(w.BinWriter, int64(r)) - emit.Int(w.BinWriter, 2) - emit.Opcodes(w.BinWriter, opcode.PACK) - emit.AppCallNoArgs(w.BinWriter, bc.contracts.Designate.Hash, "designateAsRole", callflag.All) - require.NoError(t, w.Err) - tx := transaction.New(w.Bytes(), 0) - tx.NetworkFee = 10_000_000 - tx.SystemFee = 10_000_000 - tx.ValidUntilBlock = 100 - tx.Signers = []transaction.Signer{ - { - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }, - { - Account: testchain.CommitteeScriptHash(), - Scopes: transaction.CalledByEntry, - }, - } - require.NoError(t, testchain.SignTx(bc, tx)) - tx.Scripts = append(tx.Scripts, transaction.Witness{ - InvocationScript: testchain.SignCommittee(tx), - VerificationScript: testchain.CommitteeVerificationScript(), - }) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - if ok { - require.Equal(t, vm.HaltState, aer[0].VMState) - require.Equal(t, 1, len(aer[0].Events)) - - ev := aer[0].Events[0] - require.Equal(t, bc.contracts.Designate.Hash, ev.ScriptHash) - require.Equal(t, native.DesignationEventName, ev.Name) - require.Equal(t, []stackitem.Item{ - stackitem.Make(int64(r)), - stackitem.Make(bc.BlockHeight()), - }, ev.Item.Value().([]stackitem.Item)) - } else { - require.Equal(t, vm.FaultState, aer[0].VMState) - require.Equal(t, 0, len(aer[0].Events)) - } -} - func TestDesignate_DesignateAsRole(t *testing.T) { bc := newTestChain(t) diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index b54a6f3f5..9c0c7b43c 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -1,20 +1,17 @@ -package core +package core_test import ( "fmt" "math/big" + "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/storage" "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/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/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -40,96 +37,88 @@ func BenchmarkNEO_GetGASPerVote(t *testing.B) { } } -func benchmarkGasPerVote(t *testing.B, ps storage.Store, nRewardRecords int, rewardDistance int) { - bc := newTestChainWithCustomCfgAndStore(t, ps, nil) - - 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) - - advanceChain := func(t *testing.B, count int) { - for i := 0; i < count; i++ { - require.NoError(t, bc.AddBlock(bc.newBlock())) - ic.Block.Index++ - } +func newLevelDBForTesting(t testing.TB) storage.Store { + dbPath := t.TempDir() + dbOptions := storage.LevelDBOptions{ + DataDirectoryPath: dbPath, } + newLevelStore, err := storage.NewLevelDBStore(dbOptions) + require.Nil(t, err, "NewLevelDBStore error") + return newLevelStore +} + +func newBoltStoreForTesting(t testing.TB) storage.Store { + d := t.TempDir() + dbPath := filepath.Join(d, "test_bolt_db") + boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: dbPath}) + require.NoError(t, err) + return boltDBStore +} + +func benchmarkGasPerVote(t *testing.B, ps storage.Store, nRewardRecords int, rewardDistance int) { + bc, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, ps, true) + cfg := bc.GetConfig() + + e := neotest.NewExecutor(t, bc, validators, committee) + neoHash := e.NativeHash(t, nativenames.Neo) + gasHash := e.NativeHash(t, nativenames.Gas) + neoSuperInvoker := e.NewInvoker(neoHash, validators, committee) + neoValidatorsInvoker := e.ValidatorInvoker(neoHash) + gasValidatorsInvoker := e.ValidatorInvoker(gasHash) // Vote for new committee. - sz := testchain.CommitteeSize() - accs := make([]*wallet.Account, sz) + sz := len(cfg.StandbyCommittee) + voters := make([]*wallet.Account, sz) candidates := make(keys.PublicKeys, sz) - txs := make([]*transaction.Transaction, 0, len(accs)) + txs := make([]*transaction.Transaction, 0, len(voters)*3) for i := 0; i < sz; i++ { priv, err := keys.NewPrivateKey() require.NoError(t, err) candidates[i] = priv.PublicKey() - accs[i], err = wallet.NewAccount() + voters[i], err = wallet.NewAccount() require.NoError(t, err) - require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[i])) + registerTx := neoSuperInvoker.PrepareInvoke(t, "registerCandidate", candidates[i].Bytes()) + txs = append(txs, registerTx) - 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) + to := voters[i].Contract.ScriptHash() + transferNeoTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, big.NewInt(int64(sz-i)*1000000).Int64(), nil) + txs = append(txs, transferNeoTx) + + transferGasTx := gasValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), to, int64(1_000_000_000), nil) + txs = append(txs, transferGasTx) } - require.NoError(t, bc.AddBlock(bc.newBlock(txs...))) + e.AddNewBlock(t, txs...) for _, tx := range txs { - checkTxHalt(t, bc, tx.Hash()) + e.CheckHalt(t, tx.Hash()) } + voteTxs := make([]*transaction.Transaction, 0, sz) for i := 0; i < sz; i++ { - priv := accs[i].PrivateKey() + priv := voters[i].PrivateKey() h := priv.GetScriptHash() - setSigner(tx, h) - ic.VM.Load(priv.PublicKey().GetVerificationScript()) - require.NoError(t, neo.VoteInternal(ic, h, candidates[i])) + voteTx := e.NewTx(t, []neotest.Signer{neotest.NewSingleSigner(voters[i])}, neoHash, "vote", h, candidates[i].Bytes()) + voteTxs = append(voteTxs, voteTx) + } + e.AddNewBlock(t, voteTxs...) + for _, tx := range voteTxs { + e.CheckHalt(t, tx.Hash()) } - _, err := ic.DAO.Persist() - require.NoError(t, err) // Collect set of nRewardRecords reward records for each voter. - advanceChain(t, nRewardRecords*testchain.CommitteeSize()) + e.GenerateNewBlocks(t, len(cfg.StandbyCommittee)) // Transfer some more NEO to first voter to update his balance height. - to := accs[0].Contract.ScriptHash() - w := io.NewBufBinWriter() - emit.AppCall(w.BinWriter, bc.contracts.NEO.Hash, "transfer", callflag.All, - neoOwner.BytesBE(), to.BytesBE(), int64(1), 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)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aer)) - require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) + to := voters[0].Contract.ScriptHash() + neoValidatorsInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), to, int64(1), nil) // Advance chain one more time to avoid same start/end rewarding bounds. - advanceChain(t, rewardDistance) + e.GenerateNewBlocks(t, rewardDistance) end := bc.BlockHeight() t.ResetTimer() t.ReportAllocs() t.StartTimer() for i := 0; i < t.N; i++ { - _, err := neo.CalculateBonus(ic.DAO, to, end) + _, err := bc.CalculateClaimable(to, end) require.NoError(t, err) } t.StopTimer() diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index a980e91fd..211f7476c 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -1,49 +1,67 @@ -package core +package core_test import ( + "fmt" "testing" - "github.com/nspcc-dev/neo-go/internal/random" - "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" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/stretchr/testify/require" ) -func TestFeePerByte(t *testing.T) { - chain := newTestChain(t) +func TestPolicy_FeePerByte(t *testing.T) { + bc, _, _ := chain.NewMulti(t) t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) + n := bc.FeePerByte() require.Equal(t, 1000, int(n)) }) } -func TestExecFeeFactor(t *testing.T) { - chain := newTestChain(t) +func TestPolicy_ExecFeeFactor(t *testing.T) { + bc, _, _ := chain.NewMulti(t) t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Policy.GetExecFeeFactorInternal(chain.dao) + n := bc.GetBaseExecFee() require.EqualValues(t, interop.DefaultBaseExecFee, n) }) } -func TestStoragePrice(t *testing.T) { - chain := newTestChain(t) +func TestPolicy_StoragePrice(t *testing.T) { + bc, validators, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validators, committee) t.Run("get, internal method", func(t *testing.T) { - n := chain.contracts.Policy.GetStoragePriceInternal(chain.dao) + e.AddNewBlock(t) // avoid default value got from Blockchain. + + n := bc.GetStoragePrice() require.Equal(t, int64(native.DefaultStoragePrice), n) }) } -func TestBlockedAccounts(t *testing.T) { - chain := newTestChain(t) - transferTokenFromMultisigAccount(t, chain, testchain.CommitteeScriptHash(), - chain.contracts.GAS.Hash, 100_00000000) +func TestPolicy_BlockedAccounts(t *testing.T) { + bc, validators, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validators, committee) + policyHash := e.NativeHash(t, nativenames.Policy) + policySuperInvoker := e.NewInvoker(policyHash, validators, committee) + unlucky := e.NewAccount(t, 5_0000_0000) + policyUnluckyInvoker := e.NewInvoker(policyHash, unlucky) + + // Block unlucky account. + policySuperInvoker.Invoke(t, true, "blockAccount", unlucky.ScriptHash()) + + // Transaction from blocked account shouldn't be accepted. t.Run("isBlocked, internal method", func(t *testing.T) { - isBlocked := chain.contracts.Policy.IsBlockedInternal(chain.dao, random.Uint160()) - require.Equal(t, false, isBlocked) + tx := policyUnluckyInvoker.PrepareInvoke(t, "getStoragePrice") + b := e.NewUnsignedBlock(t, tx) + e.SignBlock(b) + expectedErr := fmt.Sprintf("transaction %s failed to verify: not allowed by policy: account %s is blocked", tx.Hash().StringLE(), unlucky.ScriptHash().StringLE()) + err := e.Chain.AddBlock(b) + require.Error(t, err) + require.Equal(t, expectedErr, err.Error()) }) } diff --git a/pkg/core/notary_test.go b/pkg/core/notary_test.go index 236bae50b..46f2e38db 100644 --- a/pkg/core/notary_test.go +++ b/pkg/core/notary_test.go @@ -1,8 +1,9 @@ -package core +package core_test import ( "errors" "fmt" + "math/big" "math/rand" "path" "path/filepath" @@ -10,29 +11,32 @@ import ( "testing" "time" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/mempool" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "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/hash" "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/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/services/notary" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "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/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) -var notaryModulePath = filepath.Join("..", "services", "notary") - -func getTestNotary(t *testing.T, bc *Blockchain, walletPath, pass string, onTx func(tx *transaction.Transaction) error) (*wallet.Account, *notary.Notary, *mempool.Pool) { +func getTestNotary(t *testing.T, bc *core.Blockchain, walletPath, pass string, onTx func(tx *transaction.Transaction) error) (*wallet.Account, *notary.Notary, *mempool.Pool) { mainCfg := config.P2PNotary{ Enabled: true, UnlockWallet: config.Wallet{ @@ -46,7 +50,7 @@ func getTestNotary(t *testing.T, bc *Blockchain, walletPath, pass string, onTx f Log: zaptest.NewLogger(t), } mp := mempool.New(10, 1, true) - ntr, err := notary.NewNotary(cfg, testchain.Network(), mp, onTx) + ntr, err := notary.NewNotary(cfg, netmode.UnitTestNet, mp, onTx) require.NoError(t, err) w, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, walletPath)) @@ -68,7 +72,14 @@ func dupNotaryRequest(t *testing.T, p *payload.P2PNotaryRequest) *payload.P2PNot } func TestNotary(t *testing.T) { - bc := newTestChain(t) + bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validators, committee) + notaryHash := e.NativeHash(t, nativenames.Notary) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validators, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) + var ( nonce uint32 nvbDiffFallback uint32 = 20 @@ -145,8 +156,9 @@ func TestNotary(t *testing.T) { mp1.StopSubscriptions() }) - notaryNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()} - bc.setNodesByRole(t, true, noderoles.P2PNotary, notaryNodes) + notaryNodes := []interface{}{acc1.PrivateKey().PublicKey().Bytes(), acc2.PrivateKey().PublicKey().Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.P2PNotary), notaryNodes) type requester struct { accounts []*wallet.Account @@ -193,7 +205,7 @@ func TestNotary(t *testing.T) { VerificationScript: []byte{}, }, } - err = requester.SignTx(testchain.Network(), fallback) + err = requester.SignTx(netmode.UnitTestNet, fallback) require.NoError(t, err) return fallback } @@ -251,7 +263,7 @@ func TestNotary(t *testing.T) { for j := range main.Scripts { main.Scripts[j].VerificationScript = verificationScripts[j] if i == j { - main.Scripts[j].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().SignHashable(uint32(testchain.Network()), main)...) + main.Scripts[j].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), main)...) } } main.Scripts = append(main.Scripts, transaction.Witness{}) // empty Notary witness @@ -296,12 +308,11 @@ func TestNotary(t *testing.T) { require.Equal(t, io.GetVarSize(completedTx), completedTx.Size()) for i := 0; i < len(completedTx.Scripts)-1; i++ { - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) - _, err := bc.verifyHashAgainstScript(completedTx.Signers[i].Account, &completedTx.Scripts[i], interopCtx, -1) + _, err := bc.VerifyWitness(completedTx.Signers[i].Account, completedTx, &completedTx.Scripts[i], -1) require.NoError(t, err) } require.Equal(t, transaction.Witness{ - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(testchain.Network()), requests[0].MainTransaction)...), + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), requests[0].MainTransaction)...), VerificationScript: []byte{}, }, completedTx.Scripts[len(completedTx.Scripts)-1]) } else { @@ -316,15 +327,14 @@ func TestNotary(t *testing.T) { require.Equal(t, 2, len(completedTx.Signers)) require.Equal(t, 2, len(completedTx.Scripts)) require.Equal(t, transaction.Witness{ - InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(testchain.Network()), req.FallbackTransaction)...), + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), req.FallbackTransaction)...), VerificationScript: []byte{}, }, completedTx.Scripts[0]) // check that tx size was updated require.Equal(t, io.GetVarSize(completedTx), completedTx.Size()) - interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) - _, err := bc.verifyHashAgainstScript(completedTx.Signers[1].Account, &completedTx.Scripts[1], interopCtx, -1) + _, err := bc.VerifyWitness(completedTx.Signers[1].Account, completedTx, &completedTx.Scripts[1], -1) require.NoError(t, err) } else { completedTx := getCompletedTx(t, false, req.FallbackTransaction.Hash()) @@ -483,7 +493,8 @@ func TestNotary(t *testing.T) { checkFallbackTxs(t, r, false) ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + + e.AddNewBlock(t) checkMainTx(t, requesters, r, 1, false) checkFallbackTxs(t, r, false) // set account back for the next tests @@ -494,11 +505,11 @@ func TestNotary(t *testing.T) { requests, requesters := checkCompleteStandardRequest(t, 3, false) // check PostPersist with finalisation error setFinalizeWithError(true) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) // check PostPersist without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), true) // PostPersist: complete main transaction, multisignature account @@ -507,12 +518,12 @@ func TestNotary(t *testing.T) { checkFallbackTxs(t, requests, false) // check PostPersist with finalisation error setFinalizeWithError(true) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // check PostPersist without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), true) checkFallbackTxs(t, requests, false) @@ -521,15 +532,15 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 3, false) checkFallbackTxs(t, requests, false) // make fallbacks valid - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // check PostPersist for valid fallbacks with finalisation error - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // check PostPersist for valid fallbacks without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, true) @@ -540,15 +551,15 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteMultisigRequest(t, nSigs, nKeys, false) checkFallbackTxs(t, requests, false) // make fallbacks valid - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // check PostPersist for valid fallbacks with finalisation error - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // check PostPersist for valid fallbacks without finalisation error setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests[:nSigs], true) // the rest of fallbacks should also be applied even if the main tx was already constructed by the moment they were sent @@ -559,14 +570,14 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 5, false) checkFallbackTxs(t, requests, false) // make fallbacks valid - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // some of fallbacks should fail finalisation unluckies = []*payload.P2PNotaryRequest{requests[0], requests[4]} lucky := requests[1:4] setChoosy(true) // check PostPersist for lucky fallbacks - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, lucky, true) checkFallbackTxs(t, unluckies, false) @@ -574,7 +585,7 @@ func TestNotary(t *testing.T) { setChoosy(false) setFinalizeWithError(false) // check PostPersist for unlucky fallbacks - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, lucky, true) checkFallbackTxs(t, unluckies, true) @@ -585,19 +596,19 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 5, false, 1, 2, 3, 4, 5) checkFallbackTxs(t, requests, false) // generate blocks to reach the most earlier fallback's NVB - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) // check PostPersist for valid fallbacks without finalisation error // Add block before allowing tx to finalize to exclude race condition when // main transaction is finalized between `finalizeWithError` restore and adding new block. - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) mtx.RLock() start := len(completedTxes) mtx.RUnlock() setFinalizeWithError(false) for i := range requests { if i != 0 { - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) } require.Eventually(t, func() bool { mtx.RLock() @@ -615,13 +626,13 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 4, false) checkFallbackTxs(t, requests, false) // make fallbacks valid and remove one fallback - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) ntr1.OnRequestRemoval(requests[3]) // non of the fallbacks should be completed setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // set account back for the next tests @@ -633,13 +644,13 @@ func TestNotary(t *testing.T) { requests, requesters = checkCompleteStandardRequest(t, 4, false) checkFallbackTxs(t, requests, false) // make fallbacks valid and remove one fallback - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) unlucky := requests[3] ntr1.OnRequestRemoval(unlucky) // rest of the fallbacks should be completed setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests[:3], true) require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()]) @@ -648,20 +659,20 @@ func TestNotary(t *testing.T) { setFinalizeWithError(true) requests, requesters = checkCompleteStandardRequest(t, 4, false) // remove all fallbacks - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) for i := range requests { ntr1.OnRequestRemoval(requests[i]) } // then the whole request should be removed, i.e. there are no completed transactions setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // OnRequestRemoval: signature request, remove unexisting fallback ntr1.OnRequestRemoval(requests[0]) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) @@ -673,13 +684,13 @@ func TestNotary(t *testing.T) { checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // make fallbacks valid and remove the last fallback - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) unlucky = requests[nSigs-1] ntr1.OnRequestRemoval(unlucky) // then (m-1) out of n fallbacks should be completed setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests[:nSigs-1], true) require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()]) @@ -690,20 +701,20 @@ func TestNotary(t *testing.T) { setFinalizeWithError(true) requests, requesters = checkCompleteMultisigRequest(t, nSigs, nKeys, false) // make fallbacks valid and then remove all of them - _, err = bc.genBlocks(int(nvbDiffFallback)) + e.GenerateNewBlocks(t, int(nvbDiffFallback)) require.NoError(t, err) for i := range requests { ntr1.OnRequestRemoval(requests[i]) } // then the whole request should be removed, i.e. there are no completed transactions setFinalizeWithError(false) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) // // OnRequestRemoval: multisignature request, remove unexisting fallbac, i.e. there still shouldn't be any completed transactions after this ntr1.OnRequestRemoval(requests[0]) - require.NoError(t, bc.AddBlock(bc.newBlock())) + e.AddNewBlock(t) checkMainTx(t, requesters, requests, len(requests), false) checkFallbackTxs(t, requests, false) @@ -712,11 +723,11 @@ func TestNotary(t *testing.T) { requester1, _ := wallet.NewAccount() requester2, _ := wallet.NewAccount() amount := int64(100_0000_0000) - feer := NewNotaryFeerStub(bc) - transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester1.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50)) - checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(amount)) - transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester2.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50)) - checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(2*amount)) + gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester1.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight() + 50)}) + e.CheckGASBalance(t, notaryHash, big.NewInt(amount)) + gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester2.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight() + 50)}) + e.CheckGASBalance(t, notaryHash, big.NewInt(2*amount)) + // create request for 2 standard signatures => main tx should be completed after the second request is added to the pool requests = createMixedRequest([]requester{ { @@ -728,6 +739,7 @@ func TestNotary(t *testing.T) { typ: notary.Signature, }, }) + feer := network.NewNotaryFeer(bc) require.NoError(t, mp1.Add(requests[0].FallbackTransaction, feer, requests[0])) require.NoError(t, mp1.Add(requests[1].FallbackTransaction, feer, requests[1])) require.Eventually(t, func() bool { diff --git a/pkg/core/oracle_test.go b/pkg/core/oracle_test.go index 4f5b898ac..5529c39ca 100644 --- a/pkg/core/oracle_test.go +++ b/pkg/core/oracle_test.go @@ -1,7 +1,9 @@ -package core +package core_test import ( "bytes" + "encoding/binary" + "encoding/json" "errors" "fmt" gio "io" @@ -16,37 +18,38 @@ import ( "github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" - "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "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/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/services/oracle" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/util/slice" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) -var ( - oracleModulePath = filepath.Join("..", "services", "oracle") - pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") -) +var oracleModulePath = filepath.Join("..", "services", "oracle") -func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain, +func putOracleRequest(t *testing.T, oracleValidatorInvoker *neotest.ContractInvoker, url string, filter *string, cb string, userData []byte, gas int64) util.Uint256 { var filtItem interface{} if filter != nil { filtItem = *filter } - res, err := invokeContractMethod(bc, gas+50_000_000+5_000_000, h, "requestURL", - url, filtItem, cb, userData, gas) - require.NoError(t, err) - return res.Container + return oracleValidatorInvoker.Invoke(t, stackitem.Null{}, "requestURL", url, filtItem, cb, userData, gas) } -func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string, returnOracleRedirectionErrOn func(address string) bool) oracle.Config { +func getOracleConfig(t *testing.T, bc *core.Blockchain, w, pass string, returnOracleRedirectionErrOn func(address string) bool) oracle.Config { return oracle.Config{ Log: zaptest.NewLogger(t), Network: netmode.UnitTestNet, @@ -63,7 +66,7 @@ func getOracleConfig(t *testing.T, bc *Blockchain, w, pass string, returnOracleR } } -func getTestOracle(t *testing.T, bc *Blockchain, walletPath, pass string) ( +func getTestOracle(t *testing.T, bc *core.Blockchain, walletPath, pass string) ( *wallet.Account, *oracle.Oracle, map[uint64]*responseWithSig, @@ -87,7 +90,19 @@ func getTestOracle(t *testing.T, bc *Blockchain, walletPath, pass string) ( // Compatibility test from C# code. // https://github.com/neo-project/neo-modules/blob/master/tests/Neo.Plugins.OracleService.Tests/UT_OracleService.cs#L61 func TestCreateResponseTx(t *testing.T) { - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) require.Equal(t, int64(30), bc.GetBaseExecFee()) require.Equal(t, int64(1000), bc.FeePerByte()) @@ -106,10 +121,10 @@ func TestCreateResponseTx(t *testing.T) { Code: transaction.Success, Result: []byte{0}, } - require.NoError(t, bc.contracts.Oracle.PutRequestInternal(1, req, bc.dao)) + cInvoker.Invoke(t, stackitem.Null{}, "requestURL", req.URL, *req.Filter, req.CallbackMethod, req.UserData, int64(req.GasForResponse)) orc.UpdateOracleNodes(keys.PublicKeys{acc.PrivateKey().PublicKey()}) bc.SetOracle(orc) - tx, err := orc.CreateResponseTx(int64(req.GasForResponse), 1, resp) + tx, err = orc.CreateResponseTx(int64(req.GasForResponse), 1, resp) require.NoError(t, err) assert.Equal(t, 166, tx.Size()) assert.Equal(t, int64(2198650), tx.NetworkFee) @@ -117,7 +132,7 @@ func TestCreateResponseTx(t *testing.T) { } func TestOracle_InvalidWallet(t *testing.T) { - bc := newTestChain(t) + bc, _, _ := chain.NewMulti(t) _, err := oracle.NewOracle(getOracleConfig(t, bc, "./testdata/oracle1.json", "invalid", nil)) require.Error(t, err) @@ -127,45 +142,65 @@ func TestOracle_InvalidWallet(t *testing.T) { } func TestOracle(t *testing.T) { - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management)) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + nativeOracleH := e.NativeHash(t, nativenames.Oracle) + nativeOracleID := e.NativeID(t, nativenames.Oracle) - oracleCtr := bc.contracts.Oracle acc1, orc1, m1, ch1 := getTestOracle(t, bc, "./testdata/oracle1.json", "one") acc2, orc2, m2, ch2 := getTestOracle(t, bc, "./testdata/oracle2.json", "two") oracleNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()} // Must be set in native contract for tx verification. - bc.setNodesByRole(t, true, noderoles.Oracle, oracleNodes) + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), []interface{}{oracleNodes[0].Bytes(), oracleNodes[1].Bytes()}) orc1.UpdateOracleNodes(oracleNodes.Copy()) orc2.UpdateOracleNodes(oracleNodes.Copy()) - orcNative := bc.contracts.Oracle - md, ok := orcNative.GetMethod(manifest.MethodVerify, -1) - require.True(t, ok) - orc1.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset) - orc2.UpdateNativeContract(orcNative.NEF.Script, orcNative.GetOracleResponseScript(), orcNative.Hash, md.MD.Offset) + nativeOracleState := bc.GetContractState(nativeOracleH) + require.NotNil(t, nativeOracleState) + md := nativeOracleState.Manifest.ABI.GetMethod(manifest.MethodVerify, -1) + require.NotNil(t, md) + oracleRespScript := native.CreateOracleResponseScript(nativeOracleH) + orc1.UpdateNativeContract(nativeOracleState.NEF.Script, slice.Copy(oracleRespScript), nativeOracleH, md.Offset) + orc2.UpdateNativeContract(nativeOracleState.NEF.Script, slice.Copy(oracleRespScript), nativeOracleH, md.Offset) - cs := contracts.GetOracleContractState(t, pathToInternalContracts, util.Uint160{}, 42) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + rawManifest, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + rawNef, err := cs.NEF.Bytes() + require.NoError(t, err) + tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash()) + cInvoker := e.ValidatorInvoker(cs.Hash) - putOracleRequest(t, cs.Hash, bc, "https://get.1234", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.1234", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.timeout", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.notfound", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.forbidden", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://private.url", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.big", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.maxallowed", nil, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.maxallowed", nil, "handle", []byte{}, 100_000_000) + putOracleRequest(t, cInvoker, "https://get.1234", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.1234", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.timeout", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.notfound", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.forbidden", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://private.url", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.big", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.maxallowed", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.maxallowed", nil, "handle", []byte{}, 100_000_000) flt := "$.Values[1]" - putOracleRequest(t, cs.Hash, bc, "https://get.filter", &flt, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.filterinv", &flt, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.filter", &flt, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.filterinv", &flt, "handle", []byte{}, 10_000_000) - putOracleRequest(t, cs.Hash, bc, "https://get.invalidcontent", nil, "handle", []byte{}, 10_000_000) + putOracleRequest(t, cInvoker, "https://get.invalidcontent", nil, "handle", []byte{}, 10_000_000) checkResp := func(t *testing.T, id uint64, resp *transaction.OracleResponse) *state.OracleRequest { - req, err := oracleCtr.GetRequestInternal(bc.dao, id) - require.NoError(t, err) + // Use a hack to get request from Oracle contract, because we can't use GetRequestInternal directly. + requestKey := make([]byte, 9) + requestKey[0] = 7 // prefixRequest from native Oracle contract + binary.BigEndian.PutUint64(requestKey[1:], id) + si := bc.GetStorageItem(nativeOracleID, requestKey) + require.NotNil(t, si) + req := new(state.OracleRequest) + require.NoError(t, stackitem.DeserializeConvertible(si, req)) reqs := map[uint64]*state.OracleRequest{id: req} orc1.ProcessRequestsInternal(reqs) @@ -198,7 +233,7 @@ func TestOracle(t *testing.T) { actualHash := cp.Hash() require.Equal(t, actualHash, cachedHash, "transaction hash was changed during ") - require.NoError(t, bc.verifyAndPoolTx(tx, bc.GetMemPool(), bc)) + require.NoError(t, bc.PoolTx(tx)) } t.Run("NormalRequest", func(t *testing.T) { @@ -306,21 +341,34 @@ func TestOracle(t *testing.T) { } func TestOracleFull(t *testing.T) { - bc := initTestChain(t, nil, nil) + bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, nil, false) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + acc, orc, _, _ := getTestOracle(t, bc, "./testdata/oracle2.json", "two") mp := bc.GetMemPool() orc.OnTransaction = func(tx *transaction.Transaction) error { return mp.Add(tx, bc) } bc.SetOracle(orc) - cs := contracts.GetOracleContractState(t, pathToInternalContracts, util.Uint160{}, 42) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - go bc.Run() orc.Start() - t.Cleanup(orc.Shutdown) + t.Cleanup(func() { + orc.Shutdown() + bc.Close() + }) - bc.setNodesByRole(t, true, noderoles.Oracle, keys.PublicKeys{acc.PrivateKey().PublicKey()}) - putOracleRequest(t, cs.Hash, bc, "https://get.1234", new(string), "handle", []byte{}, 10_000_000) + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), []interface{}{acc.PrivateKey().PublicKey().Bytes()}) + + cs := contracts.GetOracleContractState(t, pathToInternalContracts, validator.ScriptHash(), 0) + e.DeployContract(t, &neotest.Contract{ + Hash: cs.Hash, + NEF: &cs.NEF, + Manifest: &cs.Manifest, + }, nil) + cInvoker := e.ValidatorInvoker(cs.Hash) + + putOracleRequest(t, cInvoker, "https://get.1234", new(string), "handle", []byte{}, 10_000_000) require.Eventually(t, func() bool { return mp.Count() == 1 }, time.Second*3, time.Millisecond*200) @@ -331,17 +379,20 @@ func TestOracleFull(t *testing.T) { } func TestNotYetRunningOracle(t *testing.T) { - bc := initTestChain(t, nil, nil) + bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, nil, false) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + acc, orc, _, _ := getTestOracle(t, bc, "./testdata/oracle2.json", "two") mp := bc.GetMemPool() orc.OnTransaction = func(tx *transaction.Transaction) error { return mp.Add(tx, bc) } bc.SetOracle(orc) - cs := contracts.GetOracleContractState(t, pathToInternalContracts, util.Uint160{}, 42) - require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs)) - go bc.Run() - bc.setNodesByRole(t, true, noderoles.Oracle, keys.PublicKeys{acc.PrivateKey().PublicKey()}) + t.Cleanup(bc.Close) + + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.Oracle), []interface{}{acc.PrivateKey().PublicKey().Bytes()}) var req state.OracleRequest var reqs = make(map[uint64]*state.OracleRequest) diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go index 0b27a92c3..3a958def5 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/core/stateroot_test.go @@ -1,6 +1,7 @@ -package core +package core_test import ( + "crypto/elliptic" "errors" "path/filepath" "sort" @@ -10,18 +11,25 @@ import ( "github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" + corestate "github.com/nspcc-dev/neo-go/pkg/core/stateroot" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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/io" + "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/services/stateroot" "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/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/atomic" @@ -70,64 +78,71 @@ func newMajorityMultisigWithGAS(t *testing.T, n int) (util.Uint160, keys.PublicK } func TestStateRoot(t *testing.T) { - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) h, pubs, accs := newMajorityMultisigWithGAS(t, 2) - bc.setNodesByRole(t, true, noderoles.StateValidator, pubs) + validatorNodes := []interface{}{pubs[0].Bytes(), pubs[1].Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.StateValidator), validatorNodes) updateIndex := bc.BlockHeight() - transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil) tmpDir := t.TempDir() w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass") cfg := createStateRootConfig(w.Path(), "pass") - srv, err := stateroot.New(cfg, bc.stateRoot, zaptest.NewLogger(t), bc, nil) + srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here. + srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil) require.NoError(t, err) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) - r, err := bc.stateRoot.GetStateRoot(bc.BlockHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) + r, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight()) require.NoError(t, err) - require.Equal(t, r.Root, bc.stateRoot.CurrentLocalStateRoot()) + require.Equal(t, r.Root, bc.GetStateModule().CurrentLocalStateRoot()) t.Run("invalid message", func(t *testing.T) { require.Error(t, srv.OnPayload(&payload.Extensible{Data: []byte{42}})) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) t.Run("drop zero index", func(t *testing.T) { - r, err := bc.stateRoot.GetStateRoot(0) + r, err := bc.GetStateModule().GetStateRoot(0) require.NoError(t, err) data, err := testserdes.EncodeBinary(stateroot.NewMessage(stateroot.RootT, r)) require.NoError(t, err) require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) t.Run("invalid height", func(t *testing.T) { - r, err := bc.stateRoot.GetStateRoot(1) + r, err := bc.GetStateModule().GetStateRoot(1) require.NoError(t, err) r.Index = 10 data := testSignStateRoot(t, r, pubs, accs...) require.Error(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) t.Run("invalid signer", func(t *testing.T) { accInv, err := wallet.NewAccount() require.NoError(t, err) pubs := keys.PublicKeys{accInv.PrivateKey().PublicKey()} require.NoError(t, accInv.ConvertMultisig(1, pubs)) - transferTokenFromMultisigAccount(t, bc, accInv.Contract.ScriptHash(), bc.contracts.GAS.Hash, 1_0000_0000) - r, err := bc.stateRoot.GetStateRoot(1) + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), accInv.Contract.ScriptHash(), 1_0000_0000, nil) + r, err := bc.GetStateModule().GetStateRoot(1) require.NoError(t, err) data := testSignStateRoot(t, r, pubs, accInv) err = srv.OnPayload(&payload.Extensible{Data: data}) - require.True(t, errors.Is(err, ErrWitnessHashMismatch), "got: %v", err) - require.EqualValues(t, 0, bc.stateRoot.CurrentValidatedHeight()) + require.True(t, errors.Is(err, core.ErrWitnessHashMismatch), "got: %v", err) + require.EqualValues(t, 0, bc.GetStateModule().CurrentValidatedHeight()) }) - r, err = bc.stateRoot.GetStateRoot(updateIndex + 1) + r, err = bc.GetStateModule().GetStateRoot(updateIndex + 1) require.NoError(t, err) data := testSignStateRoot(t, r, pubs, accs...) require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 2, bc.stateRoot.CurrentValidatedHeight()) + require.EqualValues(t, 2, bc.GetStateModule().CurrentValidatedHeight()) - r, err = bc.stateRoot.GetStateRoot(updateIndex + 1) + r, err = bc.GetStateModule().GetStateRoot(updateIndex + 1) require.NoError(t, err) require.NotEqual(t, 0, len(r.Witness)) require.Equal(t, h, r.Witness[0].ScriptHash()) @@ -145,26 +160,31 @@ func TestStateRootInitNonZeroHeight(t *testing.T) { var root util.Uint256 t.Run("init", func(t *testing.T) { // this is in a separate test to do proper cleanup - bc := newTestChainWithCustomCfgAndStore(t, st, nil) - bc.setNodesByRole(t, true, noderoles.StateValidator, pubs) - transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + bc, validator, committee := chain.NewMultiWithCustomConfigAndStore(t, nil, st, true) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) + + validatorNodes := []interface{}{pubs[0].Bytes(), pubs[1].Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.StateValidator), validatorNodes) + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil) - _, err := persistBlock(bc) - require.NoError(t, err) tmpDir := t.TempDir() w := createAndWriteWallet(t, accs[0], filepath.Join(tmpDir, "w"), "pass") cfg := createStateRootConfig(w.Path(), "pass") - srv, err := stateroot.New(cfg, bc.stateRoot, zaptest.NewLogger(t), bc, nil) + srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here. + srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, nil) require.NoError(t, err) - r, err := bc.stateRoot.GetStateRoot(2) + r, err := bc.GetStateModule().GetStateRoot(2) require.NoError(t, err) data := testSignStateRoot(t, r, pubs, accs...) require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) - require.EqualValues(t, 2, bc.stateRoot.CurrentValidatedHeight()) - root = bc.stateRoot.CurrentLocalStateRoot() + require.EqualValues(t, 2, bc.GetStateModule().CurrentValidatedHeight()) + root = bc.GetStateModule().CurrentLocalStateRoot() }) - bc2 := newTestChainWithCustomCfgAndStore(t, st, nil) + bc2, _, _ := chain.NewMultiWithCustomConfigAndStore(t, nil, st, true) srv := bc2.GetStateModule() require.EqualValues(t, 2, srv.CurrentValidatedHeight()) require.Equal(t, root, srv.CurrentLocalStateRoot()) @@ -192,7 +212,22 @@ func createStateRootConfig(walletPath, password string) config.StateRoot { func TestStateRootFull(t *testing.T) { tmpDir := t.TempDir() - bc := newTestChain(t) + bc, validator, committee := chain.NewMulti(t) + e := neotest.NewExecutor(t, bc, validator, committee) + designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validator, committee) + gasValidatorInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)) + + getDesignatedByRole := func(t *testing.T, h uint32) keys.PublicKeys { + res, err := designationSuperInvoker.TestInvoke(t, "getDesignatedByRole", int64(noderoles.StateValidator), h) + require.NoError(t, err) + nodes := res.Pop().Value().([]stackitem.Item) + pubs := make(keys.PublicKeys, len(nodes)) + for i, node := range nodes { + pubs[i], err = keys.NewPublicKeyFromBytes(node.Value().([]byte), elliptic.P256()) + require.NoError(t, err) + } + return pubs + } h, pubs, accs := newMajorityMultisigWithGAS(t, 2) w := createAndWriteWallet(t, accs[1], filepath.Join(tmpDir, "wallet2"), "two") @@ -200,7 +235,8 @@ func TestStateRootFull(t *testing.T) { var lastValidated atomic.Value var lastHeight atomic.Uint32 - srv, err := stateroot.New(cfg, bc.stateRoot, zaptest.NewLogger(t), bc, func(ep *payload.Extensible) { + srMod := bc.GetStateModule().(*corestate.Module) // Take full responsibility here. + srv, err := stateroot.New(cfg, srMod, zaptest.NewLogger(t), bc, func(ep *payload.Extensible) { lastHeight.Store(ep.ValidBlockStart) lastValidated.Store(ep) }) @@ -208,16 +244,17 @@ func TestStateRootFull(t *testing.T) { srv.Start() t.Cleanup(srv.Shutdown) - bc.setNodesByRole(t, true, noderoles.StateValidator, pubs) - transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + validatorNodes := []interface{}{pubs[0].Bytes(), pubs[1].Bytes()} + designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", + int64(roles.StateValidator), validatorNodes) + gasValidatorInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), h, 1_0000_0000, nil) require.Eventually(t, func() bool { return lastHeight.Load() == 2 }, time.Second, time.Millisecond) - checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 2, 1) - _, err = persistBlock(bc) - require.NoError(t, err) + checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 2, 1, getDesignatedByRole) + e.AddNewBlock(t) require.Eventually(t, func() bool { return lastHeight.Load() == 3 }, time.Second, time.Millisecond) - checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 3, 1) + checkVoteBroadcasted(t, bc, lastValidated.Load().(*payload.Extensible), 3, 1, getDesignatedByRole) - r, err := bc.stateRoot.GetStateRoot(2) + r, err := bc.GetStateModule().GetStateRoot(2) require.NoError(t, err) require.NoError(t, srv.AddSignature(2, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r))) require.NotNil(t, lastValidated.Load().(*payload.Extensible)) @@ -226,7 +263,7 @@ func TestStateRootFull(t *testing.T) { require.NoError(t, testserdes.DecodeBinary(lastValidated.Load().(*payload.Extensible).Data, msg)) require.NotEqual(t, stateroot.RootT, msg.Type) // not a sender for this root - r, err = bc.stateRoot.GetStateRoot(3) + r, err = bc.GetStateModule().GetStateRoot(3) require.NoError(t, err) require.Error(t, srv.AddSignature(2, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r))) require.NoError(t, srv.AddSignature(3, 0, accs[0].PrivateKey().SignHashable(uint32(netmode.UnitTestNet), r))) @@ -241,8 +278,8 @@ func TestStateRootFull(t *testing.T) { require.Equal(t, r.Root, actual.Root) } -func checkVoteBroadcasted(t *testing.T, bc *Blockchain, p *payload.Extensible, - height uint32, valIndex byte) { +func checkVoteBroadcasted(t *testing.T, bc *core.Blockchain, p *payload.Extensible, + height uint32, valIndex byte, getDesignatedByRole func(t *testing.T, h uint32) keys.PublicKeys) { require.NotNil(t, p) m := new(stateroot.Message) require.NoError(t, testserdes.DecodeBinary(p.Data, m)) @@ -255,8 +292,7 @@ func checkVoteBroadcasted(t *testing.T, bc *Blockchain, p *payload.Extensible, require.Equal(t, height, vote.Height) require.Equal(t, int32(valIndex), vote.ValidatorIndex) - pubs, _, err := bc.contracts.Designate.GetDesignatedByRole(bc.dao, noderoles.StateValidator, bc.BlockHeight()) - require.NoError(t, err) + pubs := getDesignatedByRole(t, bc.BlockHeight()) require.True(t, len(pubs) > int(valIndex)) require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r)) } diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index 4665f2ee1..7904914e2 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -174,12 +174,22 @@ func (e *Executor) DeployContractCheckFAULT(t testing.TB, c *Contract, data inte // InvokeScript adds transaction with the specified script to the chain and // returns its hash. It does no faults check. func (e *Executor) InvokeScript(t testing.TB, script []byte, signers []Signer) util.Uint256 { + tx := e.PrepareInvocation(t, script, signers) + e.AddNewBlock(t, tx) + return tx.Hash() +} + +// PrepareInvocation creates transaction with the specified script and signs it +// by the provided signer. +func (e *Executor) PrepareInvocation(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) *transaction.Transaction { tx := transaction.New(script, 0) tx.Nonce = Nonce() tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + if len(validUntilBlock) != 0 { + tx.ValidUntilBlock = validUntilBlock[0] + } e.SignTx(t, tx, -1, signers...) - e.AddNewBlock(t, tx) - return tx.Hash() + return tx } // InvokeScriptCheckHALT adds transaction with the specified script to the chain @@ -323,10 +333,12 @@ func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *b } // GenerateNewBlocks adds specified number of empty blocks to the chain. -func (e *Executor) GenerateNewBlocks(t testing.TB, count int) { +func (e *Executor) GenerateNewBlocks(t testing.TB, count int) []*block.Block { + blocks := make([]*block.Block, count) for i := 0; i < count; i++ { - e.AddNewBlock(t) + blocks[i] = e.AddNewBlock(t) } + return blocks } // SignBlock add validators signature to b. diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index 453421b7c..a98b4c10b 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -128,8 +128,7 @@ func NewSingle(t testing.TB) (*core.Blockchain, neotest.Signer) { // NewSingleWithCustomConfig is similar to NewSingle, but allows to override the // default configuration. func NewSingleWithCustomConfig(t testing.TB, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer) { - st := storage.NewMemoryStore() - return NewSingleWithCustomConfigAndStore(t, f, st, true) + return NewSingleWithCustomConfigAndStore(t, f, nil, true) } // NewSingleWithCustomConfigAndStore is similar to NewSingleWithCustomConfig, but @@ -150,6 +149,9 @@ func NewSingleWithCustomConfigAndStore(t testing.TB, f func(cfg *config.Protocol if f != nil { f(&protoCfg) } + if st == nil { + st = storage.NewMemoryStore() + } log := zaptest.NewLogger(t) bc, err := core.NewBlockchain(st, protoCfg, log) require.NoError(t, err)