diff --git a/cli/contract_test.go b/cli/contract_test.go index 7781f603a..3289e907e 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -10,9 +10,7 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/config" - "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" @@ -58,7 +56,7 @@ func TestComlileAndInvokeFunction(t *testing.T) { res := new(result.Invoke) require.NoError(t, json.Unmarshal(e.Out.Bytes(), res)) - require.Equal(t, vm.HaltState.String(), res.State) + require.Equal(t, vm.HaltState.String(), res.State, res.FaultException) require.Len(t, res.Stack, 1) require.Equal(t, []byte("on create|sub create"), res.Stack[0].Value()) @@ -77,8 +75,6 @@ func TestComlileAndInvokeFunction(t *testing.T) { rawNef, err := ioutil.ReadFile(nefName) require.NoError(t, err) - realNef, err := nef.FileFromBytes(rawNef) - require.NoError(t, err) rawManifest, err := ioutil.ReadFile(manifestName) require.NoError(t, err) @@ -87,7 +83,7 @@ func TestComlileAndInvokeFunction(t *testing.T) { "--rpc-endpoint", "http://"+e.RPC.Addr, "--wallet", validatorWallet, "--address", validatorAddr, h.StringLE(), "update", - "bytes:"+hex.EncodeToString(realNef.Script), + "bytes:"+hex.EncodeToString(rawNef), "bytes:"+hex.EncodeToString(rawManifest), ) e.checkTxPersisted(t, "Sent invocation transaction ") @@ -95,7 +91,7 @@ func TestComlileAndInvokeFunction(t *testing.T) { e.In.WriteString("one\r") e.Run(t, "neo-go", "contract", "testinvokefunction", "--rpc-endpoint", "http://"+e.RPC.Addr, - hash.Hash160(realNef.Script).StringLE(), "getValue") + h.StringLE(), "getValue") res := new(result.Invoke) require.NoError(t, json.Unmarshal(e.Out.Bytes(), res)) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 0e35439cb..e8555d0b0 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpc/request" @@ -734,13 +735,18 @@ func contractDeploy(ctx *cli.Context) error { if err != nil { return err } + sender, err := address.StringToUint160(acc.Address) + if err != nil { + return cli.NewExitError(err, 1) + } f, err := ioutil.ReadFile(in) if err != nil { return cli.NewExitError(err, 1) } + // Check the file. nefFile, err := nef.FileFromBytes(f) if err != nil { - return cli.NewExitError(fmt.Errorf("failed to restore .nef file: %w", err), 1) + return cli.NewExitError(fmt.Errorf("failed to read .nef file: %w", err), 1) } manifestBytes, err := ioutil.ReadFile(manifestFile) @@ -761,7 +767,7 @@ func contractDeploy(ctx *cli.Context) error { return err } - txScript, err := request.CreateDeploymentScript(nefFile.Script, m) + txScript, err := request.CreateDeploymentScript(f, m) if err != nil { return cli.NewExitError(fmt.Errorf("failed to create deployment script: %w", err), 1) } @@ -775,7 +781,8 @@ func contractDeploy(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) } - fmt.Fprintf(ctx.App.Writer, "Contract: %s\n", nefFile.Header.ScriptHash.StringLE()) + hash := state.CreateContractHash(sender, nefFile.Script) + fmt.Fprintf(ctx.App.Writer, "Contract: %s\n", hash.StringLE()) fmt.Fprintln(ctx.App.Writer, txHash.StringLE()) return nil } diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index a494a1165..6ab5eaa38 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -437,7 +437,7 @@ func importDeployed(ctx *cli.Context) error { if md == nil { return cli.NewExitError("contract has no `verify` method", 1) } - acc.Address = address.Uint160ToString(cs.ScriptHash()) + acc.Address = address.Uint160ToString(cs.Hash) acc.Contract.Script = cs.Script acc.Contract.Parameters = acc.Contract.Parameters[:0] for _, p := range md.Parameters { diff --git a/internal/testchain/transaction.go b/internal/testchain/transaction.go index d2a37a134..02bf32791 100644 --- a/internal/testchain/transaction.go +++ b/internal/testchain/transaction.go @@ -5,13 +5,16 @@ import ( gio "io" "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -46,28 +49,46 @@ func NewTransferFromOwner(bc blockchainer.Blockchainer, contractHash, to util.Ui } // NewDeployTx returns new deployment tx for contract with name with Go code read from r. -func NewDeployTx(name string, r gio.Reader) (*transaction.Transaction, []byte, error) { +func NewDeployTx(name string, sender util.Uint160, r gio.Reader) (*transaction.Transaction, util.Uint160, error) { + // nef.NewFile() cares about version a lot. + config.Version = "0.90.0-test" + avm, di, err := compiler.CompileWithDebugInfo(name, r) if err != nil { - return nil, nil, err + return nil, util.Uint160{}, err + } + + ne, err := nef.NewFile(avm) + if err != nil { + return nil, util.Uint160{}, err + } + neb, err := ne.Bytes() + if err != nil { + return nil, util.Uint160{}, err } - w := io.NewBufBinWriter() m, err := di.ConvertToManifest(name, nil) if err != nil { - return nil, nil, err + return nil, util.Uint160{}, err } bs, err := json.Marshal(m) if err != nil { - return nil, nil, err + return nil, util.Uint160{}, err } + + w := io.NewBufBinWriter() emit.Bytes(w.BinWriter, bs) - emit.Bytes(w.BinWriter, avm) + emit.Bytes(w.BinWriter, neb) emit.Syscall(w.BinWriter, interopnames.SystemContractCreate) if w.Err != nil { - return nil, nil, err + return nil, util.Uint160{}, w.Err } - return transaction.New(Network(), w.Bytes(), 100*native.GASFactor), avm, nil + txScript := w.Bytes() + tx := transaction.New(Network(), txScript, 100*native.GASFactor) + tx.Signers = []transaction.Signer{{Account: sender}} + h := state.CreateContractHash(tx.Sender(), avm) + + return tx, h, nil } // SignTx signs provided transactions with validator keys. diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 7150bfb01..a87bcbfbe 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -107,6 +107,7 @@ func TestAppCall(t *testing.T) { ih := hash.Hash160(inner) ic := interop.NewContext(trigger.Application, nil, dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false), nil, nil, nil, zaptest.NewLogger(t)) require.NoError(t, ic.DAO.PutContractState(&state.Contract{ + Hash: ih, Script: inner, Manifest: *m, })) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 6e27cc3b2..b243f9385 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -940,7 +940,7 @@ func TestVerifyHashAgainstScript(t *testing.T) { 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 := cs.Hash newH[0] = ^newH[0] w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} _, err := bc.verifyHashAgainstScript(newH, w, ic, gas) @@ -948,17 +948,17 @@ func TestVerifyHashAgainstScript(t *testing.T) { }) t.Run("Invalid", func(t *testing.T) { w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}} - _, err := bc.verifyHashAgainstScript(csInvalid.ScriptHash(), w, ic, gas) + _, err := bc.verifyHashAgainstScript(csInvalid.Hash, w, ic, 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, gas) + _, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, 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, gas) + _, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas) require.True(t, errors.Is(err, ErrVerificationFailed)) }) }) @@ -1071,7 +1071,7 @@ func TestIsTxStillRelevant(t *testing.T) { currentHeight := blockchain.GetHeight() return currentHeight < %d }`, bc.BlockHeight()+2) // deploy + next block - txDeploy, avm, err := testchain.NewDeployTx("TestVerify", strings.NewReader(src)) + txDeploy, h, err := testchain.NewDeployTx("TestVerify", neoOwner, strings.NewReader(src)) require.NoError(t, err) txDeploy.ValidUntilBlock = bc.BlockHeight() + 1 addSigners(txDeploy) @@ -1080,7 +1080,7 @@ func TestIsTxStillRelevant(t *testing.T) { tx := newTx(t) tx.Signers = append(tx.Signers, transaction.Signer{ - Account: hash.Hash160(avm), + Account: h, Scopes: transaction.None, }) tx.NetworkFee += 1_000_000 diff --git a/pkg/core/dao/cacheddao.go b/pkg/core/dao/cacheddao.go index a11727e88..037dc64a5 100644 --- a/pkg/core/dao/cacheddao.go +++ b/pkg/core/dao/cacheddao.go @@ -42,7 +42,7 @@ func (cd *Cached) GetContractState(hash util.Uint160) (*state.Contract, error) { // PutContractState puts given contract state into the given store. func (cd *Cached) PutContractState(cs *state.Contract) error { - cd.contracts[cs.ScriptHash()] = cs + cd.contracts[cs.Hash] = cs return cd.DAO.PutContractState(cs) } diff --git a/pkg/core/dao/cacheddao_test.go b/pkg/core/dao/cacheddao_test.go index d54f2ea89..f38987e85 100644 --- a/pkg/core/dao/cacheddao_test.go +++ b/pkg/core/dao/cacheddao_test.go @@ -27,6 +27,7 @@ func TestCachedDaoContracts(t *testing.T) { cs := &state.Contract{ ID: 123, + Hash: sh, Script: script, Manifest: *m, } diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 48ae63b95..b1fb35a7d 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -136,20 +136,23 @@ func (dao *Simple) GetContractState(hash util.Uint160) (*state.Contract, error) if err != nil { return nil, err } - if contract.ScriptHash() != hash { - return nil, fmt.Errorf("found script hash is not equal to expected") - } return contract, nil } // PutContractState puts given contract state into the given store. func (dao *Simple) PutContractState(cs *state.Contract) error { - key := storage.AppendPrefix(storage.STContract, cs.ScriptHash().BytesBE()) + key := storage.AppendPrefix(storage.STContract, cs.Hash.BytesBE()) if err := dao.Put(cs, key); err != nil { return err } - return dao.putContractScriptHash(cs) + if cs.UpdateCounter != 0 { // Update. + return nil + } + key = key[:5] + key[0] = byte(storage.STContractID) + binary.LittleEndian.PutUint32(key[1:], uint32(cs.ID)) + return dao.Store.Put(key, cs.Hash.BytesBE()) } // DeleteContractState deletes given contract state in the given store. @@ -173,16 +176,6 @@ func (dao *Simple) GetAndUpdateNextContractID() (int32, error) { return id, dao.Store.Put(key, data) } -// putContractScriptHash puts given contract script hash into the given store. -// It's a private method because it should be used after PutContractState to keep -// ID-Hash pair always up-to-date. -func (dao *Simple) putContractScriptHash(cs *state.Contract) error { - key := make([]byte, 5) - key[0] = byte(storage.STContractID) - binary.LittleEndian.PutUint32(key[1:], uint32(cs.ID)) - return dao.Store.Put(key, cs.ScriptHash().BytesBE()) -} - // GetContractScriptHash returns script hash of the contract with the specified ID. // Contract with the script hash may be destroyed. func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) { diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 9a583084e..a1a022eea 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/state" "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/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" @@ -44,24 +45,26 @@ func (t *TestSerializable) DecodeBinary(reader *io.BinReader) { func TestPutAndGetContractState(t *testing.T) { dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) - contractState := &state.Contract{Script: []byte{}} - hash := contractState.ScriptHash() + script := []byte{} + h := hash.Hash160(script) + contractState := &state.Contract{Hash: h, Script: script} err := dao.PutContractState(contractState) require.NoError(t, err) - gotContractState, err := dao.GetContractState(hash) + gotContractState, err := dao.GetContractState(contractState.Hash) require.NoError(t, err) require.Equal(t, contractState, gotContractState) } func TestDeleteContractState(t *testing.T) { dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) - contractState := &state.Contract{Script: []byte{}} - hash := contractState.ScriptHash() + script := []byte{} + h := hash.Hash160(script) + contractState := &state.Contract{Hash: h, Script: script} err := dao.PutContractState(contractState) require.NoError(t, err) - err = dao.DeleteContractState(hash) + err = dao.DeleteContractState(h) require.NoError(t, err) - gotContractState, err := dao.GetContractState(hash) + gotContractState, err := dao.GetContractState(h) require.Error(t, err) require.Nil(t, gotContractState) } diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 796705a07..91dfcbeca 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -272,10 +272,9 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, err) // Push some contract into the chain. - txDeploy, avm := newDeployTx(t, prefix+"test_contract.go", "Rubl") + txDeploy, cHash := newDeployTx(t, priv0ScriptHash, prefix+"test_contract.go", "Rubl") txDeploy.Nonce = getNextNonce() txDeploy.ValidUntilBlock = validUntilBlock - txDeploy.Signers = []transaction.Signer{{Account: priv0ScriptHash}} require.NoError(t, addNetworkFee(bc, txDeploy, acc0)) require.NoError(t, acc0.SignTx(txDeploy)) b = bc.newBlock(txDeploy) @@ -285,7 +284,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { // Now invoke this contract. script := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(script.BinWriter, hash.Hash160(avm), "putValue", "testkey", "testvalue") + emit.AppCallWithOperationAndArgs(script.BinWriter, cHash, "putValue", "testkey", "testvalue") txInv := transaction.New(testchain.Network(), script.Bytes(), 1*native.GASFactor) txInv.Nonce = getNextNonce() @@ -314,16 +313,15 @@ func initBasicChain(t *testing.T, bc *Blockchain) { b = bc.newBlock(txNeo0to1) require.NoError(t, bc.AddBlock(b)) - sh := hash.Hash160(avm) w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, sh, "init") + emit.AppCallWithOperationAndArgs(w.BinWriter, cHash, "init") initTx := transaction.New(testchain.Network(), w.Bytes(), 1*native.GASFactor) initTx.Nonce = getNextNonce() initTx.ValidUntilBlock = validUntilBlock initTx.Signers = []transaction.Signer{{Account: priv0ScriptHash}} require.NoError(t, addNetworkFee(bc, initTx, acc0)) require.NoError(t, acc0.SignTx(initTx)) - transferTx := newNEP17Transfer(sh, sh, priv0.GetScriptHash(), 1000) + transferTx := newNEP17Transfer(cHash, cHash, priv0.GetScriptHash(), 1000) transferTx.Nonce = getNextNonce() transferTx.ValidUntilBlock = validUntilBlock transferTx.Signers = []transaction.Signer{ @@ -341,7 +339,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, bc.AddBlock(b)) t.Logf("recieveRublesTx: %v", transferTx.Hash().StringLE()) - transferTx = newNEP17Transfer(sh, priv0.GetScriptHash(), priv1.GetScriptHash(), 123) + transferTx = newNEP17Transfer(cHash, priv0.GetScriptHash(), priv1.GetScriptHash(), 123) transferTx.Nonce = getNextNonce() transferTx.ValidUntilBlock = validUntilBlock transferTx.Signers = []transaction.Signer{ @@ -360,10 +358,9 @@ func initBasicChain(t *testing.T, bc *Blockchain) { t.Logf("sendRublesTx: %v", transferTx.Hash().StringLE()) // Push verification contract into the chain. - txDeploy2, _ := newDeployTx(t, prefix+"verification_contract.go", "Verify") + txDeploy2, _ := newDeployTx(t, priv0ScriptHash, prefix+"verification_contract.go", "Verify") 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) @@ -379,15 +376,13 @@ func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs .. return transaction.New(testchain.Network(), script, 10000000) } -func newDeployTx(t *testing.T, name, ctrName string) (*transaction.Transaction, []byte) { +func newDeployTx(t *testing.T, sender util.Uint160, name, ctrName string) (*transaction.Transaction, util.Uint160) { c, err := ioutil.ReadFile(name) require.NoError(t, err) - tx, avm, err := testchain.NewDeployTx(ctrName, bytes.NewReader(c)) + tx, h, err := testchain.NewDeployTx(ctrName, sender, bytes.NewReader(c)) require.NoError(t, err) - t.Logf("contractHash (%s): %s", name, hash.Hash160(avm).StringLE()) - t.Logf("contractScript: %x", avm) - - return tx, avm + t.Logf("contractHash (%s): %s", name, h.StringLE()) + return tx, h } func addSigners(txs ...*transaction.Transaction) { diff --git a/pkg/core/interop/callback/method.go b/pkg/core/interop/callback/method.go index 78947c3a1..ada8e2d41 100644 --- a/pkg/core/interop/callback/method.go +++ b/pkg/core/interop/callback/method.go @@ -29,7 +29,7 @@ func (s *MethodCallback) ArgCount() int { func (s *MethodCallback) LoadContext(v *vm.VM, args []stackitem.Item) { v.Estack().PushVal(args) v.Estack().PushVal(s.method.Name) - v.Estack().PushVal(s.contract.ScriptHash().BytesBE()) + v.Estack().PushVal(s.contract.Hash.BytesBE()) } // CreateFromMethod creates callback for a contract method. diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index b8f7a8bd0..08f064c37 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -67,12 +67,11 @@ func CallExInternal(ic *interop.Context, cs *state.Contract, return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters)) } - u := cs.ScriptHash() - ic.VM.Invocations[u]++ - ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f) + ic.VM.Invocations[cs.Hash]++ + ic.VM.LoadScriptWithHash(cs.Script, cs.Hash, ic.VM.Context().GetCallFlags()&f) var isNative bool for i := range ic.Natives { - if ic.Natives[i].Metadata().Hash.Equals(u) { + if ic.Natives[i].Metadata().Hash.Equals(cs.Hash) { isNative = true break } diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 446ebfbcc..47d643731 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -6,15 +6,16 @@ import ( "encoding/json" "errors" "fmt" + "math" "sort" "github.com/mr-tron/base58" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/state" - "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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/nef" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -60,39 +61,59 @@ func storageFind(ic *interop.Context) error { return nil } -// createContractStateFromVM pops all contract state elements from the VM -// evaluation stack, does a lot of checks and returns Contract if it -// succeeds. -func createContractStateFromVM(ic *interop.Context) (*state.Contract, error) { - script := ic.VM.Estack().Pop().Bytes() - if len(script) > MaxContractScriptSize { - return nil, errors.New("the script is too big") +// getNefAndManifestFromVM pops NEF and manifest from the VM's evaluation stack, +// does a lot of checks and returns deserialized structures if succeeds. +func getNefAndManifestFromVM(v *vm.VM) (*nef.File, *manifest.Manifest, error) { + // Always pop both elements. + nefBytes := v.Estack().Pop().BytesOrNil() + manifestBytes := v.Estack().Pop().BytesOrNil() + + if err := checkNonEmpty(nefBytes, math.MaxInt32); err != nil { // Upper limits are checked during NEF deserialization. + return nil, nil, fmt.Errorf("invalid NEF file: %w", err) } - manifestBytes := ic.VM.Estack().Pop().Bytes() - if len(manifestBytes) > manifest.MaxManifestSize { - return nil, errors.New("manifest is too big") + if err := checkNonEmpty(manifestBytes, manifest.MaxManifestSize); err != nil { + return nil, nil, fmt.Errorf("invalid manifest: %w", err) } - if !ic.VM.AddGas(int64(StoragePrice * (len(script) + len(manifestBytes)))) { - return nil, errGasLimitExceeded + + if !v.AddGas(int64(StoragePrice * (len(nefBytes) + len(manifestBytes)))) { + return nil, nil, errGasLimitExceeded } - var m manifest.Manifest - err := json.Unmarshal(manifestBytes, &m) - if err != nil { - return nil, fmt.Errorf("unable to retrieve manifest from stack: %w", err) + var resManifest *manifest.Manifest + var resNef *nef.File + if nefBytes != nil { + nf, err := nef.FileFromBytes(nefBytes) + if err != nil { + return nil, nil, fmt.Errorf("invalid NEF file: %w", err) + } + resNef = &nf } - return &state.Contract{ - Script: script, - Manifest: m, - }, nil + if manifestBytes != nil { + resManifest = new(manifest.Manifest) + err := json.Unmarshal(manifestBytes, resManifest) + if err != nil { + return nil, nil, fmt.Errorf("invalid manifest: %w", err) + } + } + return resNef, resManifest, nil } // contractCreate creates a contract. func contractCreate(ic *interop.Context) error { - newcontract, err := createContractStateFromVM(ic) + neff, manif, err := getNefAndManifestFromVM(ic.VM) if err != nil { return err } - contract, err := ic.DAO.GetContractState(newcontract.ScriptHash()) + if neff == nil { + return errors.New("no valid NEF provided") + } + if manif == nil { + return errors.New("no valid manifest provided") + } + if ic.Tx == nil { + return errors.New("no transaction provided") + } + h := state.CreateContractHash(ic.Tx.Sender(), neff.Script) + contract, err := ic.DAO.GetContractState(h) if contract != nil && err == nil { return errors.New("contract already exists") } @@ -100,10 +121,15 @@ func contractCreate(ic *interop.Context) error { if err != nil { return err } - newcontract.ID = id - if !newcontract.Manifest.IsValid(newcontract.ScriptHash()) { + if !manif.IsValid(h) { return errors.New("failed to check contract script hash against manifest") } + newcontract := &state.Contract{ + ID: id, + Hash: h, + Script: neff.Script, + Manifest: *manif, + } if err := ic.DAO.PutContractState(newcontract); err != nil { return err } @@ -129,64 +155,33 @@ func checkNonEmpty(b []byte, max int) error { // contractUpdate migrates a contract. This method assumes that Manifest and Script // of the contract can be updated independently. func contractUpdate(ic *interop.Context) error { + neff, manif, err := getNefAndManifestFromVM(ic.VM) + if err != nil { + return err + } + if neff == nil && manif == nil { + return errors.New("both NEF and manifest are nil") + } contract, _ := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) if contract == nil { return errors.New("contract doesn't exist") } - script := ic.VM.Estack().Pop().BytesOrNil() - manifestBytes := ic.VM.Estack().Pop().BytesOrNil() - if script == nil && manifestBytes == nil { - return errors.New("both script and manifest are nil") + // if NEF was provided, update the contract script + if neff != nil { + contract.Script = neff.Script } - if err := checkNonEmpty(script, MaxContractScriptSize); err != nil { - return fmt.Errorf("invalid script size: %w", err) - } - if err := checkNonEmpty(manifestBytes, manifest.MaxManifestSize); err != nil { - return fmt.Errorf("invalid manifest size: %w", err) - } - if !ic.VM.AddGas(int64(StoragePrice * (len(script) + len(manifestBytes)))) { - return errGasLimitExceeded - } - // if script was provided, update the old contract script and Manifest.ABI hash - if script != nil { - newHash := hash.Hash160(script) - if newHash.Equals(contract.ScriptHash()) { - return errors.New("the script is the same") - } else if _, err := ic.DAO.GetContractState(newHash); err == nil { - return errors.New("contract already exists") - } - oldHash := contract.ScriptHash() - // re-write existing contract variable, as we need it to be up-to-date during manifest update - contract = &state.Contract{ - ID: contract.ID, - Script: script, - Manifest: contract.Manifest, - } - if err := ic.DAO.PutContractState(contract); err != nil { - return fmt.Errorf("failed to update script: %w", err) - } - if err := ic.DAO.DeleteContractState(oldHash); err != nil { - return fmt.Errorf("failed to update script: %w", err) - } - } - // if manifest was provided, update the old contract manifest and check associated - // storage items if needed - if manifestBytes != nil { - var newManifest manifest.Manifest - err := json.Unmarshal(manifestBytes, &newManifest) - if err != nil { - return fmt.Errorf("unable to retrieve manifest from stack: %w", err) - } - // we don't have to perform `GetContractState` one more time as it's already up-to-date - contract.Manifest = newManifest - if !contract.Manifest.IsValid(contract.ScriptHash()) { + // if manifest was provided, update the contract manifest + if manif != nil { + contract.Manifest = *manif + if !contract.Manifest.IsValid(contract.Hash) { return errors.New("failed to check contract script hash against new manifest") } - if err := ic.DAO.PutContractState(contract); err != nil { - return fmt.Errorf("failed to update manifest: %w", err) - } } + contract.UpdateCounter++ + if err := ic.DAO.PutContractState(contract); err != nil { + return fmt.Errorf("failed to update contract: %w", err) + } return callDeploy(ic, contract, true) } diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index d89c9485e..1eb23d3c9 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/state" "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/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" @@ -296,6 +297,7 @@ func createVMAndContractState(t *testing.T) (*vm.VM, *state.Contract, *interop.C m := manifest.NewManifest("Test") contractState := &state.Contract{ Script: script, + Hash: hash.Hash160(script), Manifest: *m, ID: 123, } diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 31822df6a..da9a36e2f 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -105,6 +105,9 @@ func contractToStackItem(cs *state.Contract) (stackitem.Item, error) { return nil, err } return stackitem.NewArray([]stackitem.Item{ + stackitem.Make(cs.ID), + stackitem.Make(cs.UpdateCounter), + stackitem.NewByteArray(cs.Hash.BytesBE()), stackitem.NewByteArray(cs.Script), stackitem.NewByteArray(manifest), }), nil diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index dc33fb969..11e80b7e2 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -8,6 +8,7 @@ import ( "github.com/nspcc-dev/dbft/crypto" "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/callback" @@ -21,6 +22,7 @@ import ( "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/nef" "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" @@ -204,7 +206,7 @@ func TestContractIsStandard(t *testing.T) { require.NoError(t, err) pub := priv.PublicKey() - err = ic.DAO.PutContractState(&state.Contract{ID: 42, Script: pub.GetVerificationScript()}) + err = ic.DAO.PutContractState(&state.Contract{ID: 42, Hash: pub.GetScriptHash(), Script: pub.GetVerificationScript()}) require.NoError(t, err) v.Estack().PushVal(pub.GetScriptHash().BytesBE()) @@ -213,7 +215,7 @@ func TestContractIsStandard(t *testing.T) { }) t.Run("contract stored, false", func(t *testing.T) { script := []byte{byte(opcode.PUSHT)} - require.NoError(t, ic.DAO.PutContractState(&state.Contract{ID: 24, Script: script})) + require.NoError(t, ic.DAO.PutContractState(&state.Contract{ID: 24, Hash: hash.Hash160(script), Script: script})) v.Estack().PushVal(crypto.Hash160(script).BytesBE()) require.NoError(t, contractIsStandard(ic)) @@ -319,7 +321,7 @@ func TestBlockchainGetContractState(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) t.Run("positive", func(t *testing.T) { - v.Estack().PushVal(cs.ScriptHash().BytesBE()) + v.Estack().PushVal(cs.Hash.BytesBE()) require.NoError(t, bcGetContract(ic)) actual := v.Estack().Pop().Item() @@ -502,6 +504,7 @@ func getTestContractState() (*state.Contract, *state.Contract) { } cs := &state.Contract{ Script: script, + Hash: h, Manifest: *m, ID: 42, } @@ -519,6 +522,7 @@ func getTestContractState() (*state.Contract, *state.Contract) { return cs, &state.Contract{ Script: currScript, + Hash: hash.Hash160(currScript), Manifest: *m, ID: 123, } @@ -654,13 +658,31 @@ func TestContractCreate(t *testing.T) { v.GasLimit = -1 defer bc.Close() + // nef.NewFile() cares about version a lot. + config.Version = "0.90.0-test" putArgsOnStack := func() { manifest, err := json.Marshal(cs.Manifest) require.NoError(t, err) + ne, err := nef.NewFile(cs.Script) + require.NoError(t, err) + neb, err := ne.Bytes() + require.NoError(t, err) v.Estack().PushVal(manifest) - v.Estack().PushVal(cs.Script) + v.Estack().PushVal(neb) } + t.Run("no tx", func(t *testing.T) { + putArgsOnStack() + + require.Error(t, contractCreate(ic)) + }) + + ic.Tx = transaction.New(netmode.UnitTestNet, []byte{1}, 0) + var sender = util.Uint160{1, 2, 3} + ic.Tx.Signers = append(ic.Tx.Signers, transaction.Signer{Account: sender}) + cs.ID = 0 + cs.Hash = state.CreateContractHash(sender, cs.Script) + t.Run("positive", func(t *testing.T) { putArgsOnStack() @@ -670,8 +692,6 @@ func TestContractCreate(t *testing.T) { }) t.Run("contract already exists", func(t *testing.T) { - cs.Script = cs.Script[:len(cs.Script)-1] - require.NoError(t, ic.DAO.PutContractState(cs)) putArgsOnStack() require.Error(t, contractCreate(ic)) @@ -685,9 +705,12 @@ func compareContractStates(t *testing.T, expected *state.Contract, actual stacki expectedManifest, err := json.Marshal(expected.Manifest) require.NoError(t, err) - require.Equal(t, 2, len(act)) - require.Equal(t, expected.Script, act[0].Value().([]byte)) - require.Equal(t, expectedManifest, act[1].Value().([]byte)) + require.Equal(t, 5, len(act)) + require.Equal(t, expected.ID, int32(act[0].Value().(*big.Int).Int64())) + require.Equal(t, expected.UpdateCounter, uint16(act[1].Value().(*big.Int).Int64())) + require.Equal(t, expected.Hash.BytesBE(), act[2].Value().([]byte)) + require.Equal(t, expected.Script, act[3].Value().([]byte)) + require.Equal(t, expectedManifest, act[4].Value().([]byte)) } func TestContractUpdate(t *testing.T) { @@ -697,12 +720,19 @@ func TestContractUpdate(t *testing.T) { putArgsOnStack := func(script, manifest interface{}) { v.Estack().PushVal(manifest) + b, ok := script.([]byte) + if ok { + ne, err := nef.NewFile(b) + require.NoError(t, err) + script, err = ne.Bytes() + require.NoError(t, err) + } v.Estack().PushVal(script) } t.Run("no args", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack(stackitem.Null{}, stackitem.Null{}) require.Error(t, contractUpdate(ic)) }) @@ -710,19 +740,20 @@ func TestContractUpdate(t *testing.T) { t.Run("no contract", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) v.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{8, 9, 7}, smartcontract.All) + putArgsOnStack([]byte{1}, stackitem.Null{}) require.Error(t, contractUpdate(ic)) }) t.Run("too large script", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack(make([]byte, MaxContractScriptSize+1), stackitem.Null{}) require.Error(t, contractUpdate(ic)) }) t.Run("too large manifest", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack(stackitem.Null{}, make([]byte, manifest.MaxManifestSize+1)) require.Error(t, contractUpdate(ic)) }) @@ -730,70 +761,43 @@ func TestContractUpdate(t *testing.T) { t.Run("gas limit exceeded", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) v.GasLimit = 0 - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack([]byte{1}, []byte{2}) require.Error(t, contractUpdate(ic)) }) - t.Run("update script, the same script", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - v.GasLimit = -1 - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) - putArgsOnStack(cs.Script, stackitem.Null{}) - - require.Error(t, contractUpdate(ic)) - }) - - t.Run("update script, already exists", func(t *testing.T) { - require.NoError(t, ic.DAO.PutContractState(cs)) - duplicateScript := []byte{byte(opcode.PUSHDATA4)} - require.NoError(t, ic.DAO.PutContractState(&state.Contract{ - ID: 95, - Script: duplicateScript, - Manifest: manifest.Manifest{ - ABI: manifest.ABI{}, - }, - })) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) - putArgsOnStack(duplicateScript, stackitem.Null{}) - - require.Error(t, contractUpdate(ic)) - }) - + v.GasLimit = -1 t.Run("update script, positive", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) t.Run("empty manifest", func(t *testing.T) { - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) newScript := []byte{9, 8, 7, 6, 5} putArgsOnStack(newScript, []byte{}) require.Error(t, contractUpdate(ic)) }) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) newScript := []byte{9, 8, 7, 6, 5} putArgsOnStack(newScript, stackitem.Null{}) require.NoError(t, contractUpdate(ic)) - // updated contract should have new scripthash - actual, err := ic.DAO.GetContractState(hash.Hash160(newScript)) + // updated contract should have the same scripthash + actual, err := ic.DAO.GetContractState(cs.Hash) require.NoError(t, err) expected := &state.Contract{ - ID: cs.ID, - Script: newScript, - Manifest: cs.Manifest, + ID: cs.ID, + UpdateCounter: 1, + Hash: cs.Hash, + Script: newScript, + Manifest: cs.Manifest, } - _ = expected.ScriptHash() require.Equal(t, expected, actual) - - // old contract should be deleted - _, err = ic.DAO.GetContractState(cs.ScriptHash()) - require.Error(t, err) }) t.Run("update manifest, bad manifest", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack(stackitem.Null{}, []byte{1, 2, 3}) require.Error(t, contractUpdate(ic)) @@ -808,30 +812,31 @@ func TestContractUpdate(t *testing.T) { require.NoError(t, err) t.Run("empty script", func(t *testing.T) { - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack([]byte{}, manifestBytes) require.Error(t, contractUpdate(ic)) }) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) putArgsOnStack(stackitem.Null{}, manifestBytes) require.NoError(t, contractUpdate(ic)) - // updated contract should have new scripthash - actual, err := ic.DAO.GetContractState(cs.ScriptHash()) + // updated contract should have old scripthash + actual, err := ic.DAO.GetContractState(cs.Hash) require.NoError(t, err) expected := &state.Contract{ - ID: cs.ID, - Script: cs.Script, - Manifest: *manifest, + ID: cs.ID, + UpdateCounter: 2, + Hash: cs.Hash, + Script: cs.Script, + Manifest: *manifest, } - _ = expected.ScriptHash() require.Equal(t, expected, actual) }) t.Run("update both script and manifest", func(t *testing.T) { require.NoError(t, ic.DAO.PutContractState(cs)) - v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.Hash, smartcontract.All) newScript := []byte{12, 13, 14} newManifest := manifest.Manifest{ ABI: manifest.ABI{}, @@ -844,19 +849,16 @@ func TestContractUpdate(t *testing.T) { require.NoError(t, contractUpdate(ic)) // updated contract should have new script and manifest - actual, err := ic.DAO.GetContractState(hash.Hash160(newScript)) + actual, err := ic.DAO.GetContractState(cs.Hash) require.NoError(t, err) expected := &state.Contract{ - ID: cs.ID, - Script: newScript, - Manifest: newManifest, + ID: cs.ID, + UpdateCounter: 3, + Hash: cs.Hash, + Script: newScript, + Manifest: newManifest, } - _ = expected.ScriptHash() require.Equal(t, expected, actual) - - // old contract should be deleted - _, err = ic.DAO.GetContractState(cs.ScriptHash()) - require.Error(t, err) }) } @@ -871,28 +873,37 @@ func TestContractCreateDeploy(t *testing.T) { rawManifest, err := json.Marshal(cs.Manifest) require.NoError(t, err) v.Estack().PushVal(rawManifest) - v.Estack().PushVal(cs.Script) + ne, err := nef.NewFile(cs.Script) + require.NoError(t, err) + b, err := ne.Bytes() + require.NoError(t, err) + v.Estack().PushVal(b) } cs, currCs := getTestContractState() + ic.Tx = transaction.New(netmode.UnitTestNet, []byte{1}, 0) + var sender = util.Uint160{1, 2, 3} + ic.Tx.Signers = append(ic.Tx.Signers, transaction.Signer{Account: sender}) v.LoadScriptWithFlags([]byte{byte(opcode.RET)}, smartcontract.All) putArgs(cs) require.NoError(t, contractCreate(ic)) require.NoError(t, ic.VM.Run()) - v.LoadScriptWithFlags(currCs.Script, smartcontract.All) + cs.Hash = state.CreateContractHash(sender, cs.Script) + v.LoadScriptWithHash(currCs.Script, cs.Hash, smartcontract.All) err := contract.CallExInternal(ic, cs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty, nil) require.NoError(t, err) require.NoError(t, v.Run()) require.Equal(t, "create", v.Estack().Pop().String()) - v.LoadScriptWithFlags(cs.Script, smartcontract.All) + v.LoadScriptWithHash(cs.Script, cs.Hash, smartcontract.All) md := cs.Manifest.ABI.GetMethod("justReturn") v.Jump(v.Context(), md.Offset) t.Run("Update", func(t *testing.T) { newCs := &state.Contract{ ID: cs.ID, + Hash: cs.Hash, Script: append(cs.Script, byte(opcode.RET)), Manifest: cs.Manifest, } @@ -900,7 +911,7 @@ func TestContractCreateDeploy(t *testing.T) { require.NoError(t, contractUpdate(ic)) require.NoError(t, v.Run()) - v.LoadScriptWithFlags(currCs.Script, smartcontract.All) + v.LoadScriptWithHash(currCs.Script, cs.Hash, smartcontract.All) err = contract.CallExInternal(ic, newCs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty, nil) require.NoError(t, err) require.NoError(t, v.Run()) @@ -1219,6 +1230,7 @@ func TestRuntimeCheckWitness(t *testing.T) { contractScriptHash := hash.Hash160(contractScript) contractState := &state.Contract{ ID: 15, + Hash: contractScriptHash, Script: contractScript, Manifest: manifest.Manifest{ Groups: []manifest.Group{{PublicKey: pk.PublicKey()}}, diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index fef3ecbc4..fd02c6340 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -20,6 +20,7 @@ func Deploy(ic *interop.Context) error { cs := &state.Contract{ ID: md.ContractID, + Hash: md.Hash, Script: md.Script, Manifest: md.Manifest, } diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 221dd35a7..2f23752bb 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -149,6 +149,7 @@ func TestNativeContract_Invoke(t *testing.T) { err := chain.dao.PutContractState(&state.Contract{ Script: tn.meta.Script, + Hash: tn.meta.Hash, Manifest: tn.meta.Manifest, }) require.NoError(t, err) @@ -221,6 +222,7 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { chain.registerNative(tn) err := chain.dao.PutContractState(&state.Contract{ + Hash: tn.meta.Hash, Script: tn.meta.Script, Manifest: tn.meta.Manifest, }) @@ -230,7 +232,7 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { require.NoError(t, chain.dao.PutContractState(cs)) t.Run("non-native, no return", func(t *testing.T) { - res, err := invokeContractMethod(chain, testSumPrice*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.ScriptHash(), "justReturn", []interface{}{}) + res, err := invokeContractMethod(chain, testSumPrice*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.Hash, "justReturn", []interface{}{}) require.NoError(t, err) checkResult(t, res, stackitem.Null{}) // simple call is done with EnsureNotEmpty }) diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 6dae37cd4..aecbf4bdd 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -313,7 +313,7 @@ func TestNEO_TransferOnPayment(t *testing.T) { require.NoError(t, bc.dao.PutContractState(cs)) const amount = 2 - tx := transferTokenFromMultisigAccount(t, bc, cs.ScriptHash(), bc.contracts.NEO.Hash, amount) + tx := transferTokenFromMultisigAccount(t, bc, cs.Hash, bc.contracts.NEO.Hash, amount) aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) require.Equal(t, vm.HaltState, aer[0].VMState) diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go index 0ece9ebed..7cea35cea 100644 --- a/pkg/core/native_oracle_test.go +++ b/pkg/core/native_oracle_test.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "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/keys" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -81,8 +82,10 @@ func getOracleContractState(h util.Uint160) *state.Contract { perm.Methods.Add("request") m.Permissions = append(m.Permissions, *perm) + script := w.Bytes() return &state.Contract{ - Script: w.Bytes(), + Script: script, + Hash: hash.Hash160(script), Manifest: *m, ID: 42, } @@ -111,7 +114,7 @@ func TestOracle_Request(t *testing.T) { gasForResponse := int64(2000_1234) var filter = "flt" userData := []byte("custom info") - txHash := putOracleRequest(t, cs.ScriptHash(), bc, "url", &filter, userData, gasForResponse) + txHash := putOracleRequest(t, cs.Hash, bc, "url", &filter, userData, gasForResponse) req, err := orc.GetRequestInternal(bc.dao, 1) require.NotNil(t, req) @@ -119,7 +122,7 @@ func TestOracle_Request(t *testing.T) { require.Equal(t, txHash, req.OriginalTxID) require.Equal(t, "url", req.URL) require.Equal(t, filter, *req.Filter) - require.Equal(t, cs.ScriptHash(), req.CallbackContract) + require.Equal(t, cs.Hash, req.CallbackContract) require.Equal(t, "handle", req.CallbackMethod) require.Equal(t, uint64(gasForResponse), req.GasForResponse) @@ -189,7 +192,7 @@ func TestOracle_Request(t *testing.T) { t.Run("ErrorOnFinish", func(t *testing.T) { const reqID = 2 - putOracleRequest(t, cs.ScriptHash(), bc, "url", nil, []byte{1, 2}, gasForResponse) + putOracleRequest(t, cs.Hash, bc, "url", nil, []byte{1, 2}, gasForResponse) _, err := orc.GetRequestInternal(bc.dao, reqID) // ensure ID is 2 require.NoError(t, err) @@ -221,14 +224,14 @@ func TestOracle_Request(t *testing.T) { require.Equal(t, vm.FaultState, aer[0].VMState) } t.Run("non-UTF8 url", func(t *testing.T) { - doBadRequest(t, cs.ScriptHash(), "\xff", nil, []byte{1, 2}, gasForResponse) + doBadRequest(t, cs.Hash, "\xff", nil, []byte{1, 2}, gasForResponse) }) t.Run("non-UTF8 filter", func(t *testing.T) { var f = "\xff" - doBadRequest(t, cs.ScriptHash(), "url", &f, []byte{1, 2}, gasForResponse) + doBadRequest(t, cs.Hash, "url", &f, []byte{1, 2}, gasForResponse) }) t.Run("not enough gas", func(t *testing.T) { - doBadRequest(t, cs.ScriptHash(), "url", nil, nil, 1000) + doBadRequest(t, cs.Hash, "url", nil, nil, 1000) }) }) } diff --git a/pkg/core/state/contract.go b/pkg/core/state/contract.go index ea9570b38..f616d4d79 100644 --- a/pkg/core/state/contract.go +++ b/pkg/core/state/contract.go @@ -1,80 +1,50 @@ package state import ( - "encoding/json" - "errors" - "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) // Contract holds information about a smart contract in the NEO blockchain. type Contract struct { - ID int32 - Script []byte - Manifest manifest.Manifest - - scriptHash util.Uint160 + ID int32 `json:"id"` + UpdateCounter uint16 `json:"updatecounter"` + Hash util.Uint160 `json:"hash"` + Script []byte `json:"script"` + Manifest manifest.Manifest `json:"manifest"` } // DecodeBinary implements Serializable interface. func (cs *Contract) DecodeBinary(br *io.BinReader) { cs.ID = int32(br.ReadU32LE()) + cs.UpdateCounter = br.ReadU16LE() + cs.Hash.DecodeBinary(br) cs.Script = br.ReadVarBytes() cs.Manifest.DecodeBinary(br) - cs.createHash() } // EncodeBinary implements Serializable interface. func (cs *Contract) EncodeBinary(bw *io.BinWriter) { bw.WriteU32LE(uint32(cs.ID)) + bw.WriteU16LE(cs.UpdateCounter) + cs.Hash.EncodeBinary(bw) bw.WriteVarBytes(cs.Script) cs.Manifest.EncodeBinary(bw) } -// ScriptHash returns a contract script hash. -func (cs *Contract) ScriptHash() util.Uint160 { - if cs.scriptHash.Equals(util.Uint160{}) { - cs.createHash() +// CreateContractHash creates deployed contract hash from transaction sender +// and contract script. +func CreateContractHash(sender util.Uint160, script []byte) util.Uint160 { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.ABORT) + emit.Bytes(w.BinWriter, sender.BytesBE()) + emit.Bytes(w.BinWriter, script) + if w.Err != nil { + panic(w.Err) } - return cs.scriptHash -} - -// createHash creates contract script hash. -func (cs *Contract) createHash() { - cs.scriptHash = hash.Hash160(cs.Script) -} - -type contractJSON struct { - ID int32 `json:"id"` - Script []byte `json:"script"` - Manifest *manifest.Manifest `json:"manifest"` - ScriptHash util.Uint160 `json:"hash"` -} - -// MarshalJSON implements json.Marshaler. -func (cs *Contract) MarshalJSON() ([]byte, error) { - return json.Marshal(&contractJSON{ - ID: cs.ID, - Script: cs.Script, - Manifest: &cs.Manifest, - ScriptHash: cs.ScriptHash(), - }) -} - -// UnmarshalJSON implements json.Unmarshaler. -func (cs *Contract) UnmarshalJSON(data []byte) error { - var cj contractJSON - if err := json.Unmarshal(data, &cj); err != nil { - return err - } else if cj.Manifest == nil { - return errors.New("empty manifest") - } - cs.ID = cj.ID - cs.Script = cj.Script - cs.Manifest = *cj.Manifest - cs.createHash() - return nil + return hash.Hash160(w.Bytes()) } diff --git a/pkg/core/state/contract_test.go b/pkg/core/state/contract_test.go index 92dfcb0f6..5283b5acc 100644 --- a/pkg/core/state/contract_test.go +++ b/pkg/core/state/contract_test.go @@ -7,7 +7,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/stretchr/testify/assert" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" ) func TestEncodeDecodeContractState(t *testing.T) { @@ -30,21 +31,30 @@ func TestEncodeDecodeContractState(t *testing.T) { ReturnType: smartcontract.BoolType, }} contract := &Contract{ - ID: 123, - Script: script, - Manifest: *m, + ID: 123, + UpdateCounter: 42, + Hash: h, + Script: script, + Manifest: *m, } - assert.Equal(t, h, contract.ScriptHash()) - t.Run("Serializable", func(t *testing.T) { contractDecoded := new(Contract) testserdes.EncodeDecodeBinary(t, contract, contractDecoded) - assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash()) }) t.Run("JSON", func(t *testing.T) { contractDecoded := new(Contract) testserdes.MarshalUnmarshalJSON(t, contract, contractDecoded) - assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash()) }) } + +func TestCreateContractHash(t *testing.T) { + var script = []byte{1, 2, 3} + var sender util.Uint160 + var err error + + require.Equal(t, "b4b7417195feca1cdb5a99504ab641d8c220ae99", CreateContractHash(sender, script).StringLE()) + sender, err = util.Uint160DecodeStringLE("a400ff00ff00ff00ff00ff00ff00ff00ff00ff01") + require.NoError(t, err) + require.Equal(t, "e56e4ee87f89a70e9138432c387ad49f2ee5b55f", CreateContractHash(sender, script).StringLE()) +} diff --git a/pkg/rpc/client/client.go b/pkg/rpc/client/client.go index c9042a468..628f342a2 100644 --- a/pkg/rpc/client/client.go +++ b/pkg/rpc/client/client.go @@ -121,12 +121,12 @@ func (c *Client) Init() error { if err != nil { return fmt.Errorf("failed to get NEO contract scripthash: %w", err) } - c.cache.nativeHashes["neo"] = neoContractHash.ScriptHash() + c.cache.nativeHashes["neo"] = neoContractHash.Hash gasContractHash, err := c.GetContractStateByAddressOrName("gas") if err != nil { return fmt.Errorf("failed to get GAS contract scripthash: %w", err) } - c.cache.nativeHashes["gas"] = gasContractHash.ScriptHash() + c.cache.nativeHashes["gas"] = gasContractHash.Hash c.initDone = true return nil } diff --git a/pkg/rpc/client/nep17.go b/pkg/rpc/client/nep17.go index 3d68664dd..941cd908f 100644 --- a/pkg/rpc/client/nep17.go +++ b/pkg/rpc/client/nep17.go @@ -124,8 +124,12 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci recipients[i].Address, recipients[i].Amount, nil) emit.Opcodes(w.BinWriter, opcode.ASSERT) } + accAddr, err := address.StringToUint160(acc.Address) + if err != nil { + return nil, fmt.Errorf("bad account address: %v", err) + } return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, transaction.Signer{ - Account: acc.Contract.ScriptHash(), + Account: accAddr, Scopes: transaction.CalledByEntry, }) } diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 8111e49fd..22ffeec8a 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -622,6 +622,6 @@ func (c *Client) GetNativeContractHash(name string) (util.Uint160, error) { if err != nil { return util.Uint160{}, err } - c.cache.nativeHashes[lowercasedName] = cs.ScriptHash() - return cs.ScriptHash(), nil + c.cache.nativeHashes[lowercasedName] = cs.Hash + return cs.Hash, nil } diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 1d414f6b9..b3c48eba3 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -19,6 +19,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/state" "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/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpc/request" @@ -335,10 +336,10 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ m := manifest.NewManifest("Test") cs := &state.Contract{ ID: 0, + Hash: hash.Hash160(script), Script: script, Manifest: *m, } - _ = cs.ScriptHash() return cs }, }, @@ -356,10 +357,10 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ m := manifest.NewManifest("Test") cs := &state.Contract{ ID: 0, + Hash: hash.Hash160(script), Script: script, Manifest: *m, } - _ = cs.ScriptHash() return cs }, }, @@ -377,10 +378,10 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ m := manifest.NewManifest("Test") cs := &state.Contract{ ID: 0, + Hash: hash.Hash160(script), Script: script, Manifest: *m, } - _ = cs.ScriptHash() return cs }, }, diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 1d6008eca..20db7ca7e 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -56,11 +56,11 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } -const testContractHash = "55b692ecc09f240355e042c6c07e8f3fe57546b1" -const deploymentTxHash = "99e40e5d169eb9a2b6faebc6fc596c050cf3f8a70ad25de8f44309bc8ccbfbfb" +const testContractHash = "743ed26f78e29ecd595535b74a943b1f9ccbc444" +const deploymentTxHash = "8e848d367d6194c0ddc12310e0509c0bb3bf716b0bcd9c10508b4bc9c954408d" const genesisBlockHash = "a496577895eb8c227bb866dc44f99f21c0cf06417ca8f2a877cc5d761a50dac0" -const verifyContractHash = "c1213693b22cb0454a436d6e0bd76b8c0a3bfdf7" +const verifyContractHash = "a2eb22340979804cb10cc1add0b8822c201f4d8a" const verifyContractAVM = "570300412d51083021700c14aa8acf859d4fe402b34e673f2156821796a488ebdb30716813cedb2869db289740" const testVerifyContractAVM = "VwcADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGgRVUH4J+yMIaonBwAAABFADBQNDwMCCQACAQMHAwQFAgEADgYMCdswcWkRVUH4J+yMIaonBwAAABJAE0A=" @@ -142,7 +142,7 @@ var rpcTestCases = map[string][]rpcTestCase{ check: func(t *testing.T, e *executor, cs interface{}) { res, ok := cs.(*state.Contract) require.True(t, ok) - assert.Equal(t, testContractHash, res.ScriptHash().StringLE()) + assert.Equal(t, testContractHash, res.Hash.StringLE()) }, }, { @@ -1345,7 +1345,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "80009744770", + Amount: "80009690770", 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 1a29ce8cc..4023d6e32 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index 3b1fc236b..4b8bf364e 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -7,6 +7,7 @@ import ( "os" "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/util" "github.com/nspcc-dev/neo-go/pkg/vm" ) @@ -181,8 +182,9 @@ func (w *Wallet) Close() { // GetAccount returns account corresponding to the provided scripthash. func (w *Wallet) GetAccount(h util.Uint160) *Account { + addr := address.Uint160ToString(h) for _, acc := range w.Accounts { - if c := acc.Contract; c != nil && h.Equals(c.ScriptHash()) { + if acc.Address == addr { return acc } } diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index 302795845..1ce1302ce 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -167,6 +167,7 @@ func TestWallet_GetAccount(t *testing.T) { } for _, acc := range accounts { + acc.Address = address.Uint160ToString(acc.Contract.ScriptHash()) wallet.AddAccount(acc) } diff --git a/scripts/gendump/main.go b/scripts/gendump/main.go index 1ce76be2c..3c4f90496 100644 --- a/scripts/gendump/main.go +++ b/scripts/gendump/main.go @@ -18,7 +18,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native" "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/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -75,18 +74,13 @@ func main() { handleError("can't tranfser GAS", err) lastBlock = addBlock(bc, lastBlock, valScript, txMoveNeo, txMoveGas) - tx, avm, err := testchain.NewDeployTx("DumpContract", strings.NewReader(contract)) + tx, contractHash, err := testchain.NewDeployTx("DumpContract", h, strings.NewReader(contract)) handleError("can't create deploy tx", err) - tx.Signers = []transaction.Signer{{ - Account: h, - Scopes: transaction.CalledByEntry, - }} tx.NetworkFee = 10_000_000 tx.ValidUntilBlock = bc.BlockHeight() + 1 handleError("can't sign deploy tx", acc.SignTx(tx)) lastBlock = addBlock(bc, lastBlock, valScript, tx) - contractHash := hash.Hash160(avm) key := make([]byte, 10) value := make([]byte, 10) nonce := uint32(0)