Skip to content
On this page

最近为了分析某个区块链项目,花了不少时间研究智能合约。看了文档和不少文章,把关于 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 种不同的操作:

  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

NameDescriptionOn-chain RetrivalOff-chain Retrieval
Creation BytecodeCode that generates the runtime bytecodetype(ContractName).creationCodegetTransactionByHash
Runtime BytecodeCode that is stored on-chain that describes a smart contractextcodecopy(a) or type(ContractName).runtimeCodegetCode
BytecodeUmbrella term that encompasses both runtime bytecode and creation bytecodeNANA
Deployed BytecodeSame as runtime bytecodeextcodecopy(a) or type(ContractName).runtimeCodegetCode
Init CodeSame as creation bytecodetype(ContractName).creationCodegetTransactionByHash

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 签名并发送交易
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 生成的文件里,bytecodesource bytecode。 而 deployedBytecoderuntime 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]);

参考

Understanding Bytecode on Ethereum