Appearance
最近为了分析某个区块链项目,花了不少时间研究智能合约。看了文档和不少文章,把关于 abi 和字节码方面的知识也重新学习了一遍。
abi.encode vs abi.encodePacked
abi(application binary interface) 是和智能合约交互的标准方式。 调用智能合约的时候需要输入的信息需要编码成二进制,solidity 提供了 abi.encode
方法完成这个功能。 同时提供了 abi.decode
用来解码; abi.encodePacked
用来压缩编码,但因为它是有歧义的,所以没有对应的 abi.decodePacked
。
abi.decode
解码的时候需要提供结构信息,因为二进制数据不包含结构信息。所以,解码的时候需要搭配 abi.json
文件使用。
abi.encodePacked
编码之后的体积会大幅度减小,但因此也会导致碰撞风险。
编码动态类型的时候 abi.encode
会包含其位置、长度、数据本身,并以 32 字节为单位补 0。而 abi.encodePacked
会直接就地编码,也不会补 0。 所以,有超过 1 个以上动态类型的时候,是无法正常解码的,应避免使用它。
如下,一个典型的对比:
js
console.logBytes(abi.encode("AB", "CD"));
// 0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002414200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024344000000000000000000000000000000000000000000000000000000000000
console.logBytes(abi.encode("A", "BCD"));
// 0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034243440000000000000000000000000000000000000000000000000000000000
console.logBytes(abi.encodePacked("AB", "CD"));
// 0x41424344
console.logBytes(abi.encodePacked("A", "BCD"));
// 0x41424344
Deploy contract
部署合约还是通过发送交易的形式。目前发送一个交易需要包含:
json
{
"from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155", // 发送人地址
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", // 目标地址
"gas": "0x76c0", // gas 数量 30400
"gasPrice": "0x9184e72a000", // gas 价格 10000000000000
"value": "0x9184e72a", // ETH 数量 2441406250
"data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" // 其他数据
}
通过私钥对以上数据签名之后得到 v r s
,一并广播给节点即可。
通过不同的参数,可以实现 3 种不同的操作:
- 转账 ETH:要求
to
和value
有效,data
为空 - 调用合约方法:
to
为合约地址,value
可有可无数值,data
为方法签名和参数 - 部署合约:
to
为空,value
为空,data
为合约source bytecode+constructor arguments
关于 bytecode:
使用 solidity 编写合约之后编译得到第一份字节码,称之为 source bytecode
。
使用 source bytecode
和构建函数参数(使用 abi.encode
编码)拼接之后得到 creation bytecode
或init code
,将其作为交易的 data 内容来部署。
部署成功之后,链上会存储一份字节码,去除了构建函数逻辑和参数
,称之为 runtime bytecode
或 deployed bytecode
。
Name | Description | On-chain Retrival | Off-chain Retrieval |
---|---|---|---|
Creation Bytecode | Code that generates the runtime bytecode | type(ContractName).creationCode | getTransactionByHash |
Runtime Bytecode | Code that is stored on-chain that describes a smart contract | extcodecopy(a) or type(ContractName).runtimeCode | getCode |
Bytecode | Umbrella term that encompasses both runtime bytecode and creation bytecode | NA | NA |
Deployed Bytecode | Same as runtime bytecode | extcodecopy(a) or type(ContractName).runtimeCode | getCode |
Init Code | Same as creation bytecode | type(ContractName).creationCode | getTransactionByHash |
Deploy with abi and bytecode in golang
- 获取 abi 和 bin 文件
- 使用 hardhat 开发的项目:
npx hardhat export-abi --no-compile && npx hardhat export-bytecode --no-compile
- 复制现有合约:使用
eth_getTransactionByHash
获取 data,再修改构建函数参数即可。
- 使用 hardhat 开发的项目:
- 使用 abigen 生成代码
abigen --abi=./Vote.abi --bin=./Vote.bin --pkg=vote --out=./vote/vote.go
- 使用 go-ethereum 签名并发送交易
go
func DeployVote(sender string, privKey *ecdsa.PrivateKey) (contractAddr, txHash string, err error) {
var client *ethclient.Client
// client, err := ethclient.Dial("https://rpc.ankr.com/bsc") // mainnet
client, err = ethclient.Dial("http://localhost:8545") // testnet
if err != nil {
log.Println(err)
return
}
// get nonce and gasPrice
nonce, err := client.PendingNonceAt(context.Background(), common.HexToAddress(sender))
if err != nil {
return
}
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
return
}
chainID, err := client.NetworkID(context.Background())
if err != nil {
return
}
auth, err := bind.NewKeyedTransactorWithChainID(privKey, chainID)
if err != nil {
return
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0) // in wei
auth.GasPrice = gasPrice
// deploy
address, tx, _, err := chiresverify.DeployChiresverify(auth, client, "Vote", []common.Address{
common.HexToAddress(sender),
}, big.NewInt(2))
if err != nil {
return
}
return address.Hex(), tx.Hash().Hex(), nil
}
Verify bytecodes
事实证明,使用 hardhat 直接部署和使用 golang 部署,只要参数一致,creation bytecode
就是一样的。 而 runtime bytecode
即使参数不一致也是一样的。
hardhat 生成的文件里,bytecode
即 source bytecode
。 而 deployedBytecode
即 runtime bytecode
,但可能有一点点差异。
sh
# get creation bytecode
curl --location --request POST 'localhost:8545/' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc":"2.0",
"method":"eth_getTransactionByHash",
"params":[
"0xd7e6915fba65cbbeba4d56d32dae6f5824354cffa77a5210fa33fd6c7fc43d38"
],
"id":1
}' | jq
# get runtime bytecode
curl --location --request POST 'localhost:8545/' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc":"2.0",
"method":"eth_getCode",
"params":[
"0x6fd34a045cd1b450aff0aca836e3e458c1230a05",
"0x25"
],
"id":1
}' | jq
字节码超长文本对比
因为 bytecode 一般都很长,凭肉眼很难对比出差异。所以写了个小工具对比,高亮有差异的部分。
js
let a = "..."; // bytecode1
let b = "..."; // bytecode2
let s1 = "";
let s2 = "";
let co1 = "\x1B[31m";
let co2 = "\033[0m";
let ss1 = [];
let ss2 = [];
for (let i = 0; i < a.length; i++) {
if (a[i] == b[i]) {
s1 += a[i];
s2 += b[i];
} else {
s1 += co1 + a[i] + co2;
s2 += co1 + b[i] + co2;
}
if (i > 0 && i % 64 == 0) {
ss1.push(s1);
ss2.push(s2);
s1 = "";
s2 = "";
}
}
ss1.push(s1);
ss2.push(s2);
for (let i = 0; i < ss1.length; i++) {
console.log("=====", i);
console.log(ss1[i]);
console.log(ss2[i]);