以太坊智能合约字节码

最近为了分析某个区块链项目,花了不少时间研究智能合约。看了文档和不少文章,把关于 abi 和字节码方面的知识也重新学习了一遍。

# abi.encode vs abi.encodePacked

abi(application binary interface) (opens new window) 是和智能合约交互的标准方式。 调用智能合约的时候需要输入的信息需要编码成二进制,solidity 提供了 abi.encode 方法完成这个功能。 同时提供了 abi.decode 用来解码; abi.encodePacked 用来压缩编码,但因为它是有歧义的,所以没有对应的 abi.decodePacked

abi.decode 解码的时候需要提供结构信息,因为二进制数据不包含结构信息。所以,解码的时候需要搭配 abi.json 文件使用。

abi.encodePacked 编码之后的体积会大幅度减小,但因此也会导致碰撞风险。

编码动态类型的时候 abi.encode 会包含其位置、长度、数据本身,并以 32 字节为单位补 0。而 abi.encodePacked 会直接就地编码,也不会补 0。 所以,有超过 1 个以上动态类型的时候,是无法正常解码的,应避免使用它。

如下,一个典型的对比:

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

部署合约还是通过发送交易的形式。目前发送一个交易需要包含:

{
  "from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155", // 发送人地址
  "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", // 目标地址
  "gas": "0x76c0", // gas 数量 30400
  "gasPrice": "0x9184e72a000", // gas 价格 10000000000000
  "value": "0x9184e72a", // ETH 数量 2441406250
  "data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" // 其他数据
}

通过私钥对以上数据签名之后得到 v r s,一并广播给节点即可。

通过不同的参数,可以实现 3 种不同的操作:

  1. 转账 ETH:要求 tovalue 有效,data 为空
  2. 调用合约方法:to 为合约地址,value 可有可无数值,data 为方法签名和参数
  3. 部署合约:to 为空,value 为空,data 为合约 source bytecode+constructor arguments

关于 bytecode:

使用 solidity 编写合约之后编译得到第一份字节码,称之为 source bytecode

使用 source bytecode 和构建函数参数(使用 abi.encode 编码)拼接之后得到 creation bytecodeinit code,将其作为交易的 data 内容来部署。

部署成功之后,链上会存储一份字节码,去除了构建函数逻辑和参数,称之为 runtime bytecodedeployed 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

  1. 获取 abi 和 bin 文件
    • 使用 hardhat 开发的项目:npx hardhat export-abi --no-compile && npx hardhat export-bytecode --no-compile
    • 复制现有合约:使用 eth_getTransactionByHash 获取 data,再修改构建函数参数即可。
  2. 使用 abigen 生成代码
    • abigen --abi=./Vote.abi --bin=./Vote.bin --pkg=vote --out=./vote/vote.go
  3. 使用 go-ethereum 签名并发送交易
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 生成的文件里,bytecodesource bytecode。 而 deployedBytecoderuntime bytecode,但可能有一点点差异。

# 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 一般都很长,凭肉眼很难对比出差异。所以写了个小工具对比,高亮有差异的部分。

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]);

# 参考

Understanding Bytecode on Ethereum (opens new window)