以太坊上的几种签名: eth_sign, personal_sign, eth_signTypedData
以太坊的签名算法是ECDSA-secp256k1,以下介绍的每一种签名都是基于该算法,只是用来签名的数据不同。1 交易签名 eth_sign以太坊上,签名之前的交易结构如下。let transaction = { to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: ethers.utils.parseEther('1'), data: '0xE0A293E08F72454CEd99E1769c3ebd21fD2C20a1', gasLimit: '22000', maxFeePerGas: ethers.utils.parseUnits('20', 'gwei'), maxPriorityFeePerGas: ethers.utils.parseUnits('5', 'gwei'), nonce: 1, type: 2, chainId: chainId, // 31337 } 各项目的含义不再介绍,有兴趣可以查阅https://ethereum.org/en/developers/docs/transactions/...
Solidity学习-可升级合约(Transparent/UUPS/Beacon)
以太坊合约原生并不支持升级,目前的升级一般是采用代理模式实现的。代理模式在代理模式中,有2个合约:Proxy和Implementation,用户总是和Proxy进行交互。Proxy在收到用户的调用请求后,并不执行自身的代码,而是通过delegatecall去执行Implementation合约的代码。delegatecall的特殊之处就是,它并不切换上下文,因此Implementation的代码所处理的存储空间是Proxy合约的存储空间,而非自己的。 Proxy合约里存储了Implementation合约的地址,这个地址是可修改的,当我们需要进行合约升级的时候,只需要重新部署一个新的Implementation合约,同时把Proxy合约中存储的地址改成新合约的地址就行了。升级前后,合约的存储空间没有任何变化(依然是Proxy的存储空间),地址也没有变化(依然是Proxy的地址),因此升级过程对用户完全透明。 合约升级之后,要保证storage变量的兼容性。新版本可以在旧版本的变量之后增加新的变量,但是不可以删除或者修改旧版本的变量。因为自始自终,变量都只存储在Proxy合约里,升...
用golang开发ethereum
之前看到一个用golang开发以太坊的教程 https://goethereumbook.org/zh/ 这个教程非常详细,然而它太陈旧了,目前很多go-ethereum函数接口已经有所修改。最明显的是,EIP1559之后,交易的格式的已经大不一样。因此,我基于上述教程,依据最新的go-ethereum(v1.10.26)对代码demo进行了修改。 用golang开发以太坊的一个好处是,可以很方便的查看和调试geth的源码,可以帮助我们更深入地理解以太坊的底层实现。 代码库为 https://github.com/CryptoRbtree/goeth-client 下面对主要功能做简单介绍。1 账户首先需要调用ethclient.DialContext连接一个rpc,这个rpc可以是外部服务商提供的公链rpc,也可以是本地区块链的。var ( ctx = context.Background() url = "https://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID") client, err = ethclie...
以太坊上的几种签名: eth_sign, personal_sign, eth_signTypedData
以太坊的签名算法是ECDSA-secp256k1,以下介绍的每一种签名都是基于该算法,只是用来签名的数据不同。1 交易签名 eth_sign以太坊上,签名之前的交易结构如下。let transaction = { to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: ethers.utils.parseEther('1'), data: '0xE0A293E08F72454CEd99E1769c3ebd21fD2C20a1', gasLimit: '22000', maxFeePerGas: ethers.utils.parseUnits('20', 'gwei'), maxPriorityFeePerGas: ethers.utils.parseUnits('5', 'gwei'), nonce: 1, type: 2, chainId: chainId, // 31337 } 各项目的含义不再介绍,有兴趣可以查阅https://ethereum.org/en/developers/docs/transactions/...
Solidity学习-可升级合约(Transparent/UUPS/Beacon)
以太坊合约原生并不支持升级,目前的升级一般是采用代理模式实现的。代理模式在代理模式中,有2个合约:Proxy和Implementation,用户总是和Proxy进行交互。Proxy在收到用户的调用请求后,并不执行自身的代码,而是通过delegatecall去执行Implementation合约的代码。delegatecall的特殊之处就是,它并不切换上下文,因此Implementation的代码所处理的存储空间是Proxy合约的存储空间,而非自己的。 Proxy合约里存储了Implementation合约的地址,这个地址是可修改的,当我们需要进行合约升级的时候,只需要重新部署一个新的Implementation合约,同时把Proxy合约中存储的地址改成新合约的地址就行了。升级前后,合约的存储空间没有任何变化(依然是Proxy的存储空间),地址也没有变化(依然是Proxy的地址),因此升级过程对用户完全透明。 合约升级之后,要保证storage变量的兼容性。新版本可以在旧版本的变量之后增加新的变量,但是不可以删除或者修改旧版本的变量。因为自始自终,变量都只存储在Proxy合约里,升...
用golang开发ethereum
之前看到一个用golang开发以太坊的教程 https://goethereumbook.org/zh/ 这个教程非常详细,然而它太陈旧了,目前很多go-ethereum函数接口已经有所修改。最明显的是,EIP1559之后,交易的格式的已经大不一样。因此,我基于上述教程,依据最新的go-ethereum(v1.10.26)对代码demo进行了修改。 用golang开发以太坊的一个好处是,可以很方便的查看和调试geth的源码,可以帮助我们更深入地理解以太坊的底层实现。 代码库为 https://github.com/CryptoRbtree/goeth-client 下面对主要功能做简单介绍。1 账户首先需要调用ethclient.DialContext连接一个rpc,这个rpc可以是外部服务商提供的公链rpc,也可以是本地区块链的。var ( ctx = context.Background() url = "https://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID") client, err = ethclie...
Share Dialog
Share Dialog
Subscribe to rbtree
Subscribe to rbtree
<100 subscribers
<100 subscribers
之前分析过合约创建的过程
https://mirror.xyz/rbtree.eth/15XiwTWWFYdLdhf-QSqTWsPuZliIgpP1o-_bz0TdGbs
构造函数是一个特殊的函数,它只在合约部署的时候执行一次,部署之后的字节码是不包含构造函数逻辑的。在构造函数执行时,合约还没有被创建完成,那么此时如果访问这个合约的变量/函数,会发生什么事情呢?
在构造函数里,能否获取合约地址?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
address public addr;
constructor() {
addr = address(this);
}
}

在remix里部署后我们可以发现,addr的值的确是合约的地址,这说明在构造函数里,可以获取合约地址。
合约的部署实际上有两种方式,一种是传统的CREATE,一种是后来新增的CREATE2.
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md
对于CREATE,地址计算方式为:keccak256(rlp([sender, nonce]))
而对于CREATE2,地址计算方式为:keccak256( 0xff ++ sender ++ salt ++ keccak256(init_code))
这些并不需要等待合约部署完毕才能计算,因此构造函数里可以正确取得合约地址。
在构造函数里,能否获取合约地址的code和codehash?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
bytes public code0;
bytes public code1;
bytes32 public codehash0;
bytes32 public codehash1;
constructor() {
code0 = address(this).code;
codehash0 = address(this).codehash;
}
function run() external {
code1 = address(this).code;
codehash1 = address(this).codehash;
}
}


我们可以看到,在构造函数中,获得的code为空,相应的codehash显然也是错误的,实际上构造函数中获取的codehash是空串的hash。
如果你经常看代码的话,可能看见下面这个用来判断一个地址是否为合约地址的函数:
function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
}
实际上,这个代码是不可靠的,只有在一个合约已经完成构造之后、被selfdestruct之前,判断才会有效。某个code为空的地址,可能目前还没有合约但将来会被部署合约(合约地址可以预计算)、可能有合约正在构造、可能曾经被部署过合约但已经被销毁。
所以,千万不可以依赖这个方法预防合约地址的攻击。

在构造的时候,向合约转账,会触发合约的receive/fallback吗?
我们有如下两个合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VisitWhenConstructing {
constructor() payable {}
function sendEther() external {
(bool result,) = (msg.sender).call{value: address(this).balance / 2}("");
require(result, "sendEther fail!");
}
}
contract TestConstructor {
// 下面这个地址是VisitWhenConstructing的部署地址
address constant visit = 0x7EF2e0048f5bAeDe046f6BF797943daF4ED8CB47;
event Receive(uint);
event Fallback(uint);
constructor() {
VisitWhenConstructing(visit).sendEther();
}
function run() external {
VisitWhenConstructing(visit).sendEther();
}
receive() payable external {
emit Receive(msg.value);
}
fallback() payable external {
emit Fallback(msg.value);
}
}
我们首先部署VisitWhenConstructing,给它赋予1ETH。

接下来部署TestConstructor,观察会发生什么。


我们可以看出,转账成功了,TestConstructor的确收到了ETH,但是日志空空如也,说明receive和fallback里的日志都没有被触发。
其实也很好理解,我们上面说过,在构造时地址的code为空,此时没有办法判断该地址是合约,因此该地址此时会被当成EOA地址,当然不会触发receive或fallback。
在部署完成之后,我们运行run。


可见此时转账会正常触发receive函数,因为定义了receive,所以不会触发fallback了。
在构造函数中,是否可以调用合约中的其他函数?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VisitWhenConstructing {
function callSender() external {
(bool result,) = (msg.sender).call(abi.encodeWithSignature("fun2()"));
require(result, "call fail!");
// TestConstructor(msg.sender).fun2(); // revert
}
}
contract TestConstructor {
// 下面这个地址是VisitWhenConstructing的部署地址
address constant visit = 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B;
event Fun1();
event Fun2();
constructor() {
fun1();
VisitWhenConstructing(visit).callSender();
}
function fun1() internal {
emit Fun1();
}
function fun2() external {
emit Fun2();
}
}
调用函数有两种方式,一种是内部调用,一种是外部调用,我们分别用fun1和fun2来验证。

我们看一下日志可以发现,只有内部调用的fun1被执行了,外部调用的fun2并没有被执行。
这个原理应该和刚才的转账逻辑一样,外部执行的时候,因为该地址code为空,所以并不知道这个地址是合约地址,因此依然把该地址作为EOA。
虽然我们加了require(result, "call fail!");但交易并没有revert。这是因为对EOA而言,calldata是无效的,evm也不会对calldata做任何检查,因此也没有触发错误。
不过,如果我把VisitWhenConstructing里对call2的调用换成直接调用,而不用low-level call,会revert。这个现象我目前还没有想通,如果此时依然把TestConstructor地址当成EOA地址,也应该不报错才对。
此外,我还想到了一种验证方式,那就是自己对自己进行外部函数调用。
执行TestConstructor(this).fun2();的时候会直接revert。这个和上面的从其他合约做外部函数调用是一样的结果。
contract TestConstructor {
event Fun1();
event Fun2();
constructor() {
fun1();
TestConstructor(this).fun2();
}
function fun1() internal {
emit Fun1();
}
function fun2() external {
emit Fun2();
}
}
immutable变量是可以在构造的时候才进行初始化的,理论上依赖immutable变量的函数需要等初始化之后才可以被调用。
这一小节我们不必看外部调用的情形了,外部调用一定是不可以的(和第4小节的情况一样)。我们只看内部调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
uint immutable val;
event PrintVal(uint);
constructor() {
val = 1234;
uint v = getVal();
emit PrintVal(v);
//val = 1234;
}
function getVal() internal view returns (uint) {
return val;
}
}

一切正常,构造函数中可以使用immutable变量。
不过上面的例子immutable变量的初始化在使用之前,如果颠倒位置,把val=1234拿到最后呢?
尝试一下就会发现,编译器报错:
TypeError: Immutable variables cannot be read before they are initialized.
这很合理,很安全!
最后,总结一下构造函数中使用如下合约变量/函数的结果:

之前分析过合约创建的过程
https://mirror.xyz/rbtree.eth/15XiwTWWFYdLdhf-QSqTWsPuZliIgpP1o-_bz0TdGbs
构造函数是一个特殊的函数,它只在合约部署的时候执行一次,部署之后的字节码是不包含构造函数逻辑的。在构造函数执行时,合约还没有被创建完成,那么此时如果访问这个合约的变量/函数,会发生什么事情呢?
在构造函数里,能否获取合约地址?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
address public addr;
constructor() {
addr = address(this);
}
}

在remix里部署后我们可以发现,addr的值的确是合约的地址,这说明在构造函数里,可以获取合约地址。
合约的部署实际上有两种方式,一种是传统的CREATE,一种是后来新增的CREATE2.
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md
对于CREATE,地址计算方式为:keccak256(rlp([sender, nonce]))
而对于CREATE2,地址计算方式为:keccak256( 0xff ++ sender ++ salt ++ keccak256(init_code))
这些并不需要等待合约部署完毕才能计算,因此构造函数里可以正确取得合约地址。
在构造函数里,能否获取合约地址的code和codehash?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
bytes public code0;
bytes public code1;
bytes32 public codehash0;
bytes32 public codehash1;
constructor() {
code0 = address(this).code;
codehash0 = address(this).codehash;
}
function run() external {
code1 = address(this).code;
codehash1 = address(this).codehash;
}
}


我们可以看到,在构造函数中,获得的code为空,相应的codehash显然也是错误的,实际上构造函数中获取的codehash是空串的hash。
如果你经常看代码的话,可能看见下面这个用来判断一个地址是否为合约地址的函数:
function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
}
实际上,这个代码是不可靠的,只有在一个合约已经完成构造之后、被selfdestruct之前,判断才会有效。某个code为空的地址,可能目前还没有合约但将来会被部署合约(合约地址可以预计算)、可能有合约正在构造、可能曾经被部署过合约但已经被销毁。
所以,千万不可以依赖这个方法预防合约地址的攻击。

在构造的时候,向合约转账,会触发合约的receive/fallback吗?
我们有如下两个合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VisitWhenConstructing {
constructor() payable {}
function sendEther() external {
(bool result,) = (msg.sender).call{value: address(this).balance / 2}("");
require(result, "sendEther fail!");
}
}
contract TestConstructor {
// 下面这个地址是VisitWhenConstructing的部署地址
address constant visit = 0x7EF2e0048f5bAeDe046f6BF797943daF4ED8CB47;
event Receive(uint);
event Fallback(uint);
constructor() {
VisitWhenConstructing(visit).sendEther();
}
function run() external {
VisitWhenConstructing(visit).sendEther();
}
receive() payable external {
emit Receive(msg.value);
}
fallback() payable external {
emit Fallback(msg.value);
}
}
我们首先部署VisitWhenConstructing,给它赋予1ETH。

接下来部署TestConstructor,观察会发生什么。


我们可以看出,转账成功了,TestConstructor的确收到了ETH,但是日志空空如也,说明receive和fallback里的日志都没有被触发。
其实也很好理解,我们上面说过,在构造时地址的code为空,此时没有办法判断该地址是合约,因此该地址此时会被当成EOA地址,当然不会触发receive或fallback。
在部署完成之后,我们运行run。


可见此时转账会正常触发receive函数,因为定义了receive,所以不会触发fallback了。
在构造函数中,是否可以调用合约中的其他函数?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VisitWhenConstructing {
function callSender() external {
(bool result,) = (msg.sender).call(abi.encodeWithSignature("fun2()"));
require(result, "call fail!");
// TestConstructor(msg.sender).fun2(); // revert
}
}
contract TestConstructor {
// 下面这个地址是VisitWhenConstructing的部署地址
address constant visit = 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B;
event Fun1();
event Fun2();
constructor() {
fun1();
VisitWhenConstructing(visit).callSender();
}
function fun1() internal {
emit Fun1();
}
function fun2() external {
emit Fun2();
}
}
调用函数有两种方式,一种是内部调用,一种是外部调用,我们分别用fun1和fun2来验证。

我们看一下日志可以发现,只有内部调用的fun1被执行了,外部调用的fun2并没有被执行。
这个原理应该和刚才的转账逻辑一样,外部执行的时候,因为该地址code为空,所以并不知道这个地址是合约地址,因此依然把该地址作为EOA。
虽然我们加了require(result, "call fail!");但交易并没有revert。这是因为对EOA而言,calldata是无效的,evm也不会对calldata做任何检查,因此也没有触发错误。
不过,如果我把VisitWhenConstructing里对call2的调用换成直接调用,而不用low-level call,会revert。这个现象我目前还没有想通,如果此时依然把TestConstructor地址当成EOA地址,也应该不报错才对。
此外,我还想到了一种验证方式,那就是自己对自己进行外部函数调用。
执行TestConstructor(this).fun2();的时候会直接revert。这个和上面的从其他合约做外部函数调用是一样的结果。
contract TestConstructor {
event Fun1();
event Fun2();
constructor() {
fun1();
TestConstructor(this).fun2();
}
function fun1() internal {
emit Fun1();
}
function fun2() external {
emit Fun2();
}
}
immutable变量是可以在构造的时候才进行初始化的,理论上依赖immutable变量的函数需要等初始化之后才可以被调用。
这一小节我们不必看外部调用的情形了,外部调用一定是不可以的(和第4小节的情况一样)。我们只看内部调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
uint immutable val;
event PrintVal(uint);
constructor() {
val = 1234;
uint v = getVal();
emit PrintVal(v);
//val = 1234;
}
function getVal() internal view returns (uint) {
return val;
}
}

一切正常,构造函数中可以使用immutable变量。
不过上面的例子immutable变量的初始化在使用之前,如果颠倒位置,把val=1234拿到最后呢?
尝试一下就会发现,编译器报错:
TypeError: Immutable variables cannot be read before they are initialized.
这很合理,很安全!
最后,总结一下构造函数中使用如下合约变量/函数的结果:

No activity yet