ETH 智能合约

2022-12-26 Web3 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");
    }
}
1
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;
    }
}
1
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] {
    ...
}
1
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"`
}
1
2
3
4
5
6
7
8
  • AccountNonce 是交易的序号,用于防止 replay attack。
  • Price 是单位汽油的价格。
  • GasLimit 是发起者在这个交易上愿意支付的最大汽油量。
  • Recipient 是收款人的地址。
  • Amount 是转账金额。
  • Payload 是之前所说的 data 域,用于存放被调用合约的函数及相应参数。

当一个全节点收到一个对智能合约的调用时,先按照这个调用中给出的 GasLimitPrice 算出可能花掉的最大汽油费,一次性的把汽油费从这个发起调用的账户上扣除,然后根据实际执行的情况,算出实际花了多少汽油费,如果有剩余会被退回,如果不够会引起回滚。

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"`
	*/
}
1
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 字段进行微调,每次调整的比例是 11024\frac{1}{1024}

# 九、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"`
}
1
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.sendertx.origin 的含义,如下图所示。

有一个外部账户 A,他调用了一个合约 C1,C1 中有一个函数 f1,f1 又调用了另外一个合约 C2 里面的函数 f2。那么对函数 f2 来说:

  • msg.sender 是合约 C1,因为当前这个调用是 C1 发起的。
  • tx.origin 是账户 A,因为整个交易最初的发起者是 A。

# 十二、地址类型

  • <address>.balance (uint256)
    • 以 Wei 为单位的 <address> 的余额。
  • <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?如果与交易数无关,每次发布不打包其他交易怎么样?

# 十四、参考资料

Last Updated: 2023-01-28 4:31:25