0.背景:以太坊的transaction
交易(Transaction)是区块链中最基本也是最核心的一个概念。在以太坊中,交易更是重中之重,因为以太坊是一个智能合约平台,以太坊上的应用都是通过智能合约与区块链进行交互,而智能合约的执行是由交易触发的,没有交易,智能合约就是一段死的代码,可以说在以太坊中,一切都源于交易。
type Transactionstruct {
data txdata
// caches
hashatomic.Value
sizeatomic.Value
fromatomic.Value
}
在这个结构体里面只有一个data字段,它是txdata类型的,其他三个字段hash、size、from是缓存字段:
-
hash
: 交易Hash -
size
: 交易大小 -
from
: 发送方地址
txdata
也是一个结构体,它里面定义了交易的具体的字段:
type txdata struct {
AccountNonce uint64
Price, GasLimit *big.Int
Recipient *common.Address `rlp:"nil"` // nil means contract creation
Amount *big.Int
Payload []byte
V *big.Int // signature
R, S *big.Int // signature
}
txdata
这个txdata
中的Payload
字段就是展示以太坊“魔法”的关键。当和以太坊网络进行交互时,不管是直接转账还是合约调用,抑或是合约部署,其实都是向以太坊网络发起了一笔Transaction,这个Transaction中的Payload就是每次交易的输入数据。如果是转账,Payload
就是0x
为空,不用传入,因为“from”、“to”和“value”字段已经可以确定这笔交易由“谁”,转“多少ETH”,到“谁”去了;如果这笔交易是调用合约,Payload
中就需要包括你所需要调用合约所需的所有信息,例如合约方法、参数;如果是部署合约的话,Payload
就是合约编译后的字节码。
由于在ethers.js等以太库代码中,Payload
字段名用的是data
,所以为了方便演示,以下都用data
来替代Payload
。
1.转账时的data
我们使用hardhat的task,建立了以下的任务:
task("test-transaction", "This task is broken")
.setAction(async () => {
const signer = await ethers.getSigner();
const tx = await signer.sendTransaction({
to: "0x13a6D1fe418de7e5B03Fb4a15352DfeA3249eAA4",
value: ethers.utils.parseEther("1.0")
});
console.log("tx=", tx);
});
这里我们使用signer.sendTransaction
函数,给0x13a...eAA4这个地址转账了1.0 ETH,输出tx的值:
可以发现这里面的
data
值为
0x
毫无疑问,在这次转账中,是没有msg.data的。
2.合约交互时的data
有下面A合约:
contract A {
uint256 x;
function set(uint256 _x) external {
x = _x;
}
}
这是一个非常简单的合约,由一个简单的storage变量x和set函数组成。
我们使用hardhat框架写了个task:
task("test-transaction", "This task is broken")
.setAction(async () => {
const A = await ethers.getContractFactory("A");
const contract = await A.deploy();
const tx = await contract.set(100);
console.log("tx=", tx);
await tx.wait();
});
输出看看tx的结果:
tx交易结构体其中我们的探究重点是
data
:
0x60fe47b10000000000000000000000000000000000000000000000000000000000000064
因为我们执行的是set函数,我们知道在以太坊中,这个函数的函数签名是set(uint256)
,我们将这个函数签名进行keccak256
的运算:
提取前8位,即
60fe47b1
,这就是函数签名的标记,我们data
中的前八位,也是这个值。然后我们在执行时输入了参数100,这是10进制的,换算成16进制就是0x64,而这又是uint256类型,总共256位,16进制的话就需要64位,因此要向前填充62个0即
0000000000000000000000000000000000000000000000000000000000000064
。这两部分字符串拼接起来,就是
data
的值。
那么当函数签名中参数不止1个的时候,是什么情况呢?
有合约A如下:
contract A {
uint256 x;
address account;
function set(uint256 _x, address _account) external {
x = _x;
account = _account;
}
}
task如下:
task("test-transaction", "This task is broken")
.setAction(async () => {
const A = await ethers.getContractFactory("A");
const contract = await A.deploy();
const tx = await contract.set(100, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
console.log("tx=", tx);
await tx.wait();
});
输出的tx结构体如下:
tx结构体
其中的data为:
0x2f30c6f60000000000000000000000000000000000000000000000000000000000000064000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
可以发现这个data明显比上一个要长得多,还是跟上一个一样分析。
函数签名set(uint256,bool)
做keccak256
运算的结果的前8位是62f46d56
,这跟我们的data前八位一致;
然后第一个参数uint256类型的100,跟上一个一样都是62个0加上64即0000000000000000000000000000000000000000000000000000000000000064
;
第二个参数是address类型的0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,可以看到末尾的数值跟这个地址相同,同样填充0至64位(16进制),即为000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
。
同样的,这三部分字符串拼接起来,就是tx中的data
的值。
这里举了两个例子说明输入参数的拼接,其实不同的参数也有不同的处理方法,像string就会比一般的uint、bool、address这种非引用型变量要更复杂些,但是总的来说,思路是一样的,都是进行拼接得到的。
3.部署合约时候的data
hardhat的task如下:
task("test-transaction", "This task is broken")
.setAction(async () => {
const A = await ethers.getContractFactory("A");
const contract = await A.deploy();
const tx = contract.deployTransaction;
console.log("tx=", tx);
});
输出的tx结构体如下图:
tx结构体可以发现这里面的
data
,明显是一大串合约字节码,而且这里面to
的值时null。我们打开hardhat编译过后的A.json文件: A.json
这个文件中除了abi以外,还可以看到这个
bytecode
,将其和transaction的data
字段进行比对,会发现一模一样。关于
bytecode
和deployedBytecode
的区别,可以看这篇文章。其实deployedBytecode
是明显要比bytecode
短的,这是因为bytecode里包含了一些constructor操作,以及合约收款验证等预置操作,这些预置操作只有在部署合约的时候才会运行一次,之后再也不会运行,所以Transaction中的data是bytecode
,而以太坊节点保存的字节码,则是deployedBytecode
。
网友评论