core: add tests for (*Blockchain).verifyTx

This commit is contained in:
Evgenii Stratonikov 2020-08-13 13:42:21 +03:00
parent 95d86b67c3
commit e8cf4d96ce
2 changed files with 155 additions and 10 deletions

View file

@ -1192,11 +1192,21 @@ func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error {
return bc.verifyHeaderWitnesses(currHeader, prevHeader) 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. // verifyTx verifies whether a transaction is bonafide or not.
func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) error { func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) error {
height := bc.BlockHeight() height := bc.BlockHeight()
if t.ValidUntilBlock <= height || t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement { 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. // Policying.
if err := bc.contracts.Policy.CheckPolicy(bc.dao, t); err != nil { 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()) balance := bc.GetUtilityTokenBalance(t.Sender())
need := t.SystemFee + t.NetworkFee need := t.SystemFee + t.NetworkFee
if balance.Cmp(big.NewInt(need)) < 0 { 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) size := io.GetVarSize(t)
if size > transaction.MaxTransactionSize { 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() needNetworkFee := int64(size) * bc.FeePerByte()
netFee := t.NetworkFee - needNetworkFee netFee := t.NetworkFee - needNetworkFee
if netFee < 0 { 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 block == nil {
if ok := bc.memPool.Verify(t, bc); !ok { 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) emit.AppCall(bb.BinWriter, hash)
verification = bb.Bytes() verification = bb.Bytes()
} else if h := witness.ScriptHash(); hash != h { } else if h := witness.ScriptHash(); hash != h {
return nil, errors.New("witness hash mismatch") return nil, ErrWitnessHashMismatch
} }
return verification, nil 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. // 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 { func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, interopCtx *interop.Context, useKeys bool, gas int64) error {
verification, err := ScriptFromWitness(hash, witness) verification, err := ScriptFromWitness(hash, witness)
@ -1437,12 +1453,12 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
} }
err = vm.Run() err = vm.Run()
if vm.HasFailed() { 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() resEl := vm.Estack().Pop()
if resEl != nil { if resEl != nil {
if !resEl.Bool() { if !resEl.Bool() {
return fmt.Errorf("signature check failed") return fmt.Errorf("%w: invalid signature", ErrVerificationFailed)
} }
if useKeys { if useKeys {
bc.keyCacheLock.RLock() bc.keyCacheLock.RLock()
@ -1455,7 +1471,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
} }
} }
} else { } else {
return fmt.Errorf("no result returned from the script") return fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed)
} }
return nil 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). // 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 { func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block) error {
if len(t.Signers) != len(t.Scripts) { 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) interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t)
for i := range t.Signers { for i := range t.Signers {

View file

@ -1,7 +1,10 @@
package core package core
import ( import (
"errors"
"fmt"
"math/big" "math/big"
"math/rand"
"testing" "testing"
"time" "time"
@ -11,11 +14,14 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/storage" "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/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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/io"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "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/assert"
"github.com/stretchr/testify/require" "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) { func TestHasBlock(t *testing.T) {
bc := newTestChain(t) bc := newTestChain(t)
blocks, err := bc.genBlocks(50) blocks, err := bc.genBlocks(50)