core: allow to use verification contracts

In NEO3 we can't just appcall hash, as verification script has no access
to state. Instead we use `verify` method of an arbitrary contract.
This commit is contained in:
Evgenii Stratonikov 2020-08-13 18:42:53 +03:00
parent 7f2a931fb6
commit 1eb9a4c6c6
4 changed files with 75 additions and 6 deletions

View file

@ -22,6 +22,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "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/smartcontract/trigger"
"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"
@ -1430,15 +1431,33 @@ func ScriptFromWitness(hash util.Uint160, witness *transaction.Witness) ([]byte,
// Various witness verification errors. // Various witness verification errors.
var ( var (
ErrWitnessHashMismatch = errors.New("witness hash mismatch") ErrWitnessHashMismatch = errors.New("witness hash mismatch")
ErrVerificationFailed = errors.New("signature check failed") 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. // 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) var offset int
if err != nil { var initMD *manifest.Method
return err 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) gasPolicy := bc.contracts.Policy.GetMaxVerificationGas(interopCtx.DAO)
@ -1450,6 +1469,10 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
vm.SetPriceGetter(getPrice) vm.SetPriceGetter(getPrice)
vm.GasLimit = gas vm.GasLimit = gas
vm.LoadScriptWithFlags(verification, smartcontract.NoneFlag) vm.LoadScriptWithFlags(verification, smartcontract.NoneFlag)
vm.Jump(vm.Context(), offset)
if initMD != nil {
vm.Call(vm.Context(), initMD.Offset)
}
vm.LoadScript(witness.InvocationScript) vm.LoadScript(witness.InvocationScript)
if useKeys { if useKeys {
bc.keyCacheLock.RLock() bc.keyCacheLock.RLock()
@ -1458,7 +1481,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
} }
bc.keyCacheLock.RUnlock() bc.keyCacheLock.RUnlock()
} }
err = vm.Run() err := vm.Run()
if vm.HasFailed() { if vm.HasFailed() {
return fmt.Errorf("%w: vm execution has failed: %v", ErrVerificationFailed, err) return fmt.Errorf("%w: vm execution has failed: %v", ErrVerificationFailed, err)
} }

View file

@ -16,6 +16,7 @@ import (
"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/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/smartcontract/trigger"
"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"
@ -300,6 +301,42 @@ func TestVerifyTx(t *testing.T) {
}) })
} }
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))
})
})
}
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)

View file

@ -337,6 +337,7 @@ func getTestContractState() (*state.Contract, *state.Contract) {
byte(opcode.LDSFLD0), byte(opcode.ADD), byte(opcode.RET), byte(opcode.LDSFLD0), byte(opcode.ADD), byte(opcode.RET),
byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.RET), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.RET),
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) h := hash.Hash160(script)
m := manifest.NewManifest(h) m := manifest.NewManifest(h)
@ -384,6 +385,11 @@ func getTestContractState() (*state.Contract, *state.Contract) {
Offset: 18, Offset: 18,
ReturnType: smartcontract.IntegerType, ReturnType: smartcontract.IntegerType,
}, },
{
Name: manifest.MethodVerify,
Offset: 19,
ReturnType: smartcontract.BoolType,
},
} }
cs := &state.Contract{ cs := &state.Contract{
Script: script, Script: script,

View file

@ -15,6 +15,9 @@ const (
// MethodInit is a name for default initialization method. // MethodInit is a name for default initialization method.
MethodInit = "_initialize" MethodInit = "_initialize"
// MethodVerify is a name for default verification method.
MethodVerify = "verify"
// NEP5StandardName represents the name of NEP5 smartcontract standard. // NEP5StandardName represents the name of NEP5 smartcontract standard.
NEP5StandardName = "NEP-5" NEP5StandardName = "NEP-5"
// NEP10StandardName represents the name of NEP10 smartcontract standard. // NEP10StandardName represents the name of NEP10 smartcontract standard.