Skip to content

Commit 8380f4a

Browse files
authored
Merge pull request #27 from YoubetDao/feat/transaction
feat: translate transaction
2 parents 915074e + c2166e1 commit 8380f4a

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed

src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
- [客户端架构](./architecture.md)
66
- [执行层客户端](./el-clients.md)
77
- [以太坊虚拟机 (EVM)](./evm.md)
8+
- [交易剖析](./transaction.md)
89
- [数据结构](./data-structures.md)
910
- [RLP](./RLP.md)

src/transaction.md

+304
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# 交易剖析
2+
3+
**交易** 是由 **外部账户** 发布的经过加密签名的指令,通过 [JSON-RPC](/wiki/EL/JSON-RPC.md) 广播到整个网络。
4+
5+
交易包含以下字段:
6+
7+
- **nonce ($T_n$)**: 一个整数值,等于发送方已发送交易的数量。Nonce 的用途包括:
8+
- **防止重放攻击**:假设 Alice 向 Bob 发送 1 ETH 的交易,Bob 可能试图将相同的交易重新广播到网络中,从 Alice 的账户中获取额外的资金。由于交易使用了唯一的 nonce,如果 Bob 再次发送,EVM 将直接拒绝交易,从而保护 Alice 的账户免受未经授权的重复交易。
9+
- **确定合约账户地址**:在 `合约创建` 模式下,nonce 和发送者地址一起用于确定合约账户地址。
10+
- **替换交易**:当交易因低 Gas 费卡住时,矿工通常允许用相同 nonce 的交易替换原交易。一些钱包可能提供取消交易的选项,这本质上是发送一个新的交易,其具有相同的 nonce、更高的 Gas 价格和 0 的数值,从而覆盖原来的待处理交易。然而,替换交易的成功并不保证,因为这取决于矿工的行为和网络条件。
11+
12+
- **gasPrice ($T_p$)**: 一个整数值,表示每单位 Gas 支付的 Wei 数量。**Wei** 是以太坊中最小的单位。$1 \textnormal{ETH} = 10^{18} \textnormal{Wei}$。Gas 价格用于决定交易的执行优先级。Gas 价格越高,交易越有可能被矿工优先打包进区块。
13+
14+
- **gasLimit ($T_g$)**: 一个整数值,表示该交易执行时允许使用的最大 Gas 数量。如果执行过程中 Gas 超过了 gasLimit,交易将被停止。
15+
16+
- **to ($T_t$)**: 交易接收方的 20 字节地址。`to` 字段还决定了交易的模式或用途:
17+
18+
| `to` 的值 | 交易模式 | 描述 |
19+
| ---------------- | --------------------- | --------------------------------------------------------- |
20+
| __ | 合约创建模式 | 该交易用于创建一个新的合约账户。 |
21+
| 外部账户 | 价值转移 | 该交易用于向一个外部账户转移以太币。 |
22+
| 合约账户 | 合约执行 | 该交易用于调用现有的智能合约代码。 |
23+
24+
- **value ($T_v$)**: 一个整数值,表示转移到此交易接收方的 Wei 数量。在 `合约创建` 模式下,value 是新创建合约账户的初始余额。
25+
26+
- **data ($T_d$) 或 init($T_i$)**: 一个无限大小的字节数组,指定 EVM 的输入。在 `合约创建` 模式下,此值被视为 `初始化字节码`,否则是 `输入数据` 的字节数组。
27+
28+
- **Signature ($T_v, T_r, T_s$)**: [ECDSA](/wiki/Cryptography/ecdsa.md) 签名,由发送方提供。
29+
30+
---
31+
32+
## 合约创建
33+
34+
让我们将以下代码部署到一个新的合约账户:
35+
36+
```bash
37+
[00] PUSH1 06 // 推入 06
38+
[02] PUSH1 07 // 推入 07
39+
[04] MUL // 乘法
40+
[05] PUSH1 0 // 推入 00 (存储地址)
41+
[07] SSTORE // 将结果存储到存储槽 00
42+
```
43+
44+
括号内的数字表示指令的偏移量。对应的字节码:
45+
46+
```bash
47+
6006600702600055
48+
```
49+
50+
现在,让我们准备交易的 `init` 值,以部署这个字节码。实际上,`init` 由两个片段组成:
51+
52+
```
53+
<init bytecode> <runtime bytecode>
54+
```
55+
56+
`init` 仅在账户创建时由 EVM 执行一次。`init` 代码执行的返回值是 **runtime bytecode**,它存储为合约账户的一部分。每次合约账户收到交易时,都会执行 runtime bytecode。
57+
58+
让我们准备我们的 `init` 代码,使其返回我们的 runtime 代码:
59+
60+
```bash
61+
// 1. Copy to memory
62+
[00] PUSH1 08 // PUSH1 08 (length of our runtime code)
63+
[02] PUSH1 0c // PUSH1 0c (offset of the runtime code in init)
64+
[04] PUSH1 00 // PUSH1 00 (destination in memory)
65+
[06] CODECOPY // Copy code running in current environment to memory
66+
// 2. Return from memory
67+
[07] PUSH1 08 // PUSH1 08 (length of return data)
68+
[09] PUSH1 00 // PUSH1 00 (memory location to return from)
69+
[0b] RETURN // Return the runtime code and halt execution
70+
// 3. Runtime code (8 bytes long)
71+
[0c] PUSH1 06
72+
[0e] PUSH1 07
73+
[10] MUL
74+
[11] PUSH1 0
75+
[13] SSTORE
76+
```
77+
78+
这段代码做了两件简单的事情:首先,将 runtime 字节码复制到内存中,然后从内存中返回 runtime 字节码。
79+
80+
`init` 字节码:
81+
82+
```javascript
83+
6008600c60003960086000f36006600702600055
84+
```
85+
86+
接下来,准备交易的 payload:
87+
88+
```javascript
89+
[
90+
"0x", // nonce (zero nonce, since first transaction)
91+
"0x77359400", // gasPrice (we're paying 2000000000 wei per unit of gas)
92+
"0x13880", // gasLimit (80000 is standard gas for deployment)
93+
"0x", // to address (empty in contract creation mode)
94+
"0x05", //value (we'll be nice and send 5 wei to our new contract)
95+
"0x6008600c60003960086000f36006600702600055", // init code
96+
];
97+
```
98+
99+
> payload 的排列需要遵循特定的顺序。
100+
101+
对于这个例子,我们将使用 [Foundry](https://getfoundry.sh/) 在本地部署交易。Foundry 是一个以太坊开发工具包,提供了以下命令行工具:
102+
103+
- **Anvil** : 一个本地以太坊节点,专为开发场景设计。
104+
- **Cast**: 一个用于执行以太坊 RPC 调用的工具。
105+
106+
安装并启动 [anvil](https://book.getfoundry.sh/anvil/) 本地节点。
107+
108+
```
109+
$ anvil
110+
111+
112+
_ _
113+
(_) | |
114+
__ _ _ __ __ __ _ | |
115+
/ _` | | '_ \ \ \ / / | | | |
116+
| (_| | | | | | \ V / | | | |
117+
\__,_| |_| |_| \_/ |_| |_|
118+
119+
0.2.0 (5c3b075 2024-03-08T00:17:08.007462509Z)
120+
https://github.com/foundry-rs/foundry
121+
122+
Available Accounts
123+
==================
124+
125+
(0) "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" (10000.000000000000000000 ETH)
126+
.....
127+
128+
Private Keys
129+
==================
130+
131+
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
132+
.....
133+
Listening on 127.0.0.1:8545
134+
```
135+
136+
使用 anvil 的 dummy 账户签署交易:
137+
138+
```bash
139+
$ node sign.js '[ "0x", "0x77359400", "0x13880", "0x", "0x05", "0x6008600c60003960086000f36006600702600055" ]' ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
140+
141+
f864808477359400830138808005946008600c60003960086000f360066007026000551ca01446316c9bdcbe0cb87fac0b08a00e59552634c96d0d6e2bd522ea0db827c1d0a0170680b6c348610ef150c1b443152214203c7f66288ea6332579c0cdfa86cc3f
142+
```
143+
144+
> 请参阅 **附录 A** 以获取 `sign.js` 辅助脚本。
145+
146+
最后,使用 [cast](https://book.getfoundry.sh/cast/) 提交交易:
147+
148+
```javascript
149+
$ cast publish f864808477359400830138808005946008600c60003960086000f360066007026000551ca01446316c9bdcbe0cb87fac0b08a00e59552634c96d0d6e2bd522ea0db827c1d0a0170680b6c348610ef150c1b443152214203c7f66288ea6332579c0cdfa86cc3f
150+
151+
{
152+
"transactionHash": "0xdfaf2817f19963846490b330ae33eba7b42872e8c8bd111c8d7ea3846c84cd51",
153+
"transactionIndex": "0x0",
154+
"blockHash": "0xfde1475a716583d847f858c5db3e54156983b39e3dbefaa5829416e6e60a788a",
155+
"blockNumber": "0x1",
156+
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
157+
"to": null,
158+
"cumulativeGasUsed": "0xd67e",
159+
"gasUsed": "0xd67e",
160+
// Newly created contract address 👇
161+
"contractAddress": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
162+
"logs": [],
163+
"status": "0x1",
164+
"logsBloom": "0x0...",
165+
"effectiveGasPrice": "0x77359400"
166+
}
167+
```
168+
169+
查询本地 `anvil` 节点确认代码已部署:
170+
171+
```bash
172+
$ cast code 0x5fbdb2315678afecb367f032d93f642f64180aa3
173+
0x6006600702600055
174+
```
175+
176+
初始余额可用:
177+
178+
```bash
179+
$ cast balance 0x5fbdb2315678afecb367f032d93f642f64180aa3
180+
5
181+
```
182+
183+
---
184+
185+
下图模拟了合约创建的过程:
186+
187+
![Contract creation](images/evm/create-contract.gif)
188+
189+
## 合约代码执行
190+
191+
我们部署的这个简单合约功能是将 6 和 7 相乘并把结果保存到存储槽 0。现在让我们发送一笔交易来执行这个合约。
192+
193+
这笔交易的 payload 结构和之前类似,但有几点不同:`to` 字段需要填入我们刚才部署的智能合约地址,而 `value``data` 字段则留空:
194+
195+
```javascript
196+
[
197+
"0x1", // nonce (increased by 1)
198+
"0x77359400", // gasPrice (we're paying 2000000000 wei per unit of gas)
199+
"0x13880", // gasLimit (80000 is standard gas for deployment)
200+
"0x5fbdb2315678afecb367f032d93f642f64180aa3", // to address ( address of our smart contract)
201+
"0x", // value (empty; not sending any ether)
202+
"0x", // data (empty)
203+
];
204+
```
205+
206+
对交易进行签名:
207+
208+
```bash
209+
$ node sign.js '[ "0x1", "0x77359400", "0x13880", "0x5fbdb2315678afecb367f032d93f642f64180aa3", "0x", "0x"]' ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
210+
211+
f86401847735940083013880945fbdb2315678afecb367f032d93f642f64180aa380801ba047ae110d52f7879f0ad214784168406f6cbb6e72e0cab59fa4df93da6494b578a02c72fcdea5b7838b520664186707d1465596e4ad4eaf8781a721530f8b8dd5f2
212+
```
213+
214+
发布交易:
215+
216+
```bash
217+
$ cast publish f86401847735940083013880945fbdb2315678afecb367f032d93f642f64180aa380801ba047ae110d52f7879f0ad214784168406f6cbb6e72e0cab59fa4df93da6494b578a02c72fcdea5b7838b520664186707d1465596e4ad4eaf8781a721530f8b8dd5f2
218+
219+
{
220+
"transactionHash": "0xc82a658b947c6083de71a0c587322e8335448e65e7310c04832e477558b2b0ef",
221+
"transactionIndex": "0x0",
222+
"blockHash": "0x40dc37d9933773598094ec0147bef5dfe72e9654025bfaa80c4cdbf634421384",
223+
"blockNumber": "0x2",
224+
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
225+
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
226+
"cumulativeGasUsed": "0xa86a",
227+
"gasUsed": "0xa86a",
228+
"contractAddress": null,
229+
"logs": [],
230+
"status": "0x1",
231+
"logsBloom": "0x0...",
232+
"effectiveGasPrice": "0x77359400"
233+
}
234+
```
235+
236+
使用 cast 读取存储槽 **0** 的值:
237+
238+
```bash
239+
$ cast storage 0x5fbdb2315678afecb367f032d93f642f64180aa3 0x
240+
0x000000000000000000000000000000000000000000000000000000000000002a
241+
```
242+
243+
果然,结果正是 [42](<https://simple.wikipedia.org/wiki/42_(answer)>) (0x2a) 🎉。
244+
245+
---
246+
247+
合约执行的模拟:
248+
249+
![合约执行](images/evm/contract-execution.gif)
250+
251+
## 附录 A:交易签名器
252+
253+
`signer.js`:一个用于签署交易的简单 [node.js](https://nodejs.org/) 脚本。请看注释中的说明:
254+
255+
```javascript
256+
/**
257+
* 用于签署交易 payload 数组的工具脚本。
258+
* 用法:node sign.js '[payload]' [private key]
259+
*/
260+
261+
const { rlp, keccak256, ecsign } = require("ethereumjs-util");
262+
263+
// 解析命令行参数
264+
const payload = JSON.parse(process.argv[2]);
265+
const privateKey = Buffer.from(process.argv[3].replace("0x", ""), "hex");
266+
267+
// 验证私钥长度
268+
if (privateKey.length != 32) {
269+
console.error("私钥必须是64个字符长!");
270+
process.exit(1);
271+
}
272+
273+
// 第1步:将 payload 编码为 RLP 格式
274+
// 了解更多:https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
275+
const unsignedRLP = rlp.encode(payload);
276+
277+
// 第2步:对 RLP 编码后的 payload 进行哈希
278+
// 了解更多:https://ethereum.org/en/glossary/#keccak-256
279+
const messageHash = keccak256(unsignedRLP);
280+
281+
// 第3步:签名消息
282+
// 了解更多:https://epf.wiki/#/wiki/Cryptography/ecdsa
283+
const { v, r, s } = ecsign(messageHash, privateKey);
284+
285+
// 第4步:将签名附加到 payload
286+
payload.push(
287+
"0x".concat(v.toString(16)),
288+
"0x".concat(r.toString("hex")),
289+
"0x".concat(s.toString("hex"))
290+
);
291+
292+
// 第5步:输出 RLP 编码后的已签名交易
293+
console.log(rlp.encode(payload).toString("hex"));
294+
```
295+
296+
## 更多资源
297+
- 📝 Gavin Wood, ["Ethereum Yellow Paper."](https://ethereum.github.io/yellowpaper/paper.pdf)
298+
- 📘 Andreas M. Antonopoulos, Gavin Wood, ["Mastering Ethereum."](https://github.com/ethereumbook/ethereumbook)
299+
- 📝 Ethereum.org, ["RLP Encoding."](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/)
300+
- 📝 Ethereum.org, ["Transactions."](https://ethereum.org/en/developers/docs/transactions/)
301+
- 📝 Random Notes, ["Signing transactions the hard way."](https://lsongnotes.wordpress.com/2018/01/14/signing-an-ethereum-transaction-the-hard-way/)[archived](https://web.archive.org/web/20240229045603/https://lsongnotes.wordpress.com/2018/01/14/signing-an-ethereum-transaction-the-hard-way/)
302+
- 🎥 Lefteris Karapetsas, ["Understanding Transactions in EVM-Compatible Blockchains."](https://archive.devcon.org/archive/watch/6/understanding-transactions-in-evm-compatible-blockchains-powered-by-opensource/?tab=YouTube)
303+
- 🎥 Austin Griffith, ["Transactions - ETH.BUILD."](https://www.youtube.com/watch?v=er-0ihqFQB0)
304+
- 🧮 Paradigm, ["Foundry: Ethereum development toolkit."](https://github.com/foundry-rs/foundry)

0 commit comments

Comments
 (0)