diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index fd92310a6..54c5e4f69 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/options" "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/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" @@ -166,6 +167,20 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "import-deployed", + Usage: "import deployed contract", + UsageText: "import-multisig --wallet --wif --contract ", + Action: importDeployed, + Flags: append([]cli.Flag{ + walletPathFlag, + wifFlag, + cli.StringFlag{ + Name: "contract, c", + Usage: "Contract hash", + }, + }, options.RPC...), + }, { Name: "remove", Usage: "remove an account from the wallet", @@ -390,7 +405,7 @@ func importMultisig(ctx *cli.Context) error { return nil } -func importWallet(ctx *cli.Context) error { +func importDeployed(ctx *cli.Context) error { wall, err := openWallet(ctx.String("wallet")) if err != nil { return cli.NewExitError(err, 1) @@ -398,6 +413,56 @@ func importWallet(ctx *cli.Context) error { defer wall.Close() + rawHash := strings.TrimPrefix("0x", ctx.String("contract")) + h, err := util.Uint160DecodeStringLE(rawHash) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid contract hash: %w", err), 1) + } + + acc, err := newAccountFromWIF(ctx.String("wif")) + if err != nil { + return cli.NewExitError(err, 1) + } + + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + + c, err := options.GetRPCClient(gctx, ctx) + if err != nil { + return err + } + + cs, err := c.GetContractState(h) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't fetch contract info: %w", err), 1) + } + md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify) + if md == nil { + return cli.NewExitError("contract has no `verify` method", 1) + } + acc.Contract.Script = cs.Script + for _, p := range md.Parameters { + acc.Contract.Parameters = append(acc.Contract.Parameters, wallet.ContractParam{ + Name: p.Name, + Type: p.Type, + }) + } + acc.Contract.Deployed = true + + if err := addAccountAndSave(wall, acc); err != nil { + return cli.NewExitError(err, 1) + } + + return nil +} + +func importWallet(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("wallet")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + acc, err := newAccountFromWIF(ctx.String("wif")) if err != nil { return cli.NewExitError(err, 1) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 36a2f36c5..bb0032d14 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -226,24 +226,7 @@ func TestCreateBasicChain(t *testing.T) { require.NoError(t, err) // Push some contract into the chain. - name := prefix + "test_contract.go" - c, err := ioutil.ReadFile(name) - require.NoError(t, err) - avm, di, err := compiler.CompileWithDebugInfo(name, bytes.NewReader(c)) - require.NoError(t, err) - t.Logf("contractHash: %s", hash.Hash160(avm).StringLE()) - - script := io.NewBufBinWriter() - m, err := di.ConvertToManifest(smartcontract.HasStorage, nil) - require.NoError(t, err) - bs, err := m.MarshalJSON() - require.NoError(t, err) - emit.Bytes(script.BinWriter, bs) - emit.Bytes(script.BinWriter, avm) - emit.Syscall(script.BinWriter, interopnames.SystemContractCreate) - txScript := script.Bytes() - - txDeploy := transaction.New(testchain.Network(), txScript, 100*native.GASFactor) + txDeploy, avm := newDeployTx(t, prefix+"test_contract.go") txDeploy.Nonce = getNextNonce() txDeploy.ValidUntilBlock = validUntilBlock txDeploy.Signers = []transaction.Signer{{Account: priv0ScriptHash}} @@ -255,7 +238,7 @@ func TestCreateBasicChain(t *testing.T) { t.Logf("Block2 hash: %s", b.Hash().StringLE()) // Now invoke this contract. - script = io.NewBufBinWriter() + script := io.NewBufBinWriter() emit.AppCallWithOperationAndArgs(script.BinWriter, hash.Hash160(avm), "putValue", "testkey", "testvalue") txInv := transaction.New(testchain.Network(), script.Bytes(), 1*native.GASFactor) @@ -330,6 +313,16 @@ func TestCreateBasicChain(t *testing.T) { require.NoError(t, bc.AddBlock(b)) t.Logf("sendRublesTx: %v", transferTx.Hash().StringLE()) + // Push verification contract into the chain. + txDeploy2, _ := newDeployTx(t, prefix+"verification_contract.go") + txDeploy2.Nonce = getNextNonce() + txDeploy2.ValidUntilBlock = validUntilBlock + txDeploy2.Signers = []transaction.Signer{{Account: priv0ScriptHash}} + require.NoError(t, addNetworkFee(bc, txDeploy2, acc0)) + require.NoError(t, acc0.SignTx(txDeploy2)) + b = bc.newBlock(txDeploy2) + require.NoError(t, bc.AddBlock(b)) + if saveChain { outStream, err := os.Create(prefix + "testblocks.acc") require.NoError(t, err) @@ -378,6 +371,27 @@ func newNEP5Transfer(sc, from, to util.Uint160, amount int64) *transaction.Trans return transaction.New(testchain.Network(), script, 10000000) } +func newDeployTx(t *testing.T, name string) (*transaction.Transaction, []byte) { + c, err := ioutil.ReadFile(name) + require.NoError(t, err) + avm, di, err := compiler.CompileWithDebugInfo(name, bytes.NewReader(c)) + require.NoError(t, err) + t.Logf("contractHash (%s): %s", name, hash.Hash160(avm).StringLE()) + t.Logf("contractScript: %x", avm) + + script := io.NewBufBinWriter() + m, err := di.ConvertToManifest(smartcontract.HasStorage, nil) + require.NoError(t, err) + bs, err := m.MarshalJSON() + require.NoError(t, err) + emit.Bytes(script.BinWriter, bs) + emit.Bytes(script.BinWriter, avm) + emit.Syscall(script.BinWriter, interopnames.SystemContractCreate) + txScript := script.Bytes() + + return transaction.New(testchain.Network(), txScript, 100*native.GASFactor), avm +} + func addSigners(txs ...*transaction.Transaction) { for _, tx := range txs { tx.Signers = []transaction.Signer{{ diff --git a/pkg/core/interop/runtime/witness.go b/pkg/core/interop/runtime/witness.go index c06413a8c..d721990c5 100644 --- a/pkg/core/interop/runtime/witness.go +++ b/pkg/core/interop/runtime/witness.go @@ -9,6 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop" "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/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" ) @@ -20,6 +21,9 @@ func CheckHashedWitness(ic *interop.Context, hash util.Uint160) (bool, error) { return checkScope(ic.DAO, tx, ic.VM, hash) } + if !ic.VM.Context().GetCallFlags().Has(smartcontract.AllowStates) { + return false, errors.New("missing AllowStates call flag") + } return false, errors.New("script container is not a transaction") } @@ -49,6 +53,9 @@ func checkScope(d dao.DAO, tx *transaction.Transaction, v *vm.VM, hash util.Uint if callingScriptHash.Equals(util.Uint160{}) { return false, nil } + if !v.Context().GetCallFlags().Has(smartcontract.AllowStates) { + return false, errors.New("missing AllowStates call flag") + } cs, err := d.GetContractState(callingScriptHash) if err != nil { return false, err diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 7d2a00e1d..4c6ab5af8 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -77,7 +77,7 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemJSONDeserialize, Func: json.Deserialize, Price: 500000, ParamCount: 1}, {Name: interopnames.SystemJSONSerialize, Func: json.Serialize, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemRuntimeCheckWitness, Func: runtime.CheckWitness, Price: 30000, - RequiredFlags: smartcontract.AllowStates, ParamCount: 1}, + RequiredFlags: smartcontract.NoneFlag, ParamCount: 1}, {Name: interopnames.SystemRuntimeGasLeft, Func: runtime.GasLeft, Price: 400}, {Name: interopnames.SystemRuntimeGetCallingScriptHash, Func: engineGetCallingScriptHash, Price: 400}, {Name: interopnames.SystemRuntimeGetEntryScriptHash, Func: engineGetEntryScriptHash, Price: 400}, diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index f45363f6e..fb6146439 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpc/request" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -499,16 +500,19 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs } size := io.GetVarSize(tx) for i, cosigner := range tx.Signers { - if accs[i].Contract.Script == nil { - contract, err := c.GetContractState(cosigner.Account) - if err == nil { - if contract == nil { - continue + if accs[i].Contract.Deployed { + res, err := c.InvokeFunction(cosigner.Account, manifest.MethodVerify, []smartcontract.Parameter{}, tx.Signers) + if err == nil && res.State == "HALT" && len(res.Stack) == 1 { + r, err := topIntFromStack(res.Stack) + if err != nil || r == 0 { + return core.ErrVerificationFailed } - netFee, sizeDelta := core.CalculateNetworkFee(contract.Script) - tx.NetworkFee += netFee - size += sizeDelta + } else { + return core.ErrVerificationFailed } + tx.NetworkFee += res.GasConsumed + size += io.GetVarSize([]byte{}) * 2 // both scripts are empty + continue } netFee, sizeDelta := core.CalculateNetworkFee(accs[i].Contract.Script) tx.NetworkFee += netFee diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index 91f70fa19..9ad40480d 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/hex" "testing" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -132,6 +133,65 @@ func TestAddNetworkFee(t *testing.T) { cFeeM, _ := core.CalculateNetworkFee(accs[1].Contract.Script) require.Equal(t, int64(io.GetVarSize(tx))*feePerByte+cFee+cFeeM+10, tx.NetworkFee) }) + t.Run("Contract", func(t *testing.T) { + tx := transaction.New(testchain.Network(), []byte{byte(opcode.PUSH1)}, 0) + priv := testchain.PrivateKeyByID(0) + acc1, err := wallet.NewAccountFromWIF(priv.WIF()) + require.NoError(t, err) + acc1.Contract.Deployed = true + acc1.Contract.Script, _ = hex.DecodeString(verifyContractAVM) + h, _ := util.Uint160DecodeStringLE(verifyContractHash) + tx.ValidUntilBlock = chain.BlockHeight() + 10 + + t.Run("Valid", func(t *testing.T) { + acc0, err := wallet.NewAccountFromWIF(priv.WIF()) + require.NoError(t, err) + tx.Signers = []transaction.Signer{ + { + Account: acc0.PrivateKey().GetScriptHash(), + Scopes: transaction.CalledByEntry, + }, + { + Account: h, + Scopes: transaction.Global, + }, + } + require.NoError(t, c.AddNetworkFee(tx, 10, acc0, acc1)) + require.NoError(t, acc0.SignTx(tx)) + tx.Scripts = append(tx.Scripts, transaction.Witness{}) + require.NoError(t, chain.VerifyTx(tx)) + }) + t.Run("Invalid", func(t *testing.T) { + acc0, err := wallet.NewAccount() + require.NoError(t, err) + tx.Signers = []transaction.Signer{ + { + Account: acc0.PrivateKey().GetScriptHash(), + Scopes: transaction.CalledByEntry, + }, + { + Account: h, + Scopes: transaction.Global, + }, + } + require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) + }) + t.Run("InvalidContract", func(t *testing.T) { + acc0, err := wallet.NewAccountFromWIF(priv.WIF()) + require.NoError(t, err) + tx.Signers = []transaction.Signer{ + { + Account: acc0.PrivateKey().GetScriptHash(), + Scopes: transaction.CalledByEntry, + }, + { + Account: util.Uint160{}, + Scopes: transaction.Global, + }, + } + require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) + }) + }) } func TestSignAndPushInvocationTx(t *testing.T) { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index eb7cdd273..a77fcb9cf 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -51,8 +51,11 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } -const testContractHash = "93c4983afe01a75f74c1e56011bd630e9d8cc755" -const deploymentTxHash = "583cf0e49d69d8854869efc3e97ad741061da478292a7280580789351a39a1ac" +const testContractHash = "4546ec6fcdaa1c3ccdb048526b78624b457b60a4" +const deploymentTxHash = "17be1bbb0fdecae18cd4c6a2db19311f47bd540371e2ea479a46b349a66aa0b3" + +const verifyContractHash = "47ef649f9a77cad161ddaa28b39c7e450e5429e7" +const verifyContractAVM = "560340570300412d510830db4121700c14aa8acf859d4fe402b34e673f2156821796a488ebdb30716813cedb2869db289740" var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { @@ -438,7 +441,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) expected := result.UnclaimedGas{ Address: testchain.MultisigScriptHash(), - Unclaimed: *big.NewInt(36000), + Unclaimed: *big.NewInt(42000), } assert.Equal(t, expected, *actual) }, @@ -814,7 +817,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoErrorf(t, err, "could not parse response: %s", txOut) assert.Equal(t, *block.Transactions[0], actual.Transaction) - assert.Equal(t, 8, actual.Confirmations) + assert.Equal(t, 9, actual.Confirmations) assert.Equal(t, TXHash, actual.Transaction.Hash()) }) @@ -999,8 +1002,8 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "915.61059740", - LastUpdated: 6, + Amount: "815.59478530", + LastUpdated: 7, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), } diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index baacf2786..decc2c137 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/rpc/server/testdata/verification_contract.go b/pkg/rpc/server/testdata/verification_contract.go new file mode 100644 index 000000000..8ebad6985 --- /dev/null +++ b/pkg/rpc/server/testdata/verification_contract.go @@ -0,0 +1,15 @@ +package testdata + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/convert" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +// Verify is a verification contract method. +// It returns true iff it is signed by NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc (id-0 private key from testchain). +func Verify() bool { + tx := runtime.GetScriptContainer() + addr := util.FromAddress("NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc") + return util.Equals(convert.ToByteArray(tx.Sender), convert.ToByteArray(addr)) +}