本文作者:一个程序猿[1]
从命令行到以太坊节点通过 web3.py 的往返旅程
本文是对 Web3.py 一些内部细节的深入探讨。如果你 A) 有兴趣为 Web3.py 代码库做贡献,B) 实现自定义模块、方法或中间件,或者 C) 进行一些深度调试,那么这篇文章可能会适合你。
在这篇文章中,我们将看看从命令行到以太坊节点再返回这个过程,在经过 web3.py 时是什么样子的。我们将以查询一个账户余额为例,在代码中追踪其路径。本文中的示例代码来自 web3.py 代码库,但为了文章主题在一些地方做了简化,准备好了吗?
Web3
类
使用 web3.py 差不多是从实例化一个web3
对象开始的。在实例化后仍然可以配置对象,但是你需要预先传递相应的Provider[2]。在这个例子中,我们用HTTPProvider
。
from web3 import Web3, HTTPProvider
w3 = Web3(HTTPProvider('https://<your-provider-url>'))
w3.isConnected()
# True
在创建 Web3 对象时,底层其实发生了很多事情,但可以明确的是,你会获得一个请求管理器(request manager)和一些模块(module)。
代码语言:javascript复制class Web3:
def __init__(
self,
provider = None,
middlewares = None,
modules = None,
external_modules = None,
ens = cast(ENS, empty)
) -> None:
self.manager = self.RequestManager(self, provider, middlewares)
self.codec = ABICodec(build_default_registry())
if modules is None:
modules = get_default_modules()
self.attach_modules(modules)
if external_modules is not None:
self.attach_modules(external_modules)
self.ens = ens
接下来,我们从模块开始吧!
Module
类
对大多数用户来说,默认模块完全可以满足需求。如果不传入自定义选项,web3.py 将直接启动,如下:
代码语言:javascript复制def get_default_modules():
return {
"eth": Eth,
"net": Net,
"version": Version,
"parity": (Parity, {
"personal": ParityPersonal,
}),
"geth": (Geth, {
"admin": GethAdmin,
"miner": GethMiner,
"personal": GethPersonal,
"txpool": GethTxPool,
}),
"testing": Testing,
}
在本文例子中,我们要追踪的是账户余额。需要的函数存在于上面的Eth
模块中,像所有其他在以太坊 JSON-RPC API[3]标准中定义的方法一样。具体来说,我们感兴趣的 JSON-RPC 方法是eth_getBalance
。
请注意,如果你想查询余额,以太坊客户端(如 Geth 节点)希望以 JSON 格式请求,如下:
代码语言:javascript复制{
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0x3C6...", "latest"],
"id": 10
}
像这样手动请求很不方便,Web3.py 提供了更友好的接口。运行w3.eth.get_balance('0x3C6...')
就会生成并发送 JSON-RPC 请求,像上面一样。我们来了解一下这是怎么发生的。
现在我们知道,eth_getBalance
和其他标准的以太坊方法都封装在 Web3.py 的Eth
模块中。这些方法的定义如下所示:
class Eth(Module):
...
get_balance = Method(
RPC.eth_getBalance,
mungers=[BaseEth.block_id_munger],
)
...
这里有几点需要注意:
Eth
模块继承自一个Module
类,get_balance
被定义为Method
类的一个实例,并且- 包含一个
munger
参数。
从第一点开始。
每个 Web3.py 模块都继承自一个Module
类,这个类有一个retrieve_caller_fn
方法,提供了有限却非常重要的一些功能,例如当调用get_balance
方法时,输入会被格式化,构造成 JSON-RPC 负载并发送,然后再将结果格式化程序应用到响应中。
def retrieve_method_call_fn(w3, module, method):
def caller(*args, **kwargs):
# 1) Apply input mungers
(method_str, params), response_formatters = method.process_params(module, *args, **kwargs)
...
# 2) Have the RequestManager build and send the tx
result = w3.manager.request(method_str,
params,
error_formatters,
null_result_formatters)
# 3) Format human-readable results
return apply_result_formatters(result_formatters, result)
return caller
class Module:
def __init__(self, w3):
self.retrieve_caller_fn = retrieve_method_call_fn(w3, self)
self.w3 = w3
self.codec: ABICodec = w3.codec
def __get__(self, obj = None, obj_type = None):
return obj.retrieve_caller_fn(self)
请求和响应格式化程序对于区块链数据的友好展示,起着重要作用。当你调用eth_getBalance
时,以太坊客户端将返回一个十六进制的字符串,正如 JSON-RPC 规范所要求的那样:
{
'jsonrpc': '2.0',
'id': 6,
'result': '0x83a3c396d1a7b40'
}
这并不完全是人类可读的,Web3.py 会再用响应格式化程序将该十六进制字符串转换为整数。method_formatters.py
模块中有几个格式化程序,包括PYTHONIC_RESULT_FORMATTERS
:
PYTHONIC_RESULT_FORMATTERS = {
...
RPC.eth_getBalance: to_integer_if_hex,
...
}
注意,如果你在特定区块高度查询余额,以太坊客户端也需要一个十六进制字符串参数。而使用 web3.py 请求格式化程序就可以让用户很方便地传递一个整数值,如w3.eth.get_balance('0x123...', 500000)
,不需要手动将其转换为十六进制字符串:
PYTHONIC_REQUEST_FORMATTERS = {
...
RPC.eth_getBalance: apply_formatter_at_index(to_hex_if_integer, 1),
...
}
这些格式化程序对于每个方法都是唯一的,因此可以在 Method
类中注册,以便数据格式的平滑转换。
Method
类
我们对 Module
类已经有了一个宏观的认识。现在回想一下,get_balance
是 Method
类的一个实例:
class Eth(Module):
...
get_balance = Method(
RPC.eth_getBalance,
mungers=[BaseEth.block_id_munger],
)
...
Method
类只是提供了一种可组合的方式来维护方法的几个传入和传出的 payload(负载)格式化程序。
看看下面的__init__
函数,你就可以明白其维护的内容:
class Method:
def __init__(
self,
json_rpc_method = None,
mungers = None,
request_formatters = None,
result_formatters = None,
null_result_formatters = None,
method_choice_depends_on_args = None,
is_property = False,
):
self.json_rpc_method = json_rpc_method
self.mungers = _set_mungers(mungers, is_property)
self.request_formatters = request_formatters or get_request_formatters
self.result_formatters = result_formatters or get_result_formatters
self.null_result_formatters = null_result_formatters or get_null_result_formatters
self.method_choice_depends_on_args = method_choice_depends_on_args
self.is_property = is_property
对这些值来说,如果没有传入格式化程序,会做一个合理的默认选择。对get_balance
来说是这样的,因为我们只传入了json_rpc_method
和一个 munger。
最后一个问题:什么是 munger ?这个通用术语表示一些数据转换可能发生在类型格式之外。get_balance
方法提供了一个很好的例子,它接受两个参数:一个地址和一个区块高度标识符,确定在什么时间点查看这个地址的余额。接受的区块高度标识符的值包括"earliest"、"latest"、"pending"或特定的块编号。
在get_balance
方法定义中,包含了一个block_id_munger
. 如果没有定义,那么这个特定的 munger 只是简单地设置一个默认的区块高度标识符。默认情况下,值为"latest",表示我们对帐户的当前余额感兴趣。
def block_id_munger(self, account, block_identifier = None):
if block_identifier is None:
block_identifier = self.default_block
return (account, block_identifier)
至此,我们已经涵盖了重要的构建部分。接下来探究中间件,以便了解整个请求往返过程。
中间件
中间件是一些可以在请求和响应上进行拦截并执行任意操作的函数。这些操作可以包括日志记录、数据格式化、将请求重新路由到不同的端点,以及您能想到的任何其他事情。
您可能还记得,在创建 Web3 实例时,中间件会存入到RequestManager
。如果没有传入,则包含一组默认中间件:
@staticmethod
def default_middlewares(w3):
return [
(request_parameter_normalizer, 'request_param_normalizer'),
(gas_price_strategy_middleware, 'gas_price_strategy'),
(name_to_address_middleware(w3), 'name_to_address'),
(attrdict_middleware, 'attrdict'),
(pythonic_middleware, 'pythonic'),
(validation_middleware, 'validation'),
(abi_middleware, 'abi'),
(buffered_gas_estimate_middleware, 'gas_estimate'),
]
此列表中的每个元组都包含一个中间件函数以及要分配给该中间件的名称。我们探究下name_to_address
中间件。
name_to_address
中的name
指的是[以太坊名称服务 (ENS)](https://ens.domains/ "以太坊名称服务 (ENS "以太坊名称服务 (ENS)")")的名称。Web3.py 已经支持 ENS 名称,这意味着你可以请求某个 ENS 域名(例如shaq.eth
,这是人类可读的格式,而不是长地址格式0x3C6aEFF92b4B35C2e1b196B57d0f8FFB56884A17
)的余额。其背后的实现是,name_to_address
中间件拦截eth_getBalance
以 ENS 域名作为参数的请求,将名称解析为以太坊十六进制字符串地址,然后将调用转发到下一个中间件或执行请求。
# 0) original request:
w3.eth.get_balance('shaq.eth')
# 1) after input munging:
w3.eth.get_balance('shaq.eth', 'latest')
# 2) after request middleware:
w3.eth.get_balance('0x3C6aEFF92b4B35C2e1b196B57d0f8FFB56884A17', 'latest')
在 Web3.py 中,一个中间件可以影响传入和传出请求,因此有个有趣的名称“中间件洋葱(middleware onion)”来形象的表示[4]其应用层级。在这里,name_to_address
中间件仅格式化传出请求,但如果需要,可以自定义address_to_name
响应中间件,将地址转换为特定调用的 ENS 名称。
总结
让我们全程回顾一下:
- 当你创建一个新
Web3
实例并传入一个 provider 时,你将获得一些名称空间模块和一个维护中间件堆栈的RequestManager
。 - 当你在
Eth
模块上执行get_balance
方法时,输入参数munger
首先被应用。在这种情况下,如果你执行w3.eth.get_balance('shaq.eth')
,block_id_munger
将添加默认值latest
作为第二个参数。 - 接下来,Web3.py 将应用一个请求格式化程序。如果你想要在一个特定区块下 Shaq 的余额 ,比如区块号 9999999,Pythonic 请求格式化程序会将其转换为十六进制字符串——这以太坊客户端所期望的格式。
- 接下来触发中间件,在分派请求之前执行任何相关操作。例如,ENS 名称将通过
name_to_address
中间件解析为以太坊账户地址。 - 在调用所有中间件函数后,provider 构建 JSON-RPC 请求并通过适当的通道(HTTP、IPC 或 WebSockets)发送请求。
- 来自以太坊客户端的响应被解码,然后通过中间件传回,并执行相应的响应中间件。
- 最后,回到模块中,应用人类可读的响应格式化程序。如果
eth_getBalance
返回十六进制字符串0x819ef3b0a273233
,那么 Pythonic 响应格式化程序会将其转换为整数 (583760663573639731
) 并将该值以 wei 的形式返回给用户。
图形表示以上过程为:
原文链接:https://snakecharmers.ethereum.org/web3py-internals-json-rpc-round-trips/
参考资料
[1]
一个程序猿: https://learnblockchain.cn/people/9
[2]
Provider: https://web3py.readthedocs.io/en/stable/providers.html
[3]
以太坊 JSON-RPC API: https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json&uiSchema[appBar][ui:splitView]=false&uiSchema[appBar][ui:input]=false&uiSchema[appBar][ui:examplesDropdown]=false
[4]
形象的表示: https://web3py.readthedocs.io/en/stable/middleware.html#middleware-order