From e8cf4d96cef7f25ae3c18418ac7fbedc20b6dba6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 13 Aug 2020 13:42:21 +0300 Subject: [PATCH] core: add tests for (*Blockchain).verifyTx --- pkg/core/blockchain.go | 36 +++++++--- pkg/core/blockchain_test.go | 129 ++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 10 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 183fdbc8e..386d24dab 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1192,11 +1192,21 @@ func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error { return bc.verifyHeaderWitnesses(currHeader, prevHeader) } +// Various errors that could be returned upon verification. +var ( + ErrTxExpired = errors.New("transaction has expired") + ErrInsufficientFunds = errors.New("insufficient funds") + ErrTxSmallNetworkFee = errors.New("too small network fee") + ErrTxTooBig = errors.New("too big transaction") + ErrMemPoolConflict = errors.New("invalid transaction due to conflicts with the memory pool") + ErrTxInvalidWitnessNum = errors.New("number of signers doesn't match witnesses") +) + // verifyTx verifies whether a transaction is bonafide or not. func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) error { height := bc.BlockHeight() if t.ValidUntilBlock <= height || t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement { - return fmt.Errorf("transaction has expired. ValidUntilBlock = %d, current height = %d", t.ValidUntilBlock, height) + return fmt.Errorf("%w: ValidUntilBlock = %d, current height = %d", ErrTxExpired, t.ValidUntilBlock, height) } // Policying. if err := bc.contracts.Policy.CheckPolicy(bc.dao, t); err != nil { @@ -1206,20 +1216,20 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e balance := bc.GetUtilityTokenBalance(t.Sender()) need := t.SystemFee + t.NetworkFee if balance.Cmp(big.NewInt(need)) < 0 { - return fmt.Errorf("insufficient funds: balance is %v, need: %v", balance, need) + return fmt.Errorf("%w: balance is %v, need: %v", ErrInsufficientFunds, balance, need) } size := io.GetVarSize(t) if size > transaction.MaxTransactionSize { - return fmt.Errorf("too big transaction (%d > MaxTransactionSize %d)", size, transaction.MaxTransactionSize) + return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize) } needNetworkFee := int64(size) * bc.FeePerByte() netFee := t.NetworkFee - needNetworkFee if netFee < 0 { - return fmt.Errorf("insufficient funds: net fee is %v, need %v", t.NetworkFee, needNetworkFee) + return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee) } if block == nil { if ok := bc.memPool.Verify(t, bc); !ok { - return errors.New("invalid transaction due to conflicts with the memory pool") + return ErrMemPoolConflict } } @@ -1405,12 +1415,18 @@ func ScriptFromWitness(hash util.Uint160, witness *transaction.Witness) ([]byte, emit.AppCall(bb.BinWriter, hash) verification = bb.Bytes() } else if h := witness.ScriptHash(); hash != h { - return nil, errors.New("witness hash mismatch") + return nil, ErrWitnessHashMismatch } return verification, nil } +// Various witness verification errors. +var ( + ErrWitnessHashMismatch = errors.New("witness hash mismatch") + ErrVerificationFailed = errors.New("signature check failed") +) + // verifyHashAgainstScript verifies given hash against the given witness. func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, interopCtx *interop.Context, useKeys bool, gas int64) error { verification, err := ScriptFromWitness(hash, witness) @@ -1437,12 +1453,12 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa } err = vm.Run() if vm.HasFailed() { - return fmt.Errorf("vm execution has failed: %w", err) + return fmt.Errorf("%w: vm execution has failed: %v", ErrVerificationFailed, err) } resEl := vm.Estack().Pop() if resEl != nil { if !resEl.Bool() { - return fmt.Errorf("signature check failed") + return fmt.Errorf("%w: invalid signature", ErrVerificationFailed) } if useKeys { bc.keyCacheLock.RLock() @@ -1455,7 +1471,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa } } } else { - return fmt.Errorf("no result returned from the script") + return fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed) } return nil } @@ -1468,7 +1484,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa // Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block) error { if len(t.Signers) != len(t.Scripts) { - return fmt.Errorf("number of signers doesn't match witnesses (%d vs %d)", len(t.Signers), len(t.Scripts)) + return fmt.Errorf("%w: %d vs %d", ErrTxInvalidWitnessNum, len(t.Signers), len(t.Scripts)) } interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t) for i := range t.Signers { diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 7883ced7f..655831d8c 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -1,7 +1,10 @@ package core import ( + "errors" + "fmt" "math/big" + "math/rand" "testing" "time" @@ -11,11 +14,14 @@ import ( "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/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/io" "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" ) @@ -144,6 +150,129 @@ func TestGetBlock(t *testing.T) { } } +func (bc *Blockchain) newTestTx(h util.Uint160, script []byte) *transaction.Transaction { + tx := transaction.New(testchain.Network(), 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) + defer bc.Close() + + accs := make([]*wallet.Account, 2) + for i := range accs { + var err error + accs[i], err = wallet.NewAccount() + require.NoError(t, err) + } + + 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.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", + neoOwner, a.PrivateKey().GetScriptHash(), amount) + emit.Opcode(w.BinWriter, opcode.ASSERT) + } + } + require.NoError(t, w.Err) + + txMove := bc.newTestTx(neoOwner, w.Bytes()) + txMove.SystemFee = 1_000_000_000 + require.NoError(t, signTx(bc, txMove)) + b := bc.newBlock(txMove) + require.NoError(t, bc.AddBlock(b)) + + aer, err := bc.GetAppExecResult(txMove.Hash()) + require.NoError(t, err) + require.Equal(t, aer.VMState, vm.HaltState) + + res, err := invokeNativePolicyMethod(bc, "blockAccount", 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, nil) + fmt.Println(err) + require.True(t, errors.Is(err, expectedErr)) + } + + 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(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(tx)) + err := bc.verifyTx(tx, nil) + 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(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(tx)) + checkErr(t, ErrTxTooBig, tx) + }) + t.Run("SmallNetworkFee", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + tx.NetworkFee = 1 + require.NoError(t, accs[0].SignTx(tx)) + checkErr(t, ErrTxSmallNetworkFee, 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(tx)) + checkErr(t, nil, tx) + + tx2 := bc.newTestTx(h, testScript) + tx2.NetworkFee = balance / 2 + require.NoError(t, bc.memPool.Add(tx2, bc)) + checkErr(t, ErrMemPoolConflict, tx) + }) + t.Run("NotEnoughWitnesses", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + checkErr(t, ErrTxInvalidWitnessNum, tx) + }) + t.Run("InvalidWitnessHash", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + require.NoError(t, accs[0].SignTx(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(tx)) + tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10] + checkErr(t, ErrVerificationFailed, tx) + }) +} + func TestHasBlock(t *testing.T) { bc := newTestChain(t) blocks, err := bc.genBlocks(50)