telegram中文搜索技巧(www.ad6868.vip)实时更新最新最有效的telegram中文搜索技巧登录网址、telegram中文搜索技巧备用网址、telegram中文搜索技巧最新网址、telegram中文搜索技巧手机网址、telegram中文搜索技巧管理网址、telegram中文搜索技巧会员网址。提供telegram中文搜索技巧APP下载,telegram中文搜索技巧APP包含telegram中文搜索技巧代理登录线路、telegram中文搜索技巧会员登录线路、telegram中文搜索技巧信用网开户、telegram中文搜索技巧现金网开户、telegram中文搜索技巧会员注册、telegram中文搜索技巧线上投注等业务。
作者:tincho 译:登链社区
你有 1 个 DAI, 使用钱包(如Metamask)发送1个DAI到0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
(就是 vitalik.eth),点击发送。
一段时间后,钱包显示生意已被确认。突然,vitalik.eth 现在有了1个DAI的财富。这背后到底发生了什么?
让我们回放一下。并以慢动作回放。
准备好了吗?
构建生意
钱包是便于向以太坊网络发送生意的软件。
生意只是告诉以太坊网络,你作为一个用户,想要执行一个行动的一种方式。在此案例中,这将是向Vitalik发送1个DAI。而钱包(如Metamask)有助于以一种相对简朴的方式确立这种生意。
让我们先来看看钱包将确立的生意,可以被示意为一个带有字段和响应数值的工具。
我们的生意最先时看起来像这样:
{ "to": "0x6b175474e89094c44da98b954eedeac495271d0f", // [...]}
其中字段to说明目的地址。在此案例中,0x6b175474e89094c44da98b954eedeac495271d0f
是DAI智能合约的地址。
等等,什么?
我们不是应该发送1个DAI给Vitalik吗?to
不应该是Vitalik的地址吗?
嗯,不是。要发送DAI,必须制作一个生意,执行存储在区块链(以太坊数据库的花哨名称)中的一段代码,将更新DAI的纪录余额。执行这种更新的逻辑和相关存储都保留在以太坊数据库中的一个不能改变的公共盘算机程序中 - DAI智能合约。
因此,你想确立一个生意,告诉合约 嘿,伙计,更新你的内部余额,从我的余额中取出1个DAI,并添加1个DAI到Vitalik的余额
。在以太坊的行话中,hey buddy
这句话翻译为在生意的to
字段中设置DAI的地址。
然而,"to" 字段是不够的。从你喜欢的钱包的用户界面中提供的信息,钱包会要求你填写其他几个字段,以确立一个名堂优越的生意:
{ "to": "0x6b175474e89094c44da98b954eedeac495271d0f", "amount": 0, "chainId": 31337, "nonce": 0, // [...]}
以是你给 Vitalik 发送1个DAI,你既没有使用Vitalik的地址,也没有在amount
字段里填上1
。这就是生涯的艰难(而我们只是在热身)。amount
字段现实上包罗在生意中,示意你在生意中发送若干ETH(以太坊的原始钱币)。由于你现在不想发送ETH,那么钱包会准确地将该字段设置为0。
至于 "chainId",它是一个指定生意执行的链的字段。对于以太坊 Mainnet,它是1。然而,由于我将在mainnet的内陆Fork上运行这个实验,我将使用其链ID:31337,其他链有其他标识符。
那 "nonce" 字段呢?那是一个数字,每次你向网络发送生意时都应该增添。它是一种防御机制,以制止重放问题。钱包通常为你设置这个数字。为了做到这一点,他们会查询网络,询问你的账户最新使用的nonce是什么,然后响应地设置当宿世意的nonce。在上面的例子中,它被设置为0,只管在现实中它将取决于你的账户所执行的生意数目。
我适才说,钱包 "查询网络"。我的意思是,钱包执行对以太坊节点的只读挪用,而节点则回覆所要求的数据。从以太坊节点读取数据有多种方式,这取决于节点的位置,以及它所露出的API种类。
让我们想象一下,钱包可以直接网络接见一个以太坊节点。更常见的是,钱包与第三方供应商(如Infura、Alchemy、QuickNode和许多其他供应商)交互。与节点交互的请求遵照一个特殊的协议来执行远程挪用。这种协议被称为JSON-RPC。
一个试图获取账户nonce的钱包请求将类似于这样:
POST / HTTP/1.1connection: keep-alive Content-Type: application/json content-length: 124{ "jsonrpc":"2.0", "method":"eth_getTransactionCount", "params":["0x6fC27A75d76d8563840691DDE7a947d7f3F179ba","latest"], "id":6} --- HTTP/1.1 200 OKContent-Type: application/json Content-Length: 42{"jsonrpc":"2.0","id":6,"result":"0x0"}
其中0x6fC27A75d76d8563840691DDE7a947d7f3F179ba
将是提议者的账户。从响应中你可以看到,它的nonce是0。
钱包使用网络请求(在此案例中,通过HTTP)来获取数据,请求节点露出的JSON-RPC端点。上面我只包罗了一个,但现实上钱包可以查询任何他们需要的数据来确立一个生意。若是在现实生涯中,你注重到有更多的网络请求来查询其他器械,请不要惊讶。例如,下面是一个内陆测试节点在几分钟内收到的 Metamask 流量快照:
生意的数据字段
DAI是一个智能合约。它的主要逻辑在以太坊主网的地址0x6b175474e89094c44da98b954eedeac495271d0f
实现。
更详细地说,DAI是一个相符ERC20尺度的同质代币 -- 一种特殊的合约类型。意思是DAI至少实现ERC20规范中详述的接口。用(有点牵强的)web2术语来说,DAI是一个运行在以太坊上的不能变的开源网络服务。鉴于它遵照ERC20规范,我们有可能提前知道(纷歧定要看源代码)与它交互简直切露出的接口。
简短的附带说明:不是所有的ERC20代币都是这样。实现某种接口(有利于交互和集成),单不能保证详细的行为。不外,在这个演习中,我们可以平安地假设DAI在行为上是相当尺度的ERC20代币。
在DAI智能合约中,有许多功效(源代码可在这里),其中许多直接来自ERC20规范。稀奇值得注重的是外部转移
(external transferr) 函数。
contract Dai is LibNote { ... function transfer(address dst, uint wad) external returns (bool) { ... } }
这个函数允许任何持有DAI代币的人将其中一部门转账到另一个以太坊账户。它的署名是transfer(address,uint256)
。其中第一个参数是吸收方账户的地址,第二个参数是无符号整数,代表要转账的代币数目。
现在我们不关注该函数行为的详细细节。信托我,你会领会到的,该函数将发送方的余额减去所通报的金额,然后响应地增添吸收方的金额。
这一点很主要,由于当确立一个生意与智能合约交互时,人们应该知道合约的哪个函数要被执行。以及要通报哪些参数。这就像在web2中,你想向一个网络API发送一个POST请求。你很可能需要在请求中指定确切的URL和它的参数。这也是一样的。我们想转移1个DAI,以是我们必须知道若何在生意中指定它应该在DAI智能合约上执行转移
功效。
幸运的是,这是异常直接和直观的。
哈哈,我开顽笑。不是的。
下面是你在生意中必须包罗的内容,以发送1个DAI给维塔利克(记着,地址0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
):
{ // [...] "data": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000"}
让我注释一下。
为了简化集成,并有一个尺度化的方式来与智能合约交互,以太坊生态系统接纳(某种形式)的 "合约ABI规范"(ABI代表应用二进制接口)。在通俗使用场景中,我强调,在通俗使用场景中,为了执行智能合约功效,你必须首先根据合约ABI规范对换用举行编码。更高级的使用场景可能不遵照这个规范,但我们一定不会进入这个兔子洞。我只想说,用Solidity编程的通例智能合约,如DAI,通常遵照合约ABI规范。
你可以看到上面是用DAI的transfer(address,uint256)
函数将1个DAI转移到地址0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
的ABI编码的效果字节。
现在有许多工具可以对生意举行ABI编码(如:https://chaintool.tech/calldata),而且大多数钱包都以某种方式实现ABI编码来与合约交互。为了这个例子,我们可以用一个叫做 cast 的下令行工具来验证上面的字节序列是否准确,它能够用特定的参数对换用举行ABI-编码:
$ cast calldata "transfer(address,uint256)" 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 1000000000000000000 0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000
有什么困扰你的吗?有什么问题吗?
哦,对不起,是的。谁人100000000000000。说真话,我真的很想在这里为你提供一个更有力的论据。许多ERC20代币都用18位小数示意。好比说DAI。
在合约里我们只能使用无符号整数。因此,1个DAI现实上被存储为1 * 10^18 - 这是100000000000000。
现在我们有一个漂亮的ABI编码的字节序列,包罗在生意的data
字段中。现在看来是这样的:
{ "to": "0x6b175474e89094c44da98b954eedeac495271d0f", "amount": 0, "chainId": 31337, "nonce": 0, "data": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000"}
一旦我们进入生意的现实执行阶段,我们将重新审阅这个data
字段的内容。
Gas
下一步是决议为生意支付若干钱。由于请记着,所有生意都必须向破费时间和资源来执行和验证它们的节点网络支付用度。
执行生意的用度是以ETH支付的。而ETH的最终数额将取决于你的生意消耗了若清洁 Gas(也就是盘算成本有多高),你愿意为每个Gas单元的破费支付若干钱,以及网络愿意接受的最低数额。
从用户的角度来看,通常是,支付的越多,生意的速率就越快。因此,若是你想在下一个区块中向Vitalik支付1个DAI,你可能需要设置一个更高的用度,而不是你愿意守候几分钟(或更长的时间),直到Gas更廉价。
差其余钱包可能接纳差其余方式来决议支付若干Gas费。我不知道有什么单一的机制被所有人使用。确定准确用度的计谋可能涉及从节点查询与Gas有关的信息(如网络接受的最低基本用度)。
例如,在下面的请求中,你可以看到Metamask浏览器插件在确立生意时向内陆测试节点发送请求,以获取Gas费数据:
而简化后的请求-响应看起来像:
POST / HTTP/1.1Content-Type: application/json Content-Length: 99{ "id":3951089899794639, "jsonrpc":"2.0", "method":"eth_feeHistory", "params":["0x1","0x1",[10,20,30]]}--- HTTP/1.1 200 OK Content-Type: application/json Content-Length: 190{ "jsonrpc":"2.0", "id":3951089899794639, "result":{ "oldestBlock":"0x1", "baseFeePerGas":["0x342770c0","0x2da4d8cd"], "gasUsedRatio":[0.0007], "reward":[["0x59682f00","0x59682f00","0x59682f00"]] }}
eth_feeHistory
端点被一些节点露出出来,允许查询生意用度数据。若是你很好奇,可以阅读这里或这里玩玩它,或者看看规范这里。
盛行的钱包也使用更庞大的链外服务来获取Gas生意成原本估量,并向用户建议合理的价值。这里有一个例子,一个钱包请求了一个网络服务的公共端点,并收到了一堆有用的Gas相关数据:
看一下响应片断:
很酷,对吗?
无论若何,希望你能熟悉设置Gas用度价钱并不简朴,它是确立一个乐成生意的基本步骤。纵然你想做的只是发送1个DAI。这里是一个有趣的先容性指南,可以深入挖掘其中的一些机制,在生意中设置更准确的用度。
在一些劈头的靠山下,现在让我们回到现实的生意。有三个与Gas有关的字段需要设置:
{ "maxPriorityFeePerGas": ..., "maxFeePerGas": ..., "gasLimit": ...,}
钱包将使用一些提到的机制来为你填写前两个字段。有趣的是,每当钱包UI让你在某个版本的 "慢速"、"通例 "或 "快速"生意中举行选择时,它现实上是在试图决议什么值最适合这些确切的参数。现在你可以更好地明晰上面从钱包收到的JSON名堂的响应内容了。
为了确定第三个字段的值,即GasLimit,有一个利便的机制,钱包可以用来在真正提交生意之前模拟生意。这使他们能够确切的估量一笔生意会消耗若干Gas,从而设定一个合理的GasLimit。
为什么不直接设置一个伟大的GasLimit?固然是为了珍爱你的资金。智能合约可能有随便的逻辑,你是为其执行付费的人。通过在生意最先时就选择一个合理的GasLimit,你可以珍爱自己,制止在Gas用度中耗尽你账户的所有ETH资金的尴尬情形。
可以通过节点的 "eth_estimateGas" 端点举行Gas估算。在发送1个DAI之前,钱包可以行使这一机制来模拟你的生意,并确定你的DAI转账的准确GasLimit。来自钱包的请求-回应可能是这样的:
POST / HTTP/1.1Content-Type: application/json{ "id":2697097754525, "jsonrpc":"2.0", "method":"eth_estimateGas", "params":[ { "from":"0x6fC27A75d76d8563840691DDE7a947d7f3F179ba", "value":"0x0", "data":"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000", "to":"0x6b175474e89094c44da98b954eedeac495271d0f" } ]}--- HTTP/1.1 200 OK Content-Type: application/json{"jsonrpc":"2.0","id":2697097754525,"result":"0x8792"}
在响应中,你可以看到,转账将需要约莫 34706 个Gas单元。
让我们把这些信息纳入生意的有用载荷中:
{ "to": "0x6b175474e89094c44da98b954eedeac495271d0f", "amount": 0, "chainId": 31337, "nonce": 0, "data": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000", "maxPriorityFeePerGas": 2000000000, "maxFeePerGas": 120000000000, "gasLimit": 40000}
记着,"maxPriorityFeePerGas "和 "maxFeePerGas "最终将取决于发送生意时的网络条件。上面我只是为了这个例子而设置了一些随便的值。至于为GasLimit设置的值,我只是把估量值增添了一点,以提交执行生意的可能性。
接见列表和生意类型
让我们简朴谈论一下在你的生意中设置的另外两个字段。
首先,accessList
字段。高级使用场景或边缘场景可能需要生意提前指定要接见的账户地址和合约的存储槽,从而使生意的成本降低一些。
然而,提前确立这样的列表可能并不直接,现在节约的Gas可能并不那么显著。稀奇是对于简朴的生意,如发送1个DAI。因此,我们可以直接将其设置为一个空的列表。只管记着它确实存在有缘故原由,而且它在未来可能变得更有意义。
第二,生意类型。它在 "type" 字段中被指定。类型是生意内部内容的一个指标。我们的将是一个类型2的生意--由于它遵照这里指定的名堂。
{ "to": "0x6b175474e89094c44da98b954eedeac495271d0f", "amount": 0, "chainId": 31337, "nonce": 0, "data": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000", "maxPriorityFeePerGas": 2000000000, "maxFeePerGas": 120000000000, "gasLimit": 40000, "accessList": [], "type": 2}
签署生意
节点若何知道是你的账户,而不是其他人的账户在发送生意?
我们已经来到了确立有用生意的要害步骤:署名。
一旦钱包网络了足够的信息来确立生意,而且你点击发送,它将对你的生意举行数字署名。若何署名?使用你的账户的私钥(你的钱包可以接见),和一个涉及椭圆曲线的加密算法,称为ECDSA。
对于好奇的人来说,现实上被签署的是生意类型和 RLP编码 内容之间的串联的keccak256
哈希值。
keccak256(0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, amount, data, accessList]))
虽然你不应该有那么多的密码学知识来明晰这个。简朴地说,这个历程是对生意的密封。它通过在上面盖上一个只有你的私钥才气发生的伶俐的印章,使其具有防改动性。从现在最先,任何能够接见该署名生意的人(例如,以太坊节点)都可以通过密码学来验证是你的账户发生了该生意。
明确一下:署名不是加密。你的生意始终是明文的。一旦它们被果然,任何人都可以从它们的内容中获得其寄义。
签署生意的历程中,绝不新鲜,会发生一个署名。在实践中是一堆新鲜的不能读的值,你通常会发现它们被称为v
,r
和s
。若是你想更深入地领会这些现实代表的内容,以及它们对还原你的账户地址的主要性,互联网是你的同伙。
你可以通过查看@ethereumjs/tx软件包来更好地领会署名实现时的样子。也可以使用ethers包中的一些适用工具。作为一个极其简化的例子,签署生意以发送1个DAI可以是这样的:
const { FeeMarketEIP1559Transaction } = require("@ethereumjs/tx");const txData = { to: "0x6b175474e89094c44da98b954eedeac495271d0f", amount: 0, chainId: 31337, nonce: 0, data: "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000", maxPriorityFeePerGas: ethers.utils.parseUnits('2', 'gwei').toNumber(), maxFeePerGas: ethers.utils.parseUnits('120', 'gwei').toNumber(), gasLimit: 40000, accessList: [], type: 2, };const tx = FeeMarketEIP1559Transaction.fromTxData(txData);const signedTx = tx.sign(Buffer.from(process.env.PRIVATE_KEY, 'hex'));console.log(signedTx.v.toString('hex'));// 1console.log(signedTx.r.toString('hex'));// 57d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620aconsole.log(signedTx.s.toString('hex'));// e49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293
由此发生的工具将看起来像:
{ "to": "0x6b175474e89094c44da98b954eedeac495271d0f", "amount": 0, "chainId": 31337, "nonce": 0, "data": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000", "maxPriorityFeePerGas": 2000000000, "maxFeePerGas": 120000000000, "gasLimit": 40000, "accessList": [], "type": 2, "v": 1, "r": "57d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a", "s": "e49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293",}
序列化
下一步是序列化署名的生意。这意味着将上面的漂亮工具编码成一个二进制字节序列,这样它就可以被发送到以太坊网络并被吸收的节点消费。
以太坊选择的编码方式被称为RLP。生意的编码方式如下:
0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, v, r, s])
其中初始字节是生意类型。
在前面的代码片断的基础上,你可以现实看到序列化的生意这样添加:
console.log(signedTx.serialize().toString('hex'));// 02f8b1827a69808477359400851bf08eb000829c40946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000c001a0057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a9fe49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293
这就是在我的以太坊主网上的内陆 Fork 中向Vitalik发送 1 个DAI的现实有用载荷。
提交生意
一旦确立、签署和序列化,该生意必须被发送到一个以太坊节点。
节点会提供利便的JSON-RPC端点,节点可以在那里吸收生意请求。
发送生意使用eth_sendRawTransaction
。下面是一个钱包在提交生意时使用的网络流量:
总结的请求-响应看起来像:
POST / HTTP/1.1Content-Type: application/json Content-Length: 446{ "id":4264244517200, "jsonrpc":"2.0", "method":"eth_sendRawTransaction", "params":["0x02f8b1827a69808477359400851bf08eb000829c40946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000c001a0057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a9fe49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293"]}--- HTTP/1.1 200 OK Content-Type: application/json Content-Length: 114{ "jsonrpc":"2.0", "id":4264244517200, "result":"0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5"}
响应中包罗的效果包罗生意的哈希值:bf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5
.这个32字节长的十六进制字符序列是所提交生意的唯一标识符。
节点吸收
我们应该若何去弄清晰当以太坊节点收到序列化的署名生意时会发生什么?
有些人可能会在Twitter上询问,有些人可能会阅读一些Medium文章。其他的人甚至可能会阅读文档,或者看视频
只有一个地方可以找到真相:在源码。让我们用go-ethereum v1.10.18(又名Geth),一个盛行的以太坊节点的实现(一旦以太坊转向Proof-of-Stake,就是 "执行客户端")。从现在最先,我将包罗Geth的源代码链接,以便你能追随。
在收到对其eth_sendRawTransaction
端点的JSON-RPC挪用后,该节点需要对请求正文中包罗的序列化生意举行剖析。以是它最先对生意举行反序列化。从现在最先,节点将能更容易地接见生意的字段。
在这一点上,节点已经最先验证生意了。首先,确保生意的用度(即价钱 * GasLimit)不跨越节点愿意接受的最大限度(显然,默认情形下,这是一个以太币)。尚有然后,确保生意是受重放珍爱的(根据EIP155--记得我们在生意中设置的链ID
字段吗?),或者节点愿意接受不受珍爱的生意。
接下来的步骤包罗发送生意到生意池(又称mempool)。简朴地说,这个池子代表了节点在某个特准时刻所知道的生意聚集。就只有节点所知,这些还没有被纳入区块链。
在真正将生意纳入池中之前,节点检查它是否已经知道它。而且它的ECDSA署名是有用的。否则就甩掉该生意。
然后繁重的 mempool 最先。正如你可能注重到的,有许多噜苏的逻辑来确保生意池是“快乐和康健”的。
这里有相当多的主要验证。例如,GasLimit 低于区块 GasLimit,或者生意的巨细不跨越允许的最大,或者 nonce 是预期的,或者发送方有足够的资金来支付潜在的成本(即价值 + GasLimit*价钱),等等。
虽然我们可以继续下去,但我们在这里不是要成为mempool专家。纵然我们想这样做,我们也需要思量,只要他们遵照网络共识规则,每个节点运营商可能接纳差其余方式来治理mempool。这意味着执行特殊的验证或遵照自界说的生意优先级规则。为了只发送1个DAI,我们可以将 mempool 视为一组急切守候被拾取并被纳入区块的生意。
在乐成地将生意添加到池中(并做内部纪录的事情),节点返回生意哈希值。这正是我们之前在JSON-RPC请求-响应中看到的返回内容?。
检查 mempool
若是你通过 Metamask 或任何默认毗邻到传统节点的类似钱包发送生意,在某些时刻,它将“下降”在公共节点的mempools上。你可以通过自己检查mempools来确保这一点。
有一个利便的端点,一些节点露出了出来,叫做eth_newPendingTransactionFilter
。它也许是frontrunning(抢跑) bots 的好同伙。定期查询这个端点可以让我们在生意被纳入链中之前考察到,现在 1个 DAI 进入了内陆测试节点的mempool中。
在Javascript代码中,这可以通过以下方式完成:
const hre = require("hardhat"); hre.ethers.provider.on('pending', async function (tx) { // do something with the transaction});
要看到现实的eth_newPendingTransactionFilter
挪用,我们可以直接检查网络流量:
从现在最先,剧本将(自动)轮询mempool中的转变。这是随后的许多周期性挪用中的第一个,检查转变:
在收到生意后,节点最终用它的哈希值来响应:
总结的请求 - 响应看起来像:
POST / HTTP/1.1Content-Type: application/json content-length: 74{ "jsonrpc":"2.0", "method":"eth_getFilterChanges", "params":["0x1"], "id":58}--- HTTP/1.1 200 OK Content-Type: application/json Content-Length: 105{ "jsonrpc":"2.0", "id":58, "result":["0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5"]}
早些时刻我说过 "传统节点",但没有注释太多。我的意思是,有一些更专业的节点具有私人内存池的特点。它们允许用户在生意被纳入区块之前,从民众那里 "隐藏 "生意。
不管详细情形若何,这种机制通常包罗在生意提议者和区块构建者之间确立私人通道。Flashbots珍爱服务就是一个显著的例子。现实的效果是,纵然你用上面的方式来监控mempools,你也不能通过私人通道来获取那些进入区块构建者的生意。
假设发送1个DAI的生意是通过通俗通道提交给网络的,没有行使这种服务。
流传
为了使生意被包罗在区块中,它需要以某种方式到达能够确立和提出生意的节点。在事情量证实以太坊中,这些节点被称为矿工。在Proof-of-Stake以太坊中,称为验证者。虽然现实往往更庞大一些。请注重,可能有一些方式可以将区块构建外包给专业服务。
作为一个通俗用户,你应该不需要知道这些区块生产者是谁,也不需要知道他们在那里。相反,你可以简朴地发送一个有用的生意到网络中的任何通例节点,让它包罗在生意池中,并让点对点协议做他们的事情。
有一些这样的p2p协议将以太坊节点相互毗邻。除其他事项外,它们允许频仍地交流生意。
从一最先,所有节点都与他们的对等节点(默认情形下,最多50个对等节点(Peers))一起监听和广播生意。
一旦一个生意到达mempool,它就会被发送给所有尚未知道该生意的毗邻对等节点。
为了提高效率,只有一个随机的毗邻节点子集(平方根 ?)被发送完整生意。其余的是只发送生意哈希。若是需要的话,这些节点可以请求返回完整的生意。
一个生意不能永远停留在一个节点的mempool中。若是它没有被其他缘故原由首先被抛弃(例如,池子满了,生意价钱低了,或者它被更高的nonce/价钱的新生意取代)的话,它可能在一准时间后(默以为3小时)被自动删除。
在mempool中被以为可以被区块构建者拾取和处置的有用生意被跟踪在一个待处置生意的列表中。这个数据结构可以被区块构建者查询,以获得被允许进入链上的可处置生意。
事情准备和生意纳入
生意应该在浏览了mempools之后到达一个挖矿节点(至少在写这篇文章的时刻)。这种类型的节点是稀奇重的多义务处置器。对于熟悉Golang的人来说,这意味着在挖矿相关的逻辑中,有相当多的go例程和通道。对于那些不熟悉Golang的人来说,这意味着矿工的通例操作不能像我想的那样被线性注释。
本节的目的有两个方面。首先,领会我们的生意是若何以及何时被矿工从mempool中提取的。第二,找出生意的执行在哪一点上最先。
当节点的挖矿组件被初始化时,至少有两件相关的事情发生。第一,它最先监听新生意到达mempool的情形。第二,一些基本的循环被触发了。
在Geth的行话中,用生意确立一个区块并将其密封的行为被称为 "提交事情(committing work)"。因此,我们想领会这是在什么情形下发生的。
重点是"新事情"loop。这是一个自力的例程,当节点收到差异类型的通知时,会触发事情提交。该触发器需要发送一个事情要求到该节点的另一个活跃监听器(运行在矿工的"main" loop中)。当收到这样的事情要求时,提交事情最先。
节点最先举行一些初始准备。主要包罗确立区块头。这包罗寻找父区块,确保正在确立的区块的时间戳是准确的,设置区块编号,GasLimit,coinbase地址和基本用度等义务。
之后,共识引擎被挪用,举行区块头的"共识准备"。这盘算出准确的区块难度(取决于当前的网络版本)。若是你听说过以太坊的 "难度炸弹",你就知道了。
译者注: TheMerge 之后已经没有难度炸弹了。
接下来,区块密封上下文被确立。撇开其他动作,这包罗获取最后的已知状态。这是正在确立的区块中的第一个生意将被执行的状态。这可能是我们的生意发送1个DAI。
在准备好区块后,它就最先填充生意。
我们到达了这里:到现在为止,我们的未决(pending)生意只是恬静地坐在节点的内存池中,与其他生意一起被拾起。
默认情形下,生意在一个区块内按价钱和nonce排序。对于我们的情形,生意在区块中的位置现实上是无关的。
现在最先按顺序执行这些生意。一个生意被执行之后,每个生意都确立在前一个生意的效果状态之上。
执行
一个以太坊生意可以被以为是一个状态转换。
状态0:你有100个DAI,Vitalik也有100个。
生意:你发送1个DAI给Vitalik。
状态1:你有99个DAI,而Vitalik有101个。
因此,执行生意需要对区块链的当前状态应用一系列的操作。发生一个新的(差其余)状态作为效果。这将被以为是新的当前状态,直到有另一个生意进来。
在现实中,这更有趣(也更庞大)。让我们来看看。
准备事情(第一部门)
用Geth的行话说,矿工在区块中提交生意。提交生意的行为是在一个环境中举行的。这种环境包罗一个特定的状态(先不管其他的)。
因此,简而言之,提交一个生意本质上是:(1)记着当前的状态,(2)通过应用生意来修改它,(3)凭证生意的乐成,要么接受新状态,要么回滚到原来的状态。
有趣的事情发生在(2):应用生意。
首先要注重的是,生意被酿成了一个 新闻“Message”。若是你熟悉Solidity,在那里你通常会写诸如msg.data
或msg.sender
这样的器械,最后在Geth的代码中读到 message
就是迎接你进入友好之地的标志。
当检查新闻时,会很快就会注重到它与生意的至少一个区别。一条信息有一个from
字段!这个字段是署名者的以太坊地址,它是由生意中的公共署名衍生出来的(还记得新鲜的v
、r
和s
字段吗?)。
现在,执行的环境被进一步准备。首先,与区块相关的环境被确立,其中包罗区块编号、时间戳、coinbase地址和区块GasLimit等内容。然后...
野兽走了进来,它就是以太坊虚拟机。
以太坊虚拟机(EVM),卖力执行生意的基于客栈的256位盘算引擎,我们可以期待它做什么?
EVM是一台机械。作为一台机械,它有一套可以执行的指令(又称操作码)。该指令集多年来一直在转变。因此,一定会有段代码告诉EVM今天应该使用哪些操作码。当EVM实例化注释器时,它选择准确的操作代码集,取决于正在使用的版本。
最后,在真正的执行之前有两个最后步骤。EVM的生意上下文被确立(在你的Solidity智能合约中使用过tx.origin
或tx.gasPrice
吗?),EVM被赋予接见当前状态的权限。
准备事情(第二部门)
现在轮到EVM执行状态转换了。给定一个信息、一个环境和原始状态,它将使用一组有限的指令来转移到一个新的状态。其中,维塔利克有1个分外的DAI?。
在应用状态转换之前,EVM必须确保它遵守特定的共识规则。让我们看看这一点是若何做到的。
验证最先于Geth所说的"预检查",它包罗:
确保与信息的
from
地址相对应的账户没有代码。也就是说,生意起源是一个外部拥有的账户(EOA)。从而遵守EIP 3607规范。验证生意中设置的
maxFeePerGas
(Geth中的gasFeeCap
)和maxPriorityFeePerGas
(Geth中的gasTipCap
)字段是在预期局限内。此外,优先权用度不大于最大用度。而且maxFeePerGas
大于当前区块的基本用度。购置Gas,检查账户是否能够支付它计划消费的所有Gas。而且该区块中尚有足够的Gas来处置这笔生意。最后让账户提前支付Gas用度(别郁闷,以后尚有退款机制)。
接下来,EVM核算生意消耗的 "内在 (intrinsic) Gas"。在盘算内在Gas时,有几个因素需要思量。首先,生意是否是合约确立。我们这里不是,以是Gas 初始为21000 个单元](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L32)。之后,信息的 "数据 "字段中的非零字节的数目也被思量在内。每个非零字节收取16个单元(遵照本规范)。每个零字节只收取4个单元的用度。最后,若是我们提供接见列表,一些更多的Gas将被提前盘算。
我们将生意的value
字段设置为零。若是我们指定一个正值,现在将是EVM检查发送方账户是否真的有足够的余额以执行ETH转账的时刻。此外,若是我们设置了接见列表,现在它们将被初始化为状态。
正在执行的生意并不是在确立一个合约。EVM知道它由于to
字段不是零。因此,它将起者的账户nonce增添1,并执行一个挪用。
挪用将从from
到to
信息的地址,通报data
,没有value,以及消耗内在Gas后剩下的任何Gas。
挪用
DAI智能合约存储在地址0x6b175474e89094c44da98b954eedeac495271d0f
。这就是我们在生意的to
字段中设置的地址。这个初始挪用是为了让EVM执行存储在它那里的任何代码,逐个操作码执行。
操作码是EVM的指令,用十六进制数字示意,局限从00到FF。只管它们通常用它们的名字来指代。例如,00
是STOP
,FF
是SELFDESTRUCT
。一个利便的操作码列表可以在evm.codes上找到。
那么DAI的操作码到底是什么?很喜悦你这么问:
,,,,usdt接口(www.ad6868.vip)实时更新最新最有效的usdt接口登录网址、usdt接口备用网址、usdt接口最新网址、usdt接口手机网址、usdt接口管理网址、usdt接口会员网址。提供usdt接口APP下载,usdt接口APP包含usdt接口代理登录线路、usdt接口会员登录线路、usdt接口信用网开户、usdt接口现金网开户、usdt接口会员注册、usdt接口线上投注等业务。www.326681.com采用以太坊区块链高度哈希值作为统计数据,联博以太坊统计数据开源、公平、无任何作弊可能性。联博统计免费提供API接口,支持多语言接入。
不要惊慌。现在要想弄清这一切还为时尚早。
让我们逐步最先,把初始挪用剖析。它的简要文档提供了一个很好的总结:
// Call executes the contract associated with the addr with the given input as// parameters. It also handles any necessary value transfer required and takes// the necessary steps to create accounts and reverses the state in case of an// execution error or failed value transfer.func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { ... }
首先,逻辑检查是否已经触及挪用深度限制。这个限制是设置为1024,这意味着在一个生意中最多只能有1024个嵌套挪用。这里是一篇有趣的文章,可以阅读关于EVM这种行为背后的一些推理和玄妙之处。稍后我们将探讨若何增添/削减挪用深度。
相关的附带说明:挪用深度限制 并不是 EVM的客栈巨细限制 -- 客栈巨细(巧合?)也是1024个元素。
下一步是确保,若是在挪用中指定了一个正的 value ,那么发送方有足够的余额来执行转移(执行几步之后)。我们可以忽略这一点,由于我们挪用的value是零。此外,一个当前状态的快照被拍摄下来。这允许在失败时轻松恢复任何状态转变。
我们知道DAI的地址指的是一个有代码的账户。因此,它必须已经存在于以太坊的状态空间里。
然而,让我们暂时想象一下,这不是一个发送1个DAI的生意。假设它是一个没有价值的垃圾生意,目的是一个新的地址。响应的账户将需要被添加到状态。然而,若是该账户缺只是空的呢?除了虚耗节点的磁盘空间之外,似乎没有理由对它举行跟踪。EIP 158对以太坊协议举行了一些修改,以辅助制止这种情形的发生。这就是为什么你在挪用任何账户时看到这个if
条件。
我们知道的另一件事是,DAI不是预编译合约。什么是预编译合约?下面是以太坊黄皮书提供的内容:
[......]劈头的架构准备代码片断,以后可能成为原生扩展。地址1到9的合约划分执行椭圆曲线公钥恢复函数、SHA2 256位Hash方案、RIPEMD 160位Hash方案、身份函数、随便精度的模块化指数、椭圆曲线加法、椭圆曲线标量乘法、椭圆曲线配对检查以及BLAKE2压缩函数F。
简而言之,在以太坊的状态下,(到现在为止)有9个差其余特殊合约。这些账户(局限从0x0000000000000000000000000001到0x0000000000000000000009)开箱即包罗执行黄皮书中提到的操作的需要代码。固然,你可以在Geth的代码中自己检查着实现。
为了给预编译合约的故事增添一些色彩,请注重,在以太坊主网中,所有这些账户的余额至少有 1wei。这是有意的(至少在用户最先错误地发送以太币之前)。看,这里有一个近5年的生意向0x0000000000000000000000000000000009
预编译的账户发送了1wei。
不管怎样。在意识到挪用的目的地址并纰谬应于预编译的合约后,节点从状态中读取账户的代码。然后确保它是不空的。最后,下令EVM使用它的注释器,用给定的输入(生意的data
字段的内容)来运行该代码。
注释器(第一部门)
现在是EVM现实执行DAI代码的时刻了。为了完成这个义务,EVM手头有几个元素。它有一个客栈,可以容纳多达1024个元素(只管只有前16个元素可以通过可用的操作码直接接见);它有一个易失性的读/写内存空间;它有一个程序计数器;它有一个特殊的只读内存空间,叫做calldata保留挪用的输入数据。尚有一写其他器械。
像往常一样,在进入多汁的器械之前有一些需要的设置和验证。首先,挪用深度递增1。其次,若是有需要,只读模式被设置。我们的挪用不是只读的(见这里通报的false
参数)。否则一些EVM操作将不被允许。这包罗改变状态的EVM指令SSTORE
, CREATE
, CREATE2
, SELFDESTRUCT
, CALL
有正值,和LOG
。
注释器现在进入了执行循环。它包罗按顺序执行DAI代码中由程序计数器和当前EVM指令集所指示的操作码。现在我们使用的是伦敦指令集--这是在注释器第一次实例化时在跳转表中设置的。
循环还卖力保持一个康健的客栈(制止上下溢出)。并破费每个操作的牢固Gas成本,以及适那时的动态Gas成本。这些动态成本包罗,例如,EVM内存的扩展(阅读更多关于内存扩展成本的盘算这里)。请注重,Gas是在执行操作码之前(--而不是之后)消耗的。
每个可能指令的现执行为可以在这个Geth文件中找到实现。只要略微浏览一下,就可以最先看到这些指令是若何与客栈、内存、Calldata和状态一起事情的。
在这一点上,我们需要直接跳到DAI的操作码中,并为我们的生意跟踪它们的执行。然而,我不以为这是处置这个问题的最好方式。我宁愿先从EVM和Geth中走出来,然后进入Solidity 领地。这应该给我们一个更有价值的关于ERC20转移操作的高级行为的概述。
Solidity 执行
DAI智能合约是用Solidity编码的。它是一种面向工具的高级语言,当被编译时,输出EVM字节码,能够在EVM兼容的链上部署智能合约(在我们的例子中是以太坊)。
DAI的源代码可以找到在区块浏览器中验证,或在GitHub。为了便于参考,我将会指向第一个。
在我们最先之前,让我们始终切记,EVM 对 Solidity 一无所知。它对其变量、函数、合约的结构、ABI编码等一无所知。以太坊区块链存储的是通俗的EVM 字节码,而不是花哨的Solidity代码。
你可能会问,为什么当你去任何区块浏览器时,他们会在以太坊地址上向你显示Solidity代码。嗯,这只是一个幌子。在大多数区块浏览器中,人们可以上传Solidity 源代码,而浏览器则卖力用特定的编译器设置来编译该源代码。若是编译器发生的输出与区块链上的指定地址存储的内容相匹配,那么合约的源代码就被称为 "验证"。从那时起,任何导航到该地址的人都市看到该地址的Solidity代码,而不是只看到存储在该地址的EVM字节码。
上述情形的一个非微不足道的结果是,在某种水平上,我们信托区块浏览器会向我们展示正当的代码(这纷歧定是真的,纵然是意外)。不外这可能有替换方案--除非每次你想读一个合约时,都要对照自己的节点来验证源代码。
无论若何,现在回到DAI的Solidity代码。
在DAI的智能合约上(用Solidity v0.5.12编译),让我们专注于函数的执行:transfer
。
function transfer(address dst, uint wad) external returns (bool) { return transferFrom(msg.sender, dst, wad); }
当transfer
运行时,它将挪用另一个名为transferFrom
的函数,然后返回后者返回的任何布尔标志。transfer
的第一个和第二个参数(这里称为dst
和wad
)被直接通报给transferFrom
。这个函数另外读取提议者的地址(在msg.sender
中作为一个Solidity全局变量)。
对于我们的例子,这些将是通报给transferFrom
的值:
return transferFrom( msg.sender, // 0x6fC27A75d76d8563840691DDE7a947d7f3F179ba (my address on the local testing node) dst, // 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 (Vitalik's address) wad // 1000000000000000000 (1 DAI in wei units));
让我们看看transferFrom
函数,然后:
function transferFrom(address src, address dst, uint wad) public returns (bool) { ... }
首先,提议者的余额被检查与被转移的金额相对照。
require(balanceOf[src] >= wad, "Dai/insufficient-balance");
这很简朴:你转移的DAI不能多于你的余额。若是我没有1个DAI,执行将在这一点上住手,返回一个错误信息。请注重,每个地址的余额都在智能合约的存储中被跟踪。在一个名为balanceOf
的 Map 的数据结构中。若是你至少有1个DAI,我可以向你保证你的账户地址在那里的某个地方有纪录。
第二,代币allowances 被验证。
// don't bother too much about this :)if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { require(allowance[src][msg.sender] >= wad, "Dai/insufficient-allowance"); allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); }
这与我们现在没有关系。由于我们没有代表另一个账户执行转账。虽然要注重到这是所有ERC20代币应该实现的机制--DAI不是破例。实质上,你可以授权其他账户从你的账户转账DAI代币。
第三,现实举行余额交换。
balanceOf[src] = sub(balanceOf[src], wad); balanceOf[dst] = add(balanceOf[dst], wad);
当发送1个DAI时,发送方的余额削减了100000000000000,吸收方的余额增添了100000000000000。这些操作是在balanceOf
数据结构上举行读写的。值得注重的是使用了两个特殊的函数add
和sub
来举行盘算。
为什么不简朴地使用+
和-
运算符?
记着:这个合约是用Solidity 0.5.12编译的。在谁人时刻,编译器并没有像今天这样包罗上/下溢检查。因此,开发者必须记着(或被提醒?),在适当的地方自己实现它们。因此在DAI合约中使用了add
和sub
。它们只是自界说的内部函数,用于执行加法和减法,并带有约束检查以制止算术问题。
function add(uint x, uint y) internal pure returns (uint z) { require((z = x + y) >= x); }function sub(uint x, uint y) internal pure returns (uint z) { require((z = x - y) <= x); }
add
函数将x
和y
相加,若是运算效果小于x
,则住手执行(从而防止整数溢出)。
sub
函数从x
中减去y
,若是操作的效果大于x
,则住手执行(从而防止整数下溢)。
第四,触发一个转移
事宜(正如ERC20规范所建议的)。
emit Transfer(src, dst, wad);
一个事宜是一个纪录操作。在事宜中发出的数据后可以从读取区块链的链外服务中获取,但绝不会被其他合约获取。
在我们的转账操作中,发出的事宜似乎纪录了三个元素。提议者的地址(0x6fC27A75d76d8563840691DDE7a947d7f3F179ba
),吸收者的地址(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
),和发送金额(100000000000000
)。
前两个对应于事宜声明中符号为 “indexed” 的参数。索引参数有利于数据的检索,允许过滤任何响应的纪录值。除非事宜被符号为 "匿名",否则事宜的标识符也会作为一个主题被包罗。
因此,更详细地说,我们正在处置的Transfer
事宜现实上纪录了3个主题(事宜的标识符、提议者的地址和接受者的地址)和1个值(转移的DAI数目)。一旦我们涉及到低级其余EVM的器械,我们将涵盖关于这个事宜的更多细节。
在函数的最后,布尔值 "true "被返回(正如ERC20规范所建议的)。
return true;
这是一种信号,注释转移被乐成执行。这个布尔标志被通报给启动挪用的transfer
函数(它也简朴地返回它)。
这就是了!若是你曾经发送过DAI,这就是你所执行的逻辑。这就是你花钱让一个全球去中央化的节点网络为你做的事情。
等一下。我可能偏得有点远了。由于正如我之前告诉你的,EVM对Solidity一无所知。节点不执行Solidity。它们执行的是EVM的字节码。
是时刻举行真正的生意了。
EVM执行
在这一节中,将变得相当手艺化。我假设你对EVM的字节码对照熟悉。若是你不熟悉,我强烈建议你阅读这个专栏或这个系列。在那里,你会发现本节中的许多观点都有单独和更深入的注释。
DAI的原始字节码是很难阅读的 -- 我们已经在上一节见证了它。研究它的一个更漂亮的方式是使用反汇编的版本。你可以在这里找到Dai的反汇编字节码(为了便于参考,我已经把它提取到这个gist中)。
空闲内存指针和挪用的值
若是你已经熟悉 Solidity 编译器,前三条指令不应该感应惊讶。它只是在初始化空闲内存指针。
0x0: PUSH1 0x800x2: PUSH1 0x400x4: MSTORE
Solidity 编译器为内部的器械保留了从0x00
到0x80
的内存插槽。以是 空闲内存指针
是一个指向EVM内存中第一个可以自由使用的插槽的指针。它存储在0x40
,初始化时指向0x80
。
请记着,所有EVM操作码在Geth中都有对应的实现。例如,你可以真正看到MSTORE
的实现是若何弹出两个客栈元素并向EVM内存写入一个32字节的字:
func opMstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { // pop value of the stack mStart, val := scope.Stack.pop(), scope.Stack.pop() scope.Memory.Set32(mStart.Uint64(), &val) return nil, nil }
在DAI的字节码中,接下来的EVM指令确保挪用不持有任何价值。若是它有,执行将在REVERT
指令处住手。注重使用CALLVALUE
指令(在此实现来读取当前挪用的Value。
0x5: CALLVALUE 0x6: DUP1 0x7: ISZERO 0x8: PUSH2 0x100xb: JUMPI 0xc: PUSH1 0x00xe: DUP1 0xf: REVERT
我们的挪用没有持有任何值(生意的value
字段被设置为零)--以是我们可以继续。
验证calldata(第一部门)
接下来:由编译器引入的另一个检查。这一次,它要弄清晰calldata的巨细(通过CALLDATASIZE
指令获得--在这里实现是否低于4字节(见下面的0x4
和LT
指令)。在此案例中,它将跳到0x142
位置。在0x146
位置的REVERT
指令上住手执行。
0x10: JUMPDEST0x11: POP 0x12: PUSH1 0x40x14: CALLDATASIZE0x15: LT 0x16: PUSH2 0x1420x19: JUMPI...0x142: JUMPDEST 0x143: PUSH1 0x00x145: DUP1 0x146: REVERT
这意味着在DAI智能合约中,calldata的巨细被强制要求为至少4字节。这是由于Solidity使用的ABI编码机制用其署名的keccak256哈希值的前四个字节来识别函数(通常称为 "函数选择器" - 见规范 - Solidity 文档)。
若是calldata没有至少4个字节,就不能能识别出该函数。以是编译器引入了需要的EVM指令,以在此案例中提前失败。这就是你在上面眼见的情形。
为了挪用transfer(address,uint256)
函数,calldata的前四个字节必须与函数的选择器匹配。这四个字节是:
$ cast sig "transfer(address,uint256)"0xa9059cbb
与我们之前确立的生意的data
字段的前4个字节完全相同:
0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000
现在calldata的长度已经获得验证,是时刻使用它了。请看下面若何将前4个字节的calldata放在客栈的顶部(这里需要注重的主要EVM指令是CALLDATALOAD
,这里实现)。
0x1a: PUSH1 0x00x1c: CALLDATALOAD0x1d: PUSH1 0xe00x1f: SHR
现实上CALLDATALOAD
将32字节的calldata推到客栈中。它需要用SHR
指令来截断,以保留前四个字节。
函数调剂器
不要试图逐行明晰下面的内容。相反,注重突出的高级模式就好。我会添加一些分界线,使之加倍清晰:
0x20: DUP10x21: PUSH4 0x7ecebe000x26: GT 0x27: PUSH2 0xb80x2a: JUMPI0x2b: DUP1 0x2c: PUSH4 0xa9059cbb0x31: GT 0x32: PUSH2 0x7c0x35: JUMPI0x36: DUP1 0x37: PUSH4 0xa9059cbb0x3c: EQ 0x3d: PUSH2 0x6b40x40: JUMPI0x41: DUP1 0x42: PUSH4 0xb753a98c0x47: EQ 0x48: PUSH2 0x71a0x4b: JUMPI
一些被推送到客栈的十六进制值有4个字节长,这并不是巧合。这些确实是函数选择器。
上面这组指令是Solidity编译器发生的字节码的一个常见结构。它通常被称为 "函数调剂器"。它类似于一个if-else或switch流程。它只是试图将calldata的前四个字节与合约的函数的已知选择器聚集相匹配。一旦它找到一个匹配项,执行将跳到字节码的另一个部门。在那里,该特定函数的指令被放置在这个部门。
根据上述逻辑,EVM 将 calldata 的前四个字节与 ERC20 transfer
函数的选择器相匹配:0xa9059cbb
。并跳转到字节码位置0x6b4
。这就是告诉 EVM 最先执行DAI的转移。
验证calldata(第二部门)
在匹配了选择器和跳转后,现在EVM要最先运行与函数有关的详细代码了。但在跳转到其细节之前,它需要以某种方式记着位置,一旦所有与功效相关的逻辑被执行,在那里继续执行。
做到这一点的方式是简朴地保持客栈中适当的字节码位置。请看下面正在推送的0x700
值。它将在客栈中倘佯,直到在某个时间点(稍后)被检索到,并被用来跳回以竣事执行。
0x6b4: JUMPDEST 0x6b5: PUSH2 0x700
现在让我们更详细地领会一下transfer
函数。
编译器嵌入了一些逻辑,以确保calldata的巨细对一个有两个address
和uint256
类型的参数的函数是准确的。对于transfer
函数,至少是68字节(4字节用于选择器+64字节用于两个ABI编码的参数)。
0x6b8: PUSH1 0x40x6ba: DUP1 0x6bb: CALLDATASIZE0x6bc: SUB 0x6bd: PUSH1 0x400x6bf: DUP2 0x6c0: LT 0x6c1: ISZERO 0x6c2: PUSH2 0x6ca0x6c5: JUMPI 0x6c6: PUSH1 0x00x6c8: DUP1 0x6c9: REVERT
若是calldata的巨细更小,执行将在位置0x6c9
的REVERT
处住手。由于我们的生意的calldata已经被准确的ABI编码,因此有适当的长度,执行会跳到位置0x6ca
。
读取参数
下一步是让EVM读取calldata中提供的两个参数。这些是20字节长的地址0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
和数字100000000000000
(十六进制的0x0de0b6b3a7640000
)。两者都是以32个字节为一组的ABI编码。因此,需要举行一些基本的操作来读取准确的数值,并把它们放在客栈的顶部。
0x6ca: JUMPDEST 0x6cb: DUP2 0x6cc: ADD 0x6cd: SWAP1 0x6ce: DUP1 0x6cf: DUP1 0x6d0: CALLDATALOAD0x6d1: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0x6e6: AND 0x6e7: SWAP1 0x6e8: PUSH1 0x200x6ea: ADD 0x6eb: SWAP1 0x6ec: SWAP3 0x6ed: SWAP2 0x6ee: SWAP1 0x6ef: DUP1 0x6f0: CALLDATALOAD0x6f1: SWAP1 0x6f2: PUSH1 0x200x6f4: ADD 0x6f5: SWAP1 0x6f6: SWAP3 0x6f7: SWAP2 0x6f8: SWAP1 0x6f9: POP 0x6fa: POP 0x6fb: POP 0x6fc: PUSH2 0x1df40x6ff: JUMP
为了加倍直观,在依次应用上述指令集后(直到0x6fb
),客栈顶部看起来像这样:
0x0000000000000000000000000000000000000000000000000de0b6b3a7640000 0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045
这就是EVM若何迅速地从calldata中提取两个参数,将它们放在客栈中供未来使用。
上面的最后两条指令(字节码位置0x6fc
和0x6ff
)只是让执行跳到位置0x1df4
。让我们在那里继续。
transfer
函数
在简朴的Solidity剖析中,我们看到transfer(address,uint256)
函数是一个包装器,挪用更庞大的transferFrom(address,address,uint256)
函数。编译器将这种内部挪用翻译成这些EVM指令:
0x1df4: JUMPDEST 0x1df5: PUSH1 0x00x1df7: PUSH2 0x1e010x1dfa: CALLER 0x1dfb: DUP5 0x1dfc: DUP5 0x1dfd: PUSH2 0xa250x1e00: JUMP
首先注重推送值0x1e01
的指令。这就是指示EVM 记着
它应该跳回简直切位置,以便在即将到来的内部挪用后继续执行。
然后,注重CALLER
的使用(由于在Solidity中,内部挪用使用msg.sender
)。以及两个DUP5
指令。这些都是把transferFrom
的三个需要参数放在客栈的顶部:挪用者的地址,吸收者的地址,以及要转移的金额。后两个参数已经在客栈的某处,因此使用了 DUP5
。现在客栈的顶部有所有需要的参数:
0x0000000000000000000000000000000000000000000000000de0b6b3a7640000 0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045 0x0000000000000000000000006fc27a75d76d8563840691dde7a947d7f3f179ba
最后,在指令0x1dfd
和0x1e00
之后,执行跳转到位置0xa25
。在那里EVM将最先执行与 transferFrom
函数对应的指令。
transferFrom
函数
首先需要检查的是发送方是否有足够的DAI余额--否则将被回退。发送方的余额被保留在合约存储器中。然后需要的基本EVM指令是SLOAD
。然而,SLOAD
需要知道什么存储槽需要被读取。对于映射(在DAI智能合约中保留账户余额的Solidity数据结构的类型),这不是那么直接的告诉。
我不会在这里深入研究合约存储中Solidity状态变量的内部结构。你可以阅读它这里是v0.5.15。我只想说,给定映射balanceOf
的键地址k
,它响应的uint256
值将被保留在存储槽keccak256(k . p)
,其中p
是映射自己的槽位置,.
是毗邻。你可以自己做数学题。
参考状态变量的存储空间 。
为了简朴起见,我们只强调几个需要发生的操作。EVM必须 i) 盘算映射的存储槽,ii)读取数值,iii)将其与要转账的数目(已经在客栈中的数值)举行对照。因此,我们应该看到像 "SHA3 "这样的指令用于散列,"SLOAD" 用于读取存储,"LT "用于对照。
0xa25: JUMPDEST 0xa26: PUSH1 0x00xa28: DUP2 0xa29: PUSH1 0x20xa2b: PUSH1 0x00xa2d: DUP7 0xa2e: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xa43: AND 0xa44: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xa59: AND 0xa5a: DUP2 0xa5b: MSTORE 0xa5c: PUSH1 0x200xa5e: ADD 0xa5f: SWAP1 0xa60: DUP2 0xa61: MSTORE 0xa62: PUSH1 0x200xa64: ADD 0xa65: PUSH1 0x00xa67: SHA3 --> calculating storage slot0xa68: SLOAD --> reading storage0xa69: LT --> comparing balance against amount0xa6a: ISZERO 0xa6b: PUSH2 0xadc0xa6e: JUMPI
若是发送方没有足够的DAI,执行将在0xa6f
处继续,最后在0xadb
处碰着REVERT
。由于我没有遗忘在我的发送方账户余额中装入1个DAI,那么让我们继续到0xadc
位置。
下面一组指令对应于EVM验证挪用者是否与提议者的地址相符(记得合约中的if (src != msg.sender ...) { ...}
合约中的代码段)。
0xadc: JUMPDEST 0xadd: CALLER 0xade: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xaf3: AND 0xaf4: DUP5 0xaf5: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xb0a: AND 0xb0b: EQ 0xb0c: ISZERO 0xb0d: DUP1 0xb0e: ISZERO 0xb0f: PUSH2 0xbb40xb12: JUMPI...0xbb4: JUMPDEST 0xbb5: ISZERO 0xbb6: PUSH2 0xdb20xbb9: JUMPI
既然不匹配,就在0xdb2
位置继续执行。
下面这段代码没有让你想起什么吗?检查一下正在使用的指令。同样,不要单唯一行行明晰。用你的直觉来发现高级模式和最相关的指令。
0xdb2: JUMPDEST 0xdb3: PUSH2 0xdfb0xdb6: PUSH1 0x20xdb8: PUSH1 0x00xdba: DUP7 0xdbb: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xdd0: AND 0xdd1: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xde6: AND 0xde7: DUP2 0xde8: MSTORE 0xde9: PUSH1 0x200xdeb: ADD 0xdec: SWAP1 0xded: DUP2 0xdee: MSTORE 0xdef: PUSH1 0x200xdf1: ADD 0xdf2: PUSH1 0x00xdf4: SHA3 0xdf5: SLOAD 0xdf6: DUP4 0xdf7: PUSH2 0x1e770xdfa: JUMP
感受它类似于从存储器中读取映射,那是由于它就是这样! 上面是EVM从balanceOf
映射中读取发送方的余额。
然后执行跳转到0x1e77
的位置,这里是sub
函数的主体。
sub
函数将两个数字相减,在整数下溢时恢复到原状。我这里没有写字节码,只管你可以在这里 找到他。算术运算的效果被保留在客栈中。
回到对应于transferFrom
函数主体的指令,现在减法的效果将被写入存储空间-更新balanceOf
映射。试着注重下面的盘算,以获得映射项的适当存储槽,这通过SSTORE
指令的执行。这条指令是有用地将数据写入状态的指令--也就是更新合约的存储。
0xdfb: JUMPDEST 0xdfc: PUSH1 0x20xdfe: PUSH1 0x00xe00: DUP7 0xe01: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xe16: AND 0xe17: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xe2c: AND 0xe2d: DUP2 0xe2e: MSTORE 0xe2f: PUSH1 0x200xe31: ADD 0xe32: SWAP1 0xe33: DUP2 0xe34: MSTORE 0xe35: PUSH1 0x200xe37: ADD 0xe38: PUSH1 0x00xe3a: SHA3 0xe3b: DUP2 0xe3c: SWAP1 0xe3d: SSTORE
一组相当类似的操作码被运行以更新吸收者的账户余额。首先是从存储中的balanceOf
映射中读取。然后使用add
函数将余额加到正在转移的金额上。最后,效果被写到适当的存储槽。
事宜纪录(Log)
在合约的代码中,Transfer
事宜是在更新余额之后发出的。因此,在剖析的字节码中必须有一组指令来处置这种带有适当数据的事宜。
然而,事宜是另一个属于Solidity的理想天下的器械。在EVM天下中,事宜对应于纪录操作。
纪录是通过可用的LOG
指令集举行的。有几个变体,取决于有若干个主题要被纪录。在DAI的案例中,我们已经注重到,发出的Transfer
事宜有3个主题。
那么找到一组运行LOG3
指令的指令就不新鲜了。
0xeca: POP 0xecb: DUP3 0xecc: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xee1: AND 0xee2: DUP50xee3: PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xef8: AND 0xef9: PUSH32 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef0xf1a: DUP5 0xf1b: PUSH1 0x400xf1d: MLOAD 0xf1e: DUP1 0xf1f: DUP3 0xf20: DUP2 0xf21: MSTORE 0xf22: PUSH1 0x200xf24: ADD 0xf25: SWAP2 0xf26: POP 0xf27: POP 0xf28: PUSH1 0x400xf2a: MLOAD 0xf2b: DUP1 0xf2c: SWAP2 0xf2d: SUB 0xf2e: SWAP1 0xf2f: LOG3
在这些指令中,至少有一个值是突出的:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
.这是该事宜的主要标识符。也叫主题0。它是编译器在编译时盘算的一个静态值(嵌入在合约的运行时字节码中)。如前所述,事宜署名的哈希值:
$ cast keccak "Transfer(address,address,uint256)"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
就在到达LOG3
指令之前,客栈看起来像这样:
0x00000000000000000000000000000000000000000000000000000000000000800x00000000000000000000000000000000000000000000000000000000000000200xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef -- topic 0 (event identifier)0x0000000000000000000000006fc27a75d76d8563840691dde7a947d7f3F179ba -- topic 1 (sender's address) 0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045 -- topic 2 (receiver's address)
那么转移的金额在那里?在内存中! 在到达 LOG3
之前,EVM 首先被指示将金额存储在内存中。这样它就可以在以后被纪录指令消耗掉。若是你看一下0xf21
位置,你会看到MSTORE
指令卖力这样做。
以是一旦到达LOG3
,EVM 就可以平安的从内存中抓取现实纪录的数值,从偏移量0x80
最先,读取0x20
字节(上面的前两个客栈元素)。
另一种明晰日志的方式是看它的在Geth中的实现。在那里你会发现一个卖力处置所有日志指令的单一函数。你可以看到 i) 一个空的主题数组被初始化,ii) 内存偏移量和数据巨细从客栈中读取,iii) 主题从客栈中读取并插入数组中,iv) 值从内存中读取,v) 包罗它被发出的地址、主题和值的日志被附加。
这些日志厥后是若何还原的,我们很快就会发现。
返回值
transferFrom
函数的最后一件事是返回布尔值true
。这就是为什么在LOG3
之后的第一条指令只是将0x1
的值推到客栈中。
0xf30: PUSH1 0x1
接下来的指令准备让客栈退出transferFrom
函数,回到它的包装transfer
函数。请记着,这个下一步跳转的位置已经存储在客栈中了--这就是为什么你在下面的操作码中没有看到它。
0xf32: SWAP1 0xf33: POP 0xf34: SWAP4 0xf35: SWAP3 0xf36: POP 0xf37: POP 0xf38: POP 0xf39: JUMP
回到transfer
函数中,要做的就是为最后的跳转准备客栈。到一个执行将被竣事的位置。这个即将跳转的位置之前也已经存储在客栈中了(还记得被推送的0x700
值吗?)
0x1e01: JUMPDEST 0x1e02: SWAP1 0x1e03: POP 0x1e04: SWAP3 0x1e05: SWAP2 0x1e06: POP 0x1e07: POP 0x1e08: JUMP
剩下的就是为最后一条指令准备客栈:RETURN
。这条指令卖力从内存中读取一些数据,并将其传回给原始挪用者。
对于DAI转账,返回的数据将简朴地包罗由transfer
函数返回的true
布尔标志。记着,这个值已经放在客栈里了。
EVM最先抓取第一个可用的空闲内存位置。这是通过读取空闲内存的指针来完成的:
0x700: JUMPDEST 0x701: PUSH1 0x400x703: MLOAD
接下来,必须用MSTORE
将该值存储在内存中。虽然不是那么直接地告诉你,下面的指令只是编译器以为最适合为MSTORE
操作准备客栈的指令。
0x704: DUP1 0x705: DUP3 0x706: ISZERO 0x707: ISZERO 0x708: ISZERO 0x709: ISZERO 0x70a: DUP2 0x70b: MSTORE
RETURN
指令从内存中复制返回的数据。以是它需要被见告要读取若干内存,以及从那里最先。下面的指令简朴的告诉EVM从内存中读取并返回0x20
字节,从空闲内存指针最先。
0x70c: PUSH1 0x200x70e: ADD 0x70f: SWAP2 0x710: POP 0x711: POP 0x712: PUSH1 0x400x714: MLOAD 0x715: DUP1 0x716: SWAP2 0x717: SUB 0x718: SWAP1 0x719: RETURN
返回值 "0x0000000000000000000000000000000000000000000000000001"(对应于布尔值 "true")。
执行住手。
注释器 (第二部门)
字节码的执行已经竣事。注释器必须住手迭代。在Geth中,它是这样做的:
// interpreter's execution loopfor { ... // execute the operation res, err = operation.execute(&pc, in, callContext) if err != nil { break } ... }
这意味着 RETURN
操作码的执行应该以某种方式返回一个错误。纵然是像我们这样乐成的执行。事实上,它确实。只管它作为一个标志--当它与乐成执行RETURN
操作码所返回的标志相匹配时,错误现实上被删除。
Gas 退款和付款
随着注释器运行的竣事,我们回到了最初触发它的挪用中。该运行被乐成完成。因此,返回的数据和任何剩余的Gas被简朴地返回。
挪用也完成了。执行是在包裹状态转换之后举行的。
首先提供Gas退款。它被添加到生意中任何剩余的Gas中。退款金额的上限是所使用Gas的1/5(由于EIP 3529)。所有现在可用的Gas(剩余的加上退还的)被以ETH形式支付到提议者的账户,根据提议者在生意中最初设定的用度价钱。所有剩余的Gas被重新添加到区块中的可用Gas中,以便后续生意可以消耗这些Gas。
然后向coinbase地址(PoW中的矿工地址,PoS中的验证者地址)支付最初准许小费。有趣的是,对执行历程中使用的所有Gas 都举行支付。纵然其中一些厥后被退还了。此外,请注重这里*有用的小费是若何盘算的。不仅注重到它被 "maxPriorityFeePerGas" 生意字段所限制。但更主要的是,意识到它不包罗基本用度(base-fee)!这没有错 -- 以太坊喜欢看着ETH燃烧。
最后执行效果被包裹在一个更漂亮的结构中。包罗使用的Gas,任何可能中止执行的EVM错误(在我们的例子中没有),以及从EVM返回的数据。
确立生意收条
代表执行效果的结构现在被通报 向上 返回 。在这一点上,Geth 对执行状态做了一些内部整理。一旦完成,它就会累积生意中使用的Gas(包罗退款)。
最主要的是,现在是确立生意收条的时刻了。收条是一个总结与生意执行有关的数据的工具。它包罗的信息有:执行状态(乐成/失败),生意的哈希值,使用的Gas 单元,确立合约的地址(在我们的例子中没有),发出的日志,生意的bloom 过滤器,和其他。
我们很快就会检索到我们生意的收条的所有内容。
若是你想深入领会生意的日志和bloom filter的作用,请查看noxx的文章。
挖掘区块
后续生意的执行继续发生,直到区块的空间耗尽。
这时节点会挪用共识引擎来最终完成该区块。在PoW中,这需要积累挖矿奖励(向coinbase地址发放ETH的全额奖励,以及其他区块的部门奖励)并响应地更新区块的最终状态根。
接下来,现实的区块被组装,将所有数据放在准确的位置。包罗头的生意哈希或收条哈希等信息。
现在为真正的PoW挖矿做好了所有准备。一个新的 "义务"被确立并推送给准确的监听者。委托给共识引擎的挖掘义务最先。
我不会详细注释PoW的现实开采是若何举行的。互联网上已经有许多关于它的内容。只需注重,在Geth中,这涉及一个多线程的实验和错误历程,以找到一个数字,知足一个需要条件。不用说,一旦以太坊切换到权益证实,挖掘历程的处置方式将有很大差异。
挖出的区块被推送到适当的channel和在效果循环中吸收。其中收条和日志会响应地更新,并在其被有用挖出后提供最新的区块数据。
区块最后被写入链中,置于链的顶端。
广播区块
下一步是向整个网络宣布,一个新的区块已经被开采出来。同时,该区块在内部存储到待定区块聚集。耐心地守候其他节点简直认。
通告已经完成公布一个特定的事宜,被挖掘的广播循环(loop)吸收。在那里,该区块被完全流传到一个子网的对等节点,并以较轻的方式提供应其他人。
更详细地说,广播需要向毗邻的对等节点的平方根发送块数据。在内部实现是将数据推送到块通道( channel) 行列,直到其通过p2p层发送。p2p新闻被识别为NewBlockMsg
。其余节点的收到一个包罗区块哈希的轻量级通告。
请注重,这只对PoW有用。在权益证实中,区块流传将发生在共识引擎上。
验证区块
对等节点不停监听新闻。每种类型的可能的新闻都有一个相关的处置程序,一旦收到响应的新闻,就立刻挪用。
因此,在获得带有区块数据的 "NewBlockMsg "新闻时,其响应的处置程序被执行。处置程序对新闻举行解码并对流传的块运行一些早期验证。这些验证包罗对报头数据的劈头理智检查,主要是确保它们被填充和约束。以及对区块的uncle和生意哈希值举行验证。
然后发送新闻的对等节点被符号为拥有该区块。从而制止了以后将区块流传回给它。
最后,数据包被向下通报到第二个处置程序,在那里区块将被入队导入到链的内陆副本。入队是通过向响应的通道(channel)直接发送导入请求完成的。当请求被拾取时,它就会触发现实的入队操作。最后推送区块数据到行列中。
该区块现在在内陆行列中,准备被处置。这个行列在节点的区块提取器主循环中被定期读取。当区块到达前面时,节点将拾取它并实验导入。
在现实插入候选块之前,至少有两个值得强调的验证。
首先,内陆链必须已经包罗被流传块的父节点。
第二,区块的头必须是有用的。这些验证是真正的验证。意思是说,那些对共识真正主要的,而且在以太坊的黄皮书中被指定。因此,它们是由共识引擎处置。
举例来说,引擎会检查区块的事情证实是否有用,或者区块的时间戳是否不在已往或不在未来太远,或者区块高度是否已经准确增添,等等。
在验证了它相符共识规则之后,整个区块被进一步流传到一个对等节点的子集。然后才是现实的导入运行。
在导入历程中会有许多事情发生。以是我将直接切入正题。
在几个分外的验证之后,父区块状态被检索。这是新区块的第一个生意将被执行的状态。以它为参考点,整个区块被处置。若是你曾经听说过所有以太坊节点都要执行和验证每一笔生意,现在你可以确定了。之后,完成后状态被验证(见若何这里)。最后,该区块被写入到内陆链。
乐成导入继续公布该区块到其他节点的对等物上(不是完全广播)。
整个验证历程被复制到所有收到区块的节点。很大一部门会接受它进入他们的内陆链,以后会有更多的区块到达,插入到它的上面。
检索生意
在包罗生意的区块上挖出几个区块后,就可以最先平安地假设生意确实已经被确认。
从链上找回生意是异常简朴的。我们所需要的是它的哈希值。利便的是,在我们第一次提交生意的时刻就已经获得了它。
生意自己的数据,加上区块的哈希值和编号,总是可以在节点的eth_getTransactionByHash
端点上检索到。不出所料,它现在返回:
{ "hash": "0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5", "type": 2, "accessList": [], "blockHash": "0xe880ba015faa9aeead0c41e26c6a62ba4363822ddebde6dd77a759a753ad2db2", "blockNumber": 15166167, "transactionIndex": 0, "confirmations": 6, "from": "0x6fC27A75d76d8563840691DDE7a947d7f3F179ba", "maxPriorityFeePerGas": 2000000000, "maxFeePerGas": 120000000000, "gasLimit": 40000, "to": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "value": 0, "nonce": 0, "data": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000", "r": "0x057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a", "s": "0x00e49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293", "v": 1, "creates": null, "chainId": 31337}
生意的收条可以在eth_getTransactionReceipt
端点请求。凭证你运行这个查询的节点,你可能还会获得预期生意收条数据之外的分外信息。这是我从mainnet的内陆分叉中获得的生意收条:
{ "to": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "from": "0x6fC27A75d76d8563840691DDE7a947d7f3F179ba", "contractAddress": null, "transactionIndex": 0, "gasUsed": 34706, "logsBloom": "0x00000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000008000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000010000000000000004000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000002000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000", "blockHash": "0x8b6d44d6cf39d01181b90677f8a77a2605d6e70c40d649eda659499063a19c77", "transactionHash": "0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5", "logs": [ { "transactionIndex": 0, "blockNumber": 15166167, "transactionHash": "0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5", "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000006fc27a75d76d8563840691dde7a947d7f3f179ba", "0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045" ], "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", "logIndex": 0, "blockHash": "0x8b6d44d6cf39d01181b90677f8a77a2605d6e70c40d649eda659499063a19c77" } ], "blockNumber": 15166167, "confirmations": 6, // number of blocks I waited before fetching the receipt "cumulativeGasUsed": 34706, "effectiveGasPrice": 9661560402, "type": 2, "byzantium": true, "status": 1}
你看到了吗?它说"status":1
.
这只能说明一件事:乐成
后记
这个故事绝对可以有更多的内容。
从某种意义上说,它是永无止境的。总会有一个更多的注重事项。尚有一个附带说明。一个替换的执行路径。另一个节点的实现。另一个我可能已经跳过的EVM指令。另一个以差异方式处置事情的钱包。所有的事情都市使我们更靠近于找到当你发送1个DAI时发生的 "真正真相"。
幸运的是,这不是我计划做的事。我希望最后的+1万字没有让你这么想?。请允许我在这里说明一些情形。
事后看来,这篇文章是夹杂了好奇心和挫折感的副产物。
好奇是由于我做以太坊智能合约平安已经跨越4年了,但我还没有像我希望的那样花更多的时间来手动深入探索基础层的庞大性。我真的想获得第一手的履历,看看以太坊自己的现实实现。但智能合约总是被挡在中央。现在我终于找到了更多的和平时光,这似乎是回归本源并最先这次冒险的准确时机。
然则好奇心是不够的。我需要一个捏词。一个触发点。我知道我所想的会很艰难。以是我需要一个足够壮大的理由,不仅是为了最先事情。而且,更主要的是,每当我以为自己厌倦了试图从以太坊的代码中找出意义时,就会重新最先。
我在我没有注重的地方找到了它。我在挫折中发现了它。
沮丧的是,我们在汇款时已经习惯了绝对令人震惊的缺乏透明度的情形。若是你曾经需要在一个资源管制日益严酷的生长中国家举行汇款,毫无疑问,你会感受到我的心情。以是我想提醒自己,我们可以做得更好。我决议用文字来表达我的挫折感。
这篇文章也给我提了个醒。若是你能避开那些模糊的器械、价钱、JPEG中的猴子、Ponzis、Rugpulls和偷窃,这里仍然有价值。这不是 "神奇 "的互联网钱币。这里有真正的数学、密码学和盘算机科学。作为开放源码,你可以看到每一块的移动。你险些可以触摸到它们。不管是哪一天,也不管是什么时刻。无论你是谁。无论你来自那里。
查看更多,区块链百家乐声明:该文看法仅代表作者自己,与本平台无关。转载请注明:澳门威尼斯官网 (www.ad6868.vip):以太坊开奖网(www.326681.com)_深度剖析:在发送1个DAI时发生了什么澳门威尼斯官网(www.ad6868.vip)实时更新最新最有效的澳门威尼斯官网登录网址、澳门威尼斯官网备用网址、澳门威尼斯官网最新网址、澳门威尼斯官网手机网址、澳门威尼斯官网管理网址、澳门威尼斯官网会员网址。提供澳门威尼斯官网APP下载,澳门威尼斯官网APP包含澳门威尼斯官网代理登录线路、澳门威尼斯官网会员登录线路、澳门威尼斯官网信用网开户、澳门威尼斯官网现金网开户、澳门威尼斯官网会员注册、澳门威尼斯官网线上投注等业务。