转载请注明出处:www.huamo.online
字节杭州 求贤若渴:
在js console
中转账ETH
在我之前的文章《Ethereum初探》中,已经详述了在以太坊私链中转账的操作,可以在那里细细查看。
背后流程
Geth Console执行JS通用流程
在探讨sendTransaction
转账流程之前,让我们先看下JS执行的通用流程。如下图所示。
图中展示了从geth console
启动jsre
,到用户输入js代码,解释器执行代码,并通过RPC调用实际的go方法的完整流程。这个流程是通用的,即无论你输入什么js代码,刚开始的阶段都是如此,下面让我来详细剖析一下各个环节。
监听命令阶段
- [1]当用户在命令行输入
geth console
启动客户端时,会调用localConsole()
方法,启动一个全新的geth节点,同时附加上一个JS console,亦即JSRE。
1 | // cmd/geth/consolecmd.go |
代码中列出了3个比较关键的方法。console.New()
会启动一个console实例,这个console实例包含了一个JSRE对象。
1 | type Console struct { |
同时,console.New()
中还包含了console
的初始化工作:console.init()
。这个初始化工作有2点比较重要:
加载
web3.js
文件,并注入到JSRE
中将
rpc
传输接口注入到JSRE
中:jethObj.Object().Set("send", bridge.Send)
,这为之后执行JS代码提供了通信基础。其中bridge.go
正如名字表示的那样,是一个工具方法集合,将js运行时环境和Go RPC调用桥接起来。
注入的方式为
otto
原生提供,它允许你自定义Go方法,并注入到解释器中,这样你就可以直接在js代码中调用这个go方法。如下图所示
Welcome()
没什么好说,打印欢迎信息。
[2]接下来的console.Interactive()则是略为关键,它开启了一个交互式会话,包含2个关键的通道(
chan
):scheduler
,abort
。前者监听用户输入,后者则接受用户强行结束会话的信号。Interactive()单独启动一个goroutine,将用户的输入放在scheduler通道中,然后在主进程中使用select获取两个通道的输出。
1 | // console/console.go |
- [3]在获取到用户的输入后,接下来就开始执行代码,这里面借用了一个第三方库
github.com/robertkrimen/otto
,它是一个golang
实现的js
解释器。在scheduler
通道获取用户输入后,会使用jsre.Evaluate()
方法执行input
内容,该方法内部直接调用otto
的vm.Run()
方法,传入解释器并运行。如前所述,在console.init()
过程中,jsre
中已经注入了web3.js
和send
,所以实际执行过程中,都会通过bridge.Send()
将方法RPC
到以太坊的API
服务中去执行,并返回执行结果。
1 |
|
发送请求
上面说过,基本上所有的方法都是通过console/bridge.go
中的bridge.Send()
方法发送出去,我在该方法中加了一条调试语句,可以看到在请求发送前,数据包是什么样的。
1 | // console/bridge.go |
从图中可以看出,例如eth.accounts
,personal.unlockAccount()
,eth.sendTransaction()
这些方法都走到了Send()
中。其中eth.sendTransaction()
这个js方法对应于eth_sendTransaction()
的golang方法,而传参是一个结构体对象。
请求reqs其实是一个数组,也就是说可以支持批量发送请求,在
Send()
代码中,也是用一个for循环遍历发送请求的。
reqs
中的ID
用以标记请求编号,这样收到resp
后,可以用同样的ID
标记它,这样好对应起来。
如果你在go-ethereum
搜eth_sendTransaction()
这个方法,会很失望,并没有想要的方法声明。问题的关键是bridge
是如何将reqs
请求send
出去的,看起来是又做了一层处理,发送代码为b.client.Call(&result, req.Method, req.Params...)
。所以我们需要看看Call()
的实现。好吧,失算了,Call()并没有做什么特殊处理,只是包装了一个msg,并将其发送了出去。
接受请求
ok,既然走到rpc/client.go
中的send()
方法也并没有什么特殊处理,那么只能到rpc/server.go
中寻找eth_sendTransaction()
的答案了。
由于我们是geth attach
方式运行的jsre
,所以是通过IPC
方式和服务端进行的通信,这里重点查看IPC RPC
服务端的逻辑。
而玄机就在rpc/server.go
的readRequest()
方法中,该方法就像一个预处理器,会读取一批次请求数据,解析数据,将结果递交上层serveRequest()
方法,而后者则会递交给handle()
方法,通过反射调用对应的方法,并将结果返回给客户端。
对于readRequest()
方法,它首先通过调用rpc/json.go
中的parseRequest()
方法,将请求中的Method进行处理,这里便给出了eth_sendTransaction()
的处理方案。让我们看看代码中是如何处理的。
1 | // rpc/json.go |
所以,对于我们在jsre
中输入的eth.sendTransaction()
这样的命令,客户端会包装为eth_sendTransaction
作为Method
发送给远程服务器,而到了服务端,则会拆分这个字符串,并把eth
作为服务名service
,而把sendTransaction
作为方法名method
交给后续处理。
RPC服务端启动详解
服务端的这种解析请求的方式需要说明一下,这来自于RPC
服务端启动的实现,这里面有很多独到的设计,大量用到了Golang
的反射特性,可以让各个API
分散在独立的模块中,而调用则可以统一在一个handle()
中。
在以太坊RPC服务端启动的时候,实际上是位于P2P服务启动之后,恰好,我完整的替换过以太坊的P2P实现,所以对P2P这一块还算略微熟悉,哈哈。P2P包含了两层,底层KAD实现通信基础设施,上层运行各种子协议,包括eth / shh / bzz / les
等协议,实现了以太坊的大部分主体功能。这里有张图,简要讲述了以太坊的节点发现原理。
再扯回来,节点可以统一管理各种子服务,之所以称为子服务更准确点,是因为它包含了P2P的子协议,也包含了RPC的API。以太坊首先在各个独立模块中,实现了这些子服务,在geth
启动时,会将这些子服务实例化为node/service.go:Service
类型(实现了这个接口的所有方法即可),并装入到map[reflect.Type]Service
哈希表中。其中主键是这个子服务结构体的数据类型。这个过程我们称之为注册子服务。
下面用注册eth
子服务来举例说明
1 | // cmd/utils/flags.go |
再来细看看所谓的子服务到底是个什么玩意。
1 | // node/service.go |
这里面有2个关键的方法Protocols() & APIs()
,前者维护了子协议实例,它是一个结构体,其中有一个重要的Run()
方法,定义了特定的子协议该如何运行,包括启动和消息循环,这都是P2P
的内容,暂时不用理会,而后者,APIs()
则是我们今天的主角,它会返回这个子协议提供的所有API
方法。
1 | // rpc/types.go |
这里的Namespace
标记了该API
方法所属的命名空间,Service
则托管了实际的API方法。sendTransaction()
隶属eth
协议,所以eth_sendTransaction
的意思是:调用eth
协议中的sendTransaction()
方法。
到了API struct
这一层还不够,因为API.Service
接受的是API
方法句柄,它并不能够被具名调用,而我们知道(假设大家都知道),反射可以做这样的事情:我反射一个方法,然后再通过reflect:MethodByName()
获取到方法反射体,进而reflect:Call()
就可以调用这个方法。这正是我们需要的,所以在rpc/server.go:RegisterName()
中,将方法命名空间和方法句柄注册到服务器哈希表中,其中方法句柄被反射,方便之后处理请求时直接调用。
1 | // rpc/endpoints.go |
注册完之后,就是启动RPC
服务了,在node/node.go:startRPC()
中,我们可以看到,以太坊实际上会启动4种RPC
服务:startInProc() / startIPC() / startHTTP() / startWS()
,虽然我们常常看到的各种文档上都是3种:IPC / HTTP / WebSocket
,那是因为InProc
是进程内部通讯,不暴露出来的而已。注意:InProc
和IPC
是2种服务,前者是InProcess
进程内通信,后者是Inter Process Communication
进程间通信。
sendTransaction()实现
根据上面的原理,我们可以通过Namespace: "eth"
在代码中寻找合适的sendTransaction()
方法,你也许会找到如下两个疑似正确的方法:
1 | // internal/ethapi/api.go |
但你最终会发现PublicTransactionPoolAPI.SendTransaction()
才是我们需要的,因为它的Namespace
是eth
,而PrivateAccountAPI.SendTransaction()
的命名空间则是personal
。
至于为什么这里的方法名大写开头,而我们传进来的则是
eth_sendTransaction
,这是如何匹配上的,玄机在于RPC服务反射注册API方法时,suitableCallbacks()
做了这个dirty work
,有兴趣的可以到这里查看它做了哪些工作。
EIP155
是关于代号伪龙的以太坊第四次硬分叉中,一种简单的防重放攻击的保护措施文档:在伪龙升级(区块编号2675000
)以后,当交易的transaction.v
满足一定条件时,计算交易哈希时,不再仅哈希前6个元素,而是哈希9个元素。
参考资料
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
https://blog.ethereum.org/2016/11/18/hard-fork-no-4-spurious-dragon/
转载请注明出处:www.huamo.online