ETH 智能合约
# 一、什么是智能合约
智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。
智能合约的帐户保存了合约当前的运行状态:
- balance:当前余额
- nonce:交易次数
- code:合约代码
- storage:存储,数据结构是一棵 MPT
Solidity 是智能合约最常用的语言,语法上与 JavaScript 很接近。
# 二、外部账户如何调用智能合约
创建一个交易,接收地址为要调用的那个智能合约的地址,data 域填写要调用的函数及其参数的编码值。
SENDER_ADDRESS
是发起调用的账户地址。TO_CONTRACT_ADDRESS
是被调用的合约的地址。TX DATA
中给出了被调用的函数,如果这个函数有参数的话,那么参数的取值也是在这里说明的。VALUE
表示发起调用时转过去多少钱,这里是零,说明仅仅是为了调用它的函数,并不是真的要转账。GAS USED
表示这个交易花了多少汽油费。GAS PRICE
是单位汽油的价格。GAS LIMIT
是对这个交易,我最多愿意支付多少汽油。
# 三、一个合约如何调用另一个合约中的函数
# 3.1 直接调用
contract A {
event LogCallFoo(string str);
function foo(string str) returns (uint) {
emit LogCallFoo(str);
return 123;
}
}
contract B {
uint ua;
function callAFooDirectly(address addr) public {
A a = A(addr);
ua = a.foo("call foo directly");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果在执行 a.foo()
过程中抛出错误,则 callAFooDirectly
也抛出错误,本次调用全部回滚。ua
为执行 a.foo(“call foo directly”)
的返回值。
以太坊中规定一个交易只有外部账户才能发起,合约账户不能主动发起一个交易。 因此上面这个例子中,实际需要一个外部账户调用合约 B 中的 callAFooDirectly
函数,进而调用合约 A 中的 foo
函数。
# 3.2 使用 address 类型的 call() 函数
contract C {
function callAFooByCall(address addr) public returns (bool) {
bytes4 funcsig = bytes4(keccak256("foo(string)"));
if (addr.call(funcsig, "call foo by func call"))
return true;
return false;
}
}
2
3
4
5
6
7
8
call()
的第一个参数被编码成 4 个字节,表示要调用函数的签名,其他参数会被扩展到 32 字节,表示要调用函数的参数。这里相当于 A(addr).foo("call foo by func call")
。
该方法和直接调用的区别在于错误处理的不同,这里在调用出错后,call()
函数会返回 false
,表明这个调用是失败的,但 发起调用的这个函数并不会抛出异常。
# 3.3 代理调用 delegatecall()
该方法的使用与 call()
相同,只是不能使用 .value()
。
两者的区别在于是否切换上下文:
call()
切换到被调用的智能合约上下文中。delegatecall()
只使用给定地址的代码,其它属性(存储,余额等)都取自当前合约。delegatecall()
的目的是使用存储在另外一个合约中的库代码。
# 3.4 fallback() 函数
function() public [payable] {
...
}
2
3
- 匿名函数,没有参数也没有返回值。
- 在两种情况下会被调用:
- 直接向一个合约地址转账而不加任何 data;
- 被调用的函数不存在。
- 如果转账金额不为零,同样需要声明
payable
,否则会抛出异常。
# 四、智能合约的创建和运行
- 智能合约的代码写完后,要编译成 bytecode。
- 创建合约:外部帐户发起一个转账交易到 0x0 的地址
- 转账的金额是 0,但是要支付汽油费;
- 合约的代码放在 data 域里。
智能合约运行在 EVM(Ethereum Virtual Machine)上。
以太坊是一个交易驱动的状态机,调用智能合约的交易发布到区块链上后,每个矿工都会执行这个交易,从当前状态确定性地转移到下一个状态。
# 五、汽油费(gas fee)
智能合约是一个图灵完备的编程模型。简单来讲,一切可计算的问题都能计算,这样的虚拟机或者编程语言就叫图灵完备的。当然图灵完备也可能因为陷入死循环而导致程序崩溃。
出现死循环怎么办?
执行合约中的指令要收取汽油费,由发起交易的人来支付,如下图是一个交易的数据结构。
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
}
2
3
4
5
6
7
8
AccountNonce
是交易的序号,用于防止 replay attack。Price
是单位汽油的价格。GasLimit
是发起者在这个交易上愿意支付的最大汽油量。Recipient
是收款人的地址。Amount
是转账金额。Payload
是之前所说的 data 域,用于存放被调用合约的函数及相应参数。
当一个全节点收到一个对智能合约的调用时,先按照这个调用中给出的 GasLimit
和 Price
算出可能花掉的最大汽油费,一次性的把汽油费从这个发起调用的账户上扣除,然后根据实际执行的情况,算出实际花了多少汽油费,如果有剩余会被退回,如果不够会引起回滚。
EVM 中不同指令消耗的汽油费是不一样的。简单的指令很便宜,复杂的或者需要存储状态的指令就很贵。
# 六、错误处理
以太坊中的交易执行具有原子性,一个交易要么全部执行,要么完全不执行,不会只执行一部分。这个交易既包括普通的转账交易,也包括对智能合约的调用。
智能合约中不存在自定义的 try-catch 结构。一旦遇到异常,除特殊情况外,本次执行操作全部回滚,同时花掉的汽油费是不退的,这样可以避免恶意节点浪费计算资源。
一些可以抛出错误的语句:
assert(bool condition)
:用于内部错误。require(bool condition)
:用于输入或外部组件引起的错误。revert()
:终止运行并回滚状态变动。
# 七、嵌套调用
嵌套调用是指一个合约调用另一个合约中的函数。
嵌套调用是否会触发连锁式的回滚?
直接调用 异常会触发连锁回滚,call() 函数调用 异常直接返回 false,不会触发回滚。
需要注意,一个合约直接向一个合约帐户里转账,没有指明调用哪个函数,仍然可能会引起嵌套调用。
# 八、Block Header
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time uint64 `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
/*
TODO (MariusVanDerWijden) Add this field once needed
// Random was added during the merge and contains the BeaconState randomness
Random common.Hash `json:"random" rlp:"optional"`
*/
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GasLimit
是这个区块里所有交易能够消耗汽油的上限(不是每个交易中的GasLimit
求和),是为了避免单个区块资源消耗过大。GasUsed
是这个区块里所有交易消耗的汽油费求和。
每个矿工在发布一个区块的时候,可以对块头的 GasLimit
字段进行微调,每次调整的比例是 。
# 九、Receipt 数据结构
// Receipt represents the results of a transaction.
type Receipt struct {
// Consensus fields: These fields are defined by the Yellow Paper
Type uint8 `json:"type,omitempty"`
PostState []byte `json:"root"`
Status uint64 `json:"status"`
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Logs []*Log `json:"logs" gencodec:"required"`
// Implementation fields: These fields are added by geth when processing a transaction.
// They are stored in the chain database.
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
// Inclusion information: These fields provide information about the inclusion of the
// transaction corresponding to this receipt.
BlockHash common.Hash `json:"blockHash,omitempty"`
BlockNumber *big.Int `json:"blockNumber,omitempty"`
TransactionIndex uint `json:"transactionIndex"`
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Status
会给出这个交易执行的情况是什么样的。
# 十、智能合约可以获得的区块信息
block.blockhash(uint blockNumber) returns (bytes32)
:给定区块的哈希(仅对最近的 256 个区块有效而不包括当前区块)。block.coinbase
(address
):挖出当前区块的矿工地址。block.difficulty
(uint
):当前区块难度。block.gaslimit
(uint
):当前区块 gas 限额。block.number
(uint
):当前区块号。block.timestamp
(uint
):自 unix epoch 起始当前区块以秒计的时间戳。
# 十一、智能合约可以获得的调用信息
msg.data
(bytes
):完整的 calldata,即数据域。msg.gas
(uint
):当前调用剩余的 gas,决定了后续还能进行哪些操作。msg.sender
(address
):消息发送者(当前调用)。msg.sig
(bytes4
):calldata 的前 4 字节(也就是函数标识符)。msg.value
(uint
):随消息发送的 wei 的数量。now
(uint
):目前区块时间戳(block.timestamp
)。tx.gasprice
(uint
):交易的 gas 价格。tx.origin
(address
):交易发起者(完全的调用链)。
要注意区分 msg.sender
和 tx.origin
的含义,如下图所示。
有一个外部账户 A,他调用了一个合约 C1,C1 中有一个函数 f1,f1 又调用了另外一个合约 C2 里面的函数 f2。那么对函数 f2 来说:
msg.sender
是合约 C1,因为当前这个调用是 C1 发起的。tx.origin
是账户 A,因为整个交易最初的发起者是 A。
# 十二、地址类型
<address>.balance
(uint256
)- 以 Wei 为单位的
<address>
的余额。
- 以 Wei 为单位的
<address>.transfer(uint256 amount)
- 向
<address>
发送数量为 amount 的 Wei,失败时抛出异常,发送 2300 gas 的矿工费,不可调节。
- 向
<address>.send(unit256 amount) returns (bool)
- 向
<address>
发送数量为 amount 的 Wei,失败时返回 false,发送 2300 gas 的矿工费,不可调节。
- 向
<address>.call(...) returns (bool)
- 发送底层 CALL,失败时返回 false,发送所有可用 gas,不可调节。
<address>.callcode(...) returns (bool)
- 发送底层 CALLCODE,失败时返回 false,发送所有可用 gas,不可调节。
<address>.delegatecall(...) returns (bool)
- 发送底层 DELEGATECALL,失败时返回 false,发送所有可用 gas,不可调节。
三种发送 ETH 的方式:
<address>.transfer(uint256 amount)
<address>.send(uint256 amount) returns (bool)
<address>.call.value(uint256 amount)()
# 十三、一些问题
1. fallback() 还有其他的触发场景吗?比如 data 中的参数错误。
2. 挖矿奖励怎么确定?块中交易数越多奖励越高?是否影响块头的 GasLimit?如果与交易数无关,每次发布不打包其他交易怎么样?