Merge pull request #1693 from nspcc-dev/fix/deploy

Provide additional data on deployment
This commit is contained in:
Roman Khimov 2021-01-29 17:23:29 +03:00 committed by GitHub
commit cb56eb4696
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 199 additions and 37 deletions

View file

@ -13,7 +13,7 @@ var key = "key"
const mgmtKey = "mgmt"
func _deploy(isUpdate bool) {
func _deploy(data interface{}, isUpdate bool) {
var value string
ctx := storage.GetContext()

View file

@ -4,7 +4,7 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/storage"
var Key = "sub"
func _deploy(isUpdate bool) {
func _deploy(data interface{}, isUpdate bool) {
ctx := storage.GetContext()
value := "sub create"
if isUpdate {

View file

@ -16,7 +16,7 @@ func init() {
trigger = runtime.GetTrigger()
}
func _deploy(isUpdate bool) {
func _deploy(_ interface{}, isUpdate bool) {
if isUpdate {
Log("_deploy method called before contract update")
return

View file

@ -25,7 +25,7 @@ func init() {
ctx = storage.GetContext()
}
func _deploy(isUpdate bool) {
func _deploy(_ interface{}, isUpdate bool) {
if isUpdate {
ticksLeft := storage.Get(ctx, ticksKey).(int) + 1
storage.Put(ctx, ticksKey, ticksLeft)

View file

@ -336,11 +336,11 @@ func (c *codegen) convertInitFuncs(f *ast.File, pkg *types.Package, seenBefore b
func isDeployFunc(decl *ast.FuncDecl) bool {
if decl.Name.Name != "_deploy" || decl.Recv != nil ||
decl.Type.Params.NumFields() != 1 ||
decl.Type.Params.NumFields() != 2 ||
decl.Type.Results.NumFields() != 0 {
return false
}
typ, ok := decl.Type.Params.List[0].Type.(*ast.Ident)
typ, ok := decl.Type.Params.List[1].Type.(*ast.Ident)
return ok && typ.Name == "bool"
}
@ -1869,7 +1869,7 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error {
hasDeploy := deployLocals > -1
if hasDeploy {
emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(deployLocals), 1})
emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(deployLocals), 2})
c.convertDeployFuncs()
c.deployEndOffset = c.prog.Len()
emit.Opcodes(c.prog.BinWriter, opcode.RET)

View file

@ -150,11 +150,18 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo {
Start: uint16(c.initEndOffset + 1),
End: uint16(c.deployEndOffset),
},
Parameters: []DebugParam{{
Parameters: []DebugParam{
{
Name: "data",
Type: "Any",
TypeSC: smartcontract.AnyType,
},
{
Name: "isUpdate",
Type: "Boolean",
TypeSC: smartcontract.BoolType,
}},
},
},
ReturnType: "Void",
ReturnTypeSC: smartcontract.VoidType,
SeqPoints: c.sequencePoints[manifest.MethodDeploy],

View file

@ -53,7 +53,7 @@ func MethodParams(addr interop.Hash160, h interop.Hash256,
type MyStruct struct {}
func (ms MyStruct) MethodOnStruct() { }
func (ms *MyStruct) MethodOnPointerToStruct() { }
func _deploy(isUpdate bool) {}
func _deploy(data interface{}, isUpdate bool) {}
`
info, err := getBuildInfo("foo.go", src)
@ -100,11 +100,18 @@ func _deploy(isUpdate bool) {}
t.Run("param types", func(t *testing.T) {
paramTypes := map[string][]DebugParam{
"_deploy": {{
"_deploy": {
{
Name: "data",
Type: "Any",
TypeSC: smartcontract.AnyType,
},
{
Name: "isUpdate",
Type: "Boolean",
TypeSC: smartcontract.BoolType,
}},
},
},
"MethodInt": {{
Name: "a",
Type: "ByteString",
@ -160,6 +167,7 @@ func _deploy(isUpdate bool) {}
Name: "_deploy",
Offset: 0,
Parameters: []manifest.Parameter{
manifest.NewParameter("data", smartcontract.AnyType),
manifest.NewParameter("isUpdate", smartcontract.BoolType),
},
ReturnType: smartcontract.VoidType,

View file

@ -104,7 +104,13 @@ type ContractMD struct {
ContractID int32
NEF nef.File
Hash util.Uint160
Methods map[string]MethodAndPrice
Methods map[MethodAndArgCount]MethodAndPrice
}
// MethodAndArgCount represents method's signature.
type MethodAndArgCount struct {
Name string
ArgCount int
}
// NewContractMD returns Contract with the specified list of methods.
@ -112,7 +118,7 @@ func NewContractMD(name string, id int32) *ContractMD {
c := &ContractMD{
Name: name,
ContractID: id,
Methods: make(map[string]MethodAndPrice),
Methods: make(map[MethodAndArgCount]MethodAndPrice),
}
// NEF is now stored in contract state and affects state dump.
@ -129,10 +135,35 @@ func NewContractMD(name string, id int32) *ContractMD {
// AddMethod adds new method to a native contract.
func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method) {
c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, *desc)
md.MD = desc
desc.Safe = md.RequiredFlags&(callflag.All^callflag.ReadOnly) == 0
c.Methods[desc.Name] = *md
index := sort.Search(len(c.Manifest.ABI.Methods), func(i int) bool {
md := c.Manifest.ABI.Methods[i]
if md.Name != desc.Name {
return md.Name >= desc.Name
}
return len(md.Parameters) > len(desc.Parameters)
})
c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, manifest.Method{})
copy(c.Manifest.ABI.Methods[index+1:], c.Manifest.ABI.Methods[index:])
c.Manifest.ABI.Methods[index] = *desc
key := MethodAndArgCount{
Name: desc.Name,
ArgCount: len(desc.Parameters),
}
c.Methods[key] = *md
}
// GetMethod returns method `name` with specified number of parameters.
func (c *ContractMD) GetMethod(name string, paramCount int) (MethodAndPrice, bool) {
key := MethodAndArgCount{
Name: name,
ArgCount: paramCount,
}
mp, ok := c.Methods[key]
return mp, ok
}
// AddEvent adds new event to a native contract.

View file

@ -479,10 +479,16 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) {
emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.SUB,
opcode.CONVERT, opcode.Opcode(stackitem.BooleanT), opcode.RET)
deployOff := w.Len()
emit.Opcodes(w.BinWriter, opcode.JMPIF, 2+8+3)
emit.String(w.BinWriter, "create")
emit.Opcodes(w.BinWriter, opcode.CALL, 3+8+3, opcode.RET)
emit.String(w.BinWriter, "update")
emit.Opcodes(w.BinWriter, opcode.SWAP, opcode.JMPIF, 2+8+1+1+5+3)
emit.String(w.BinWriter, "create") // 8 bytes
emit.Int(w.BinWriter, 2) // 1 byte
emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte
emit.Syscall(w.BinWriter, interopnames.SystemBinarySerialize) // 5 bytes
emit.Opcodes(w.BinWriter, opcode.CALL, 3+8+1+1+5+3, opcode.RET)
emit.String(w.BinWriter, "update") // 8 bytes
emit.Int(w.BinWriter, 2) // 1 byte
emit.Opcodes(w.BinWriter, opcode.PACK) // 1 byte
emit.Syscall(w.BinWriter, interopnames.SystemBinarySerialize) // 5 bytes
emit.Opcodes(w.BinWriter, opcode.CALL, 3, opcode.RET)
putValOff := w.Len()
emit.String(w.BinWriter, "initial")
@ -501,6 +507,9 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) {
emit.String(w.BinWriter, "LastPayment")
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeNotify)
emit.Opcodes(w.BinWriter, opcode.RET)
update3Off := w.Len()
emit.Int(w.BinWriter, 3)
emit.Opcodes(w.BinWriter, opcode.JMP, 2+1)
updateOff := w.Len()
emit.Int(w.BinWriter, 2)
emit.Opcodes(w.BinWriter, opcode.PACK)
@ -588,6 +597,7 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) {
Name: manifest.MethodDeploy,
Offset: deployOff,
Parameters: []manifest.Parameter{
manifest.NewParameter("data", smartcontract.AnyType),
manifest.NewParameter("isUpdate", smartcontract.BoolType),
},
ReturnType: smartcontract.VoidType,
@ -624,6 +634,16 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) {
},
ReturnType: smartcontract.VoidType,
},
{
Name: "update",
Offset: update3Off,
Parameters: []manifest.Parameter{
manifest.NewParameter("nef", smartcontract.ByteArrayType),
manifest.NewParameter("manifest", smartcontract.ByteArrayType),
manifest.NewParameter("data", smartcontract.AnyType),
},
ReturnType: smartcontract.VoidType,
},
{
Name: "destroy",
Offset: destroyOff,

View file

@ -28,7 +28,7 @@ func Call(ic *interop.Context) error {
return errors.New("it is not allowed to use Neo.Native.Call directly to call native contracts. System.Contract.Call should be used")
}
operation := ic.VM.Estack().Pop().String()
m, ok := c.Metadata().Methods[operation]
m, ok := c.Metadata().GetMethod(operation, ic.VM.Estack().Len())
if !ok {
return fmt.Errorf("method %s not found", operation)
}

View file

@ -77,12 +77,26 @@ func newManagement() *Management {
md = newMethodAndPrice(m.deploy, 0, callflag.WriteStates|callflag.AllowNotify)
m.AddMethod(md, desc)
desc = newDescriptor("deploy", smartcontract.ArrayType,
manifest.NewParameter("script", smartcontract.ByteArrayType),
manifest.NewParameter("manifest", smartcontract.ByteArrayType),
manifest.NewParameter("data", smartcontract.AnyType))
md = newMethodAndPrice(m.deployWithData, 0, callflag.WriteStates|callflag.AllowNotify)
m.AddMethod(md, desc)
desc = newDescriptor("update", smartcontract.VoidType,
manifest.NewParameter("script", smartcontract.ByteArrayType),
manifest.NewParameter("manifest", smartcontract.ByteArrayType))
md = newMethodAndPrice(m.update, 0, callflag.WriteStates|callflag.AllowNotify)
m.AddMethod(md, desc)
desc = newDescriptor("update", smartcontract.VoidType,
manifest.NewParameter("script", smartcontract.ByteArrayType),
manifest.NewParameter("manifest", smartcontract.ByteArrayType),
manifest.NewParameter("data", smartcontract.AnyType))
md = newMethodAndPrice(m.updateWithData, 0, callflag.WriteStates|callflag.AllowNotify)
m.AddMethod(md, desc)
desc = newDescriptor("destroy", smartcontract.VoidType)
md = newMethodAndPrice(m.destroy, 1000000, callflag.WriteStates|callflag.AllowNotify)
m.AddMethod(md, desc)
@ -204,9 +218,14 @@ func (m *Management) getNefAndManifestFromItems(ic *interop.Context, args []stac
return resNef, resManifest, nil
}
// deploy is an implementation of public deploy method, it's run under
// VM protections, so it's OK for it to panic instead of returning errors.
// deploy is an implementation of public 2-argument deploy method.
func (m *Management) deploy(ic *interop.Context, args []stackitem.Item) stackitem.Item {
return m.deployWithData(ic, append(args, stackitem.Null{}))
}
// deployWithData is an implementation of public 3-argument deploy method.
// It's run under VM protections, so it's OK for it to panic instead of returning errors.
func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) stackitem.Item {
neff, manif, err := m.getNefAndManifestFromItems(ic, args, true)
if err != nil {
panic(err)
@ -224,7 +243,7 @@ func (m *Management) deploy(ic *interop.Context, args []stackitem.Item) stackite
if err != nil {
panic(err)
}
m.callDeploy(ic, newcontract, false)
m.callDeploy(ic, newcontract, args[2], false)
m.emitNotification(ic, contractDeployNotificationName, newcontract.Hash)
return contractToStack(newcontract)
}
@ -266,9 +285,13 @@ func (m *Management) Deploy(d dao.DAO, sender util.Uint160, neff *nef.File, mani
return newcontract, nil
}
func (m *Management) update(ic *interop.Context, args []stackitem.Item) stackitem.Item {
return m.updateWithData(ic, append(args, stackitem.Null{}))
}
// update is an implementation of public update method, it's run under
// VM protections, so it's OK for it to panic instead of returning errors.
func (m *Management) update(ic *interop.Context, args []stackitem.Item) stackitem.Item {
func (m *Management) updateWithData(ic *interop.Context, args []stackitem.Item) stackitem.Item {
neff, manif, err := m.getNefAndManifestFromItems(ic, args, false)
if err != nil {
panic(err)
@ -280,7 +303,7 @@ func (m *Management) update(ic *interop.Context, args []stackitem.Item) stackite
if err != nil {
panic(err)
}
m.callDeploy(ic, contract, true)
m.callDeploy(ic, contract, args[2], true)
m.emitNotification(ic, contractUpdateNotificationName, contract.Hash)
return stackitem.Null{}
}
@ -299,6 +322,9 @@ func (m *Management) Update(d dao.DAO, hash util.Uint160, neff *nef.File, manif
}
// if manifest was provided, update the contract manifest
if manif != nil {
if manif.Name != contract.Manifest.Name {
return nil, errors.New("contract name can't be changed")
}
if !manif.IsValid(contract.Hash) {
return nil, errors.New("invalid manifest for this contract")
}
@ -378,11 +404,11 @@ func (m *Management) setMinimumDeploymentFee(ic *interop.Context, args []stackit
return stackitem.Null{}
}
func (m *Management) callDeploy(ic *interop.Context, cs *state.Contract, isUpdate bool) {
md := cs.Manifest.ABI.GetMethod(manifest.MethodDeploy, 1)
func (m *Management) callDeploy(ic *interop.Context, cs *state.Contract, data stackitem.Item, isUpdate bool) {
md := cs.Manifest.ABI.GetMethod(manifest.MethodDeploy, 2)
if md != nil {
err := contract.CallFromNative(ic, m.Hash, cs, manifest.MethodDeploy,
[]stackitem.Item{stackitem.NewBool(isUpdate)}, false)
[]stackitem.Item{data, stackitem.NewBool(isUpdate)}, false)
if err != nil {
panic(err)
}

View file

@ -71,6 +71,57 @@ func TestStartFromHeight(t *testing.T) {
checkContractState(t, bc2, cs1.Hash, cs1)
}
func TestContractDeployAndUpdateWithParameter(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
// nef.NewFile() cares about version a lot.
config.Version = "0.90.0-test"
mgmtHash := bc.ManagementContractHash()
cs1, _ := getTestContractState(bc)
cs1.Manifest.Permissions = []manifest.Permission{*manifest.NewPermission(manifest.PermissionWildcard)}
cs1.ID = 1
cs1.Hash = state.CreateContractHash(testchain.MultisigScriptHash(), cs1.NEF.Checksum, cs1.Manifest.Name)
manif1, err := json.Marshal(cs1.Manifest)
require.NoError(t, err)
nef1b, err := cs1.NEF.Bytes()
require.NoError(t, err)
aer, err := invokeContractMethod(bc, 11_00000000, mgmtHash, "deploy", nef1b, manif1, int64(42))
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer.VMState)
t.Run("_deploy called", func(t *testing.T) {
res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "getValue")
require.NoError(t, err)
require.Equal(t, 1, len(res.Stack))
item, err := stackitem.DeserializeItem(res.Stack[0].Value().([]byte))
require.NoError(t, err)
expected := []stackitem.Item{stackitem.Make("create"), stackitem.Make(42)}
require.Equal(t, stackitem.NewArray(expected), item)
})
cs1.NEF.Script = append(cs1.NEF.Script, byte(opcode.RET))
cs1.NEF.Checksum = cs1.NEF.CalculateChecksum()
nef1b, err = cs1.NEF.Bytes()
require.NoError(t, err)
cs1.UpdateCounter++
aer, err = invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nef1b, nil, "new data")
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer.VMState)
t.Run("_deploy called", func(t *testing.T) {
res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "getValue")
require.NoError(t, err)
require.Equal(t, 1, len(res.Stack))
item, err := stackitem.DeserializeItem(res.Stack[0].Value().([]byte))
require.NoError(t, err)
expected := []stackitem.Item{stackitem.Make("update"), stackitem.Make("new data")}
require.Equal(t, stackitem.NewArray(expected), item)
})
}
func TestContractDeploy(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
@ -166,7 +217,10 @@ func TestContractDeploy(t *testing.T) {
res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "getValue")
require.NoError(t, err)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, []byte("create"), res.Stack[0].Value())
item, err := stackitem.DeserializeItem(res.Stack[0].Value().([]byte))
require.NoError(t, err)
expected := []stackitem.Item{stackitem.Make("create"), stackitem.Null{}}
require.Equal(t, stackitem.NewArray(expected), item)
})
t.Run("get after deploy", func(t *testing.T) {
checkContractState(t, bc, cs1.Hash, cs1)
@ -198,6 +252,7 @@ func TestContractDeploy(t *testing.T) {
Name: manifest.MethodDeploy,
Offset: 0,
Parameters: []manifest.Parameter{
manifest.NewParameter("data", smartcontract.AnyType),
manifest.NewParameter("isUpdate", smartcontract.BoolType),
},
ReturnType: smartcontract.VoidType,
@ -226,6 +281,7 @@ func TestContractDeploy(t *testing.T) {
Name: manifest.MethodDeploy,
Offset: 0,
Parameters: []manifest.Parameter{
manifest.NewParameter("data", smartcontract.AnyType),
manifest.NewParameter("isUpdate", smartcontract.BoolType),
},
ReturnType: smartcontract.ArrayType,
@ -319,6 +375,17 @@ func TestContractUpdate(t *testing.T) {
checkFAULTState(t, res)
})
t.Run("change name", func(t *testing.T) {
var badManifest = cs1.Manifest
badManifest.Name += "tail"
manifB, err := json.Marshal(badManifest)
require.NoError(t, err)
res, err := invokeContractMethod(bc, 10_00000000, cs1.Hash, "update", nef1b, manifB)
require.NoError(t, err)
checkFAULTState(t, res)
})
cs1.NEF.Script = append(cs1.NEF.Script, byte(opcode.RET))
cs1.NEF.Checksum = cs1.NEF.CalculateChecksum()
nef1b, err = cs1.NEF.Bytes()
@ -346,7 +413,10 @@ func TestContractUpdate(t *testing.T) {
res, err := invokeContractMethod(bc, 1_00000000, cs1.Hash, "getValue")
require.NoError(t, err)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, []byte("update"), res.Stack[0].Value())
item, err := stackitem.DeserializeItem(res.Stack[0].Value().([]byte))
require.NoError(t, err)
expected := []stackitem.Item{stackitem.Make("update"), stackitem.Null{}}
require.Equal(t, stackitem.NewArray(expected), item)
})
t.Run("check contract", func(t *testing.T) {
checkContractState(t, bc, cs1.Hash, cs1)