diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 45b00f554..182ed9ac5 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -22,6 +22,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "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" @@ -1179,24 +1180,41 @@ func (bc *Blockchain) ApplyPolicyToTxSet(txes []*transaction.Transaction) []*tra return txes } +// Various errors that could be returns upon header verification. +var ( + ErrHdrHashMismatch = errors.New("previous header hash doesn't match") + ErrHdrIndexMismatch = errors.New("previous header index doesn't match") + ErrHdrInvalidTimestamp = errors.New("block is not newer than the previous one") +) + func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error { if prevHeader.Hash() != currHeader.PrevHash { - return errors.New("previous header hash doesn't match") + return ErrHdrHashMismatch } if prevHeader.Index+1 != currHeader.Index { - return errors.New("previous header index doesn't match") + return ErrHdrIndexMismatch } if prevHeader.Timestamp >= currHeader.Timestamp { - return errors.New("block is not newer than the previous one") + return ErrHdrInvalidTimestamp } 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 +1224,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,17 +1423,41 @@ 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") + ErrUnknownVerificationContract = errors.New("unknown verification contract") + ErrInvalidVerificationContract = errors.New("verification contract is missing `verify` method") +) + // 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) - if err != nil { - return err + var offset int + var initMD *manifest.Method + verification := witness.VerificationScript + if len(verification) != 0 { + if witness.ScriptHash() != hash { + return ErrWitnessHashMismatch + } + } else { + cs, err := interopCtx.DAO.GetContractState(hash) + if err != nil { + return ErrUnknownVerificationContract + } + md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify) + if md == nil { + return ErrInvalidVerificationContract + } + verification = cs.Script + offset = md.Offset + initMD = cs.Manifest.ABI.GetMethod(manifest.MethodInit) } gasPolicy := bc.contracts.Policy.GetMaxVerificationGas(interopCtx.DAO) @@ -1427,6 +1469,10 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa vm.SetPriceGetter(getPrice) vm.GasLimit = gas vm.LoadScriptWithFlags(verification, smartcontract.NoneFlag) + vm.Jump(vm.Context(), offset) + if initMD != nil { + vm.Call(vm.Context(), initMD.Offset) + } vm.LoadScript(witness.InvocationScript) if useKeys { bc.keyCacheLock.RLock() @@ -1435,14 +1481,14 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa } bc.keyCacheLock.RUnlock() } - err = vm.Run() + 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 +1501,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 +1514,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 66d10bdc4..7f281b012 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" @@ -12,15 +15,46 @@ 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/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" ) +func TestVerifyHeader(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + prev := bc.topBlock.Load().(*block.Block).Header() + t.Run("Invalid", func(t *testing.T) { + t.Run("Hash", func(t *testing.T) { + h := prev.Hash() + h[0] = ^h[0] + hdr := newBlock(bc.config, 1, h).Header() + require.True(t, errors.Is(bc.verifyHeader(hdr, prev), ErrHdrHashMismatch)) + }) + t.Run("Index", func(t *testing.T) { + hdr := newBlock(bc.config, 3, prev.Hash()).Header() + require.True(t, errors.Is(bc.verifyHeader(hdr, prev), ErrHdrIndexMismatch)) + }) + t.Run("Timestamp", func(t *testing.T) { + hdr := newBlock(bc.config, 1, prev.Hash()).Header() + hdr.Timestamp = 0 + require.True(t, errors.Is(bc.verifyHeader(hdr, prev), ErrHdrInvalidTimestamp)) + }) + }) + t.Run("Valid", func(t *testing.T) { + hdr := newBlock(bc.config, 1, prev.Hash()).Header() + require.NoError(t, bc.verifyHeader(hdr, prev)) + }) +} + func TestAddHeaders(t *testing.T) { bc := newTestChain(t) defer bc.Close() @@ -145,6 +179,183 @@ 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 TestVerifyHashAgainstScript(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + cs, csInvalid := getTestContractState() + ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil) + require.NoError(t, ic.DAO.PutContractState(cs)) + require.NoError(t, ic.DAO.PutContractState(csInvalid)) + + gas := bc.contracts.Policy.GetMaxVerificationGas(ic.DAO) + t.Run("Contract", func(t *testing.T) { + t.Run("Missing", func(t *testing.T) { + newH := cs.ScriptHash() + newH[0] = ^newH[0] + w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} + err := bc.verifyHashAgainstScript(newH, w, ic, false, 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.ScriptHash(), w, ic, false, 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.ScriptHash(), w, ic, false, gas) + require.NoError(t, err) + }) + t.Run("InvalidSignature", func(t *testing.T) { + w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH3)}} + err := bc.verifyHashAgainstScript(cs.ScriptHash(), w, ic, false, 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, false, 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, false, gas) + require.True(t, errors.Is(err, ErrVerificationFailed)) + }) +} + func TestHasBlock(t *testing.T) { bc := newTestChain(t) blocks, err := bc.genBlocks(50) diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 1a1b7d8bb..cf869efa4 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -337,6 +337,7 @@ func getTestContractState() (*state.Contract, *state.Contract) { byte(opcode.LDSFLD0), byte(opcode.ADD), byte(opcode.RET), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.RET), byte(opcode.RET), + byte(opcode.LDSFLD0), byte(opcode.SUB), byte(opcode.CONVERT), byte(stackitem.BooleanT), byte(opcode.RET), } h := hash.Hash160(script) m := manifest.NewManifest(h) @@ -384,6 +385,11 @@ func getTestContractState() (*state.Contract, *state.Contract) { Offset: 18, ReturnType: smartcontract.IntegerType, }, + { + Name: manifest.MethodVerify, + Offset: 19, + ReturnType: smartcontract.BoolType, + }, } cs := &state.Contract{ Script: script, diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index 1d528eec8..f2b76e9d7 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -15,6 +15,9 @@ const ( // MethodInit is a name for default initialization method. MethodInit = "_initialize" + // MethodVerify is a name for default verification method. + MethodVerify = "verify" + // NEP5StandardName represents the name of NEP5 smartcontract standard. NEP5StandardName = "NEP-5" // NEP10StandardName represents the name of NEP10 smartcontract standard.