GO上的自定义代码执行

这实际上与智能合约有关。


但是,如果您不太了解智能合约是什么,并且通常与加密货币相去甚远,那么什么是数据库中的存储过程,您就可以完全想象。 用户创建代码段,然后在我们的服务器上工作。 用户可以方便地编写和发布它们,并且我们可以安全地执行它们。

不幸的是,我们还没有开发安全性,所以现在我将不对其进行描述,但是我会给出一些提示。

我们还在Go上编写代码,它的运行时施加了一些非常具体的限制,主要的限制是,我们基本上不能链接到不在旅途中编写的另一个项目,这将在我们每次执行第三方代码时停止运行。 总的来说,我们可以选择使用某种解释器,为此我们发现了一个完全健全的Lua和一个完全健全的WASM,但是我不希望将客户添加到Lua中,但是有了WASM,现在问题多于收益,它处于草拟状态,它每月更新一次,因此我们将等到规范确定下来。 我们将其用作第二个引擎。

由于与自己的良心进行了长时间的战斗,因此决定在GO上编写智能合约。 事实是,如果构建用于执行已编译的GO代码的体系结构,则出于安全性考虑,您将不得不将该执行转移到一个单独的进程中,以确保安全性,并且转移到一个单独的进程上会降低IPC的性能,尽管在将来,当我们了解可执行文件的数量时代码,选择这种解决方案甚至让人感到愉快。 事实是,它具有可伸缩性,尽管它会增加每个呼叫的延迟。 我们可以引发许多远程运行时。

有关所做决定的更多信息,以便使之清楚。 每个智能合约由两部分组成,一部分是类代码,第二部分是对象数据,因此,在相同的代码上,一旦我们发布了代码,就可以创建许多行为基本相同但设置不同的合约,并且状态不同。 如果我们进一步讨论,那么这已经是关于区块链的问题,而不是这个故事的主题。

因此,我们执行GO


我们决定使用插件机制,它不仅准备好而且很好。 他做了以下工作,我们以一种特殊的方式将一个插件编译到共享库中,然后加载它,在其中找到符号并在其中传递执行。 但是要注意的是,GO有一个运行时,这几乎是一兆字节的代码,并且默认情况下,该运行时也将进入该库,并且我们到处都有一个raznipipenny运行时。 但是现在我们决定去争取它,确保将来可以击败它。

构建库时,一切都很简单,您可以使用键-buildmode = plugin进行构建,并获取.so文件,然后将其打开。

p, err := plugin.Open(path) 

寻找您感兴趣的角色:

 symbol, err := p.Lookup(Method) 

现在,根据变量是函数还是函数,您可以调用它或将其用作变量。

在此机制的底层是一个简单的dlopen(3),我们加载该库,检查它是否是一个插件并对其进行包装,在创建包装器时,所有导出的字符都包装在接口{}中并存储。 如果它是一个函数,则必须将其简化为正确的函数类型,并简单地调用(如果是变量),然后像变量一样工作。

要记住的主要事情是,如果符号是变量,则它在整个过程中都是全局的,您不能盲目地使用它。

如果已在插件中声明了类型,则此类型在单独的包中有意义,以便主进程可以使用它,例如,将参数作为参数传递给插件的功能。 这是可选的,您不能蒸煮并使用反射。

我们的合同是相应“类”的对象,在开始时,该对象的实例存储在我们的导出变量中,因此我们可以创建另一个相同的变量:

 export, err := p.Lookup("EXPORT") obj := reflect.New(reflect.ValueOf(export).Elem().Type()).Interface() 

并且已经在正确类型的此局部变量内,反序列化对象的状态。 还原对象后,我们可以在其上调用方法。 之后,对象被序列化并添加回存储中,我们为合同中的方法加油打气。

如果您对方法感兴趣,但又懒于阅读文档,则:

 method := reflect.ValueOf(obj).MethodByName(Method) res:= method.Call(in) 

在中间,您仍然需要用包含正确参数类型的空接口填充in数组,如果您感兴趣的话,自己看看它是如何完成的,源是开放的,尽管要在历史上找到这个位置很困难。

总的来说,一切对我们都有效,您可以使用类之类的代码编写代码,将其放在区块链上,再次在区块链上创建此类合同,然后对其进行方法调用,并将合同的新状态写回到区块链上。 太好了! 如何使用现有代码创建新合同? 很简单,我们有构造函数返回一个新创建的对象,即新合同。 到目前为止,一切都通过反射进行,用户必须编写:

 var EXPORT ContractType 

这样我们就知道什么符号代表合同,并实际将其用作模板。

我们真的不喜欢它。 而且我们努力。

解析中


首先,用户不应写任何多余的东西,其次,我们认为合同与合同之间的交互应该简单,并且在不提高区块链的情况下进行测试,区块链既缓慢又困难。

因此,我们决定将合同包装在包装器中,该包装器是在合同和包装器模板的基础上生成的,原则上是一种可以理解的解决方案。 首先,包装器为我们创建了一个导出对象,其次,当用户编写合同时,包装器将替换用于收集合同的库,将基础库与内部的Mokas一起使用,并在发布合同时,将其替换为可与区块链本身一起使用的战斗库。 。

首先,您需要分析代码并了解我们通常所拥有的,找到从BaseContract继承的结构,以便围绕它生成包装。

这非常简单,我们使用[]字节中的代码读取文件,尽管解析器本身可以读取文件,但最好将所有AST元素都引用的文本放在某处,它们引用文件中的字节数,以后我们希望接收就其结构代码而言,我们只是采取类似的方法。

 func (pf *ParsedFile) codeOfNode(n ast.Node) string { return string(pf.code[n.Pos()-1 : n.End()-1]) } 

实际上,我们解析文件并获取将要从中爬网文件的最高AST节点。

 fileSet = token.NewFileSet() node, err := parser.ParseFile(fileSet, name, code, parser.ParseComments) 

接下来,我们从顶部节点开始遍历代码,并在单独的结构中收集所有有趣的内容。

 for _, decl := range node.Decls { switch d := decl.(type) { case *ast.GenDecl: … case *ast.FuncDecl: … } } 

Decls,它已经被解析成一个数组,一个文件中定义的所有内容的列表,但是它是一个Decl接口的数组,没有描述其中的内容,因此每个元素都需要转换为特定的类型,此处语言的作者脱离了使用接口的想法, go / ast中的接口是一个基类。

我们对GenDecl和FuncDecl类型的节点感兴趣。 GenDecl是变量或类型的定义,您需要检查内部类型的确切含义,然后再次将其强制转换为可以使用的TypeDecl类型。 FuncDecl更简单-它是一个函数,如果已填充Recv字段,则这是相应结构的方法。 我们将所有这些东西收集在一个方便的存储中,因为这样我们就使用文本/模板,并且它没有太多的表达能力。

我们唯一需要单独记住的是从BaseContract继承的数据类型的名称,我们将围绕它进行讨论。

代码生成


因此,我们知道合同中的所有类型和函数,我们需要能够根据传入的方法名称和参数化序列数组对对象进行方法调用。 但是毕竟,在代码生成时,我们知道了合同的整个设备,因此我们将合同文件旁边的另一个文件放在具有相同包名的文件旁边,然后将所有必需的导入内容推送到该文件中,这些类型已经在主文件中定义了并且是不必要的。

最主要的是函数的包装。 包装器的名称加上某种前缀,现在很容易找到包装器。

 symbol, err := p.Lookup("INSMETHOD_" + Method) wrapper, ok := symbol.(func(ph proxyctx.ProxyHelper, object []byte, data []byte) (object []byte, result []byte, err error)) 

每个包装器都有相同的签名,因此,当我们从主程序调用它时,我们不需要额外的反射,唯一的事情是函数包装器与方法包装器不同,它们不接收也不返回对象的状态。

包装纸里面有什么?

我们创建一个与函数的参数相对应的空变量数组,将其放入接口数组类型的变量中,然后将参数反序列化到其中,如果我们是一个方法,我们还需要序列化对象的状态,通常是这样的:

 {{ range $method := .Methods }} func INSMETHOD_{{ $method.Name }}(ph proxyctx.ProxyHelper, object []byte, data []byte) ([]byte, []byte, error) { self := new({{ $.ContractType }}) err := ph.Deserialize(object, self) if err != nil { return nil, nil, err } {{ $method.ArgumentsZeroList }} err = ph.Deserialize(data, &args) if err != nil { return nil, nil, err } {{ if $method.Results }} {{ $method.Results }} := self.{{ $method.Name }}( {{ $method.Arguments }} ) {{ else }} self.{{ $method.Name }}( {{ $method.Arguments }} ) {{ end }} state := []byte{} err = ph.Serialize(self, &state) if err != nil { return nil, nil, err } {{ range $i := $method.ErrorInterfaceInRes }} ret{{ $i }} = ph.MakeErrorSerializable(ret{{ $i }}) {{ end }} ret := []byte{} err = ph.Serialize([]interface{} { {{ $method.Results }} }, &ret) return state, ret, err } {{ end }} 

细心的读者会对什么是代理助手感兴趣? -这是我们仍然需要的组合对象,但是现在我们使用它的序列化和反序列化功能。

好吧,任何人都会问:“但是这些是您的论点,它们来自何处?” 这也是一个可以理解的答案,是的,文本/模板没有来自天空的星星,这就是为什么我们在代码中而不是模板中计算这些行的原因。

method.ArgumentsZeroList包含类似

 var arg0 int = 0 Var arg1 string = “” Var arg2 ackwardType = ackwardType{} Args := []interface{}{&arg0, &arg1, &arg2} 

并且参数相应地包含“ arg0,arg1,arg2”。

因此,我们可以使用任何签名调用任何我们想要的东西。

但是我们无法序列化任何答案,事实是序列化器可以通过反射工作,并且不能访问结构的未导出字段,这就是为什么我们有一个特殊的代理帮助器方法,该方法使用错误接口对象并从中创建基础类型的对象。错误,它与通常的错误不同,因为错误文本位于导出的字段中,并且尽管有一些损失,我们也可以对其进行序列化。

但是,如果我们使用代码生成的灭菌器,那么我们甚至都不需要它,因为它是在同一包中编译的,因此我们可以访问未导出的字段。

但是,如果我们想从合同中调用合同呢?


如果您认为从合同中调用合同很容易,那么您就不会理解问题的严重性。 事实是,另一个合同的有效性必须通过共识确认,并且此调用的事实必须在区块链上签名,通常,仅凭另一个合同进行编译并调用其方法是行不通的,尽管我真的很想这样做。 但是我们是程序员的朋友,所以我们应该给他们机会直接做所有事情,并将所有技巧隐藏在系统的内部。 因此,合同的开发就像是直接调用一样,并且合同之间是透明的相互拉动,但是当我们收集要发布的合同时,我们会滑动一个代理而不是另一个合同,后者只知道其地址并要求有关该合同的签名。

如何组织这一切? -我们必须将其他合同存储在一个特殊的目录中,我们的生成器将能够识别该目录并为导入的每个合同创建代理。

也就是说,如果我们遇到了:

 import “ContractsDir/ContractAddress" 

我们将其写入进口合同清单。

顺便说一句,您不需要知道合同的源代码,只需知道我们已经编译的描述,因此,如果我们在某个地方发布这样的描述,并且所有调用都经过主系统,那么我们就不在乎另一个合同是用该语言编写的,如果可以在其中调用方法,则可以在Go上为其编写存根,该存根看起来像是带有可直接调用的合同的程序包。 拿破仑的计划,让我们开始吧。

原则上,我们已经有了具有以下签名的代理帮助器方法:

 RouteCall(ref Address, method string, args []byte) ([]byte, error) 

可以直接从合同中调用此方法,它调用远程合同,返回需要解析的序列化响应并返回到合同。

但是对于用户来说,一切都必须看起来像:

 ret := contractPackage.GetObject(Address).Method(arg1,arg2, …) 

让我们开始吧,首先,在代理中,您需要列出合同方法签名中使用的所有类型,但是我们记得,对于每个AST节点,我们都可以采用其文本表示形式,现在该机制的时代到了。

接下来,我们需要创建一种合同,原则上,他已经知道他的班级,只需要一个地址即可。

 type {{ .ContractType }} struct { Reference Address } 

接下来,我们需要以某种方式实现GetObject函数,该函数在区块链上的地址处将返回一个代理实例,该代理实例知道如何使用此合同,并且对于用户而言,它看起来像一个合同实例。

 func GetObject(ref Address) (r *{{ .ContractType }}) { return &{{ .ContractType }}{Reference: ref} } 

有趣的是,用户调试模式下的GetObject方法直接是BaseContract结构方法,但是没有什么可以阻止我们观察SLA来做对我们方便的事情。 现在,我们可以创建一个代理合同,该合同由我们控制。 实际创建方法还有待解决。

 {{ range $method := .MethodsProxies }} func (r *{{ $.ContractType }}) {{ $method.Name }}( {{ $method.Arguments }} ) ( {{ $method.ResultsTypes }} ) { {{ $method.InitArgs }} var argsSerialized []byte err := proxyctx.Current.Serialize(args, &argsSerialized) if err != nil { panic(err) } res, err := proxyctx.Current.RouteCall(r.Reference, "{{ $method.Name }}", argsSerialized) if err != nil { panic(err) } {{ $method.ResultZeroList }} err = proxyctx.Current.Deserialize(res, &resList) if err != nil { panic(err) } return {{ $method.Results }} } {{ end }} 

由于我们很懒,并且正好存储方法的ast.Node,因此这里的参数列表也是如此,因此计算需要大量模板不知道的类型转换,因此所有操作都需要事先准备。 使用函数,一切都会变得更加复杂,这是另一篇文章的主题。

我们拥有的功能是对象构造函数,并且非常着重于如何在系统中实际创建对象,创建的事实已注册在远程执行器上,将对象转移到另一个执行器上,将其检查并实际存储在其中,并且有许多无效的保存方法这方面的知识称为隐窝。 这个想法基本上很简单,其中只存储地址的包装程序,以及将调用序列化并拉动我们的单例的方法结合起来完成其余的工作。 我们无法使用传输的代理帮助程序,因为用户没有将其传递给我们,因此我们不得不将其设置为单例。

另一个技巧-实际上,我们仍然使用调用上下文,该对象存储有关谁,何时,为什么,为什么调用智能合约的信息,用户根据此信息来决定是否完全执行,如果可能的话然后如何。

以前,我们只是简单地传递了上下文,它是BaseContract类型中的一个不可表达的字段,带有一个setter和getter,并且该setter只允许设置该字段一次,因此上下文是在执行合约之前设置的,用户只能读取它。

但这是问题所在,用户仅读取此上下文,如果他对某种系统功能进行了调用,例如对另一个合约的代理调用,则该代理调用不会收到任何上下文,因为没有人将其传递给他。 然后goroutine本地存储进入现场。 我们决定不编写自己的代码,而是使用github.com/tylerb/gls。

它允许您设置和获取当前goroutine的上下文。 因此,如果在合同内部未创建goroutine,则只需在启动合同之前在gls中设置上下文,现在我们为用户提供的不是方法,而是函数。

 func GetContext() *core.LogicCallContext { return gls.Get("ctx").(*core.LogicCallContext) } 

他很乐意使用它,但是例如,我们在RouteCall()中使用它,以了解当前哪个合同正在调用某人。

原则上,用户可以创建goroutine,但是如果他这样做,则上下文会丢失,因此我们需要对此进行一些操作,例如,如果用户使用go关键字,那么我们必须将此类调用包装在包装器中,上下文会记住并创建goroutine并在其中恢复上下文,但这是另一篇文章的主题。

一起


我们基本上喜欢GO语言工具链的工作方式,实际上,它是一堆做着一件事情的不同命令,例如,当您进行构建时,这些命令会一起执行。 我们决定做同样的事情,一个团队将合同文件放在一个临时目录中,第二个团队在其旁边放置一个包装器,然后第三次调用,这会为每个导入的合同创建一个代理,第四个团队将其全部编译,第五个将其发布在区块链上。 并且有一个命令以正确的顺序运行它们。

Hooray,我们现在有了从GO启动GO的工具链和运行时。 仍然存在许多问题,例如,您需要以某种方式卸载未使用的代码,需要以某种方式确定其已挂起并重新启动挂起的进程,但是这些任务很清楚如何解决。

是的,当然,我们编写的代码并不伪装成库,不能直接使用,但是阅读工作代码生成示例总是很棒,有时我会错过它。 因此,可以在编译器中查看部分代码生成,但可以在executor中查看它的启动方式。

Source: https://habr.com/ru/post/zh-CN425985/


All Articles