0%

在 Solidity 中,钻石模式(Diamond Pattern)是一种设计模式,用于解决智能合约的可扩展性问题。它允许通过代理合约动态地加载和管理多个逻辑合约(Facet),从而克服单个合约的大小限制。

钻石模式的主要组件

  1. 钻石合约(Diamond Contract):主要的入口,负责转发调用到对应的逻辑合约。
  2. 逻辑合约(Facet Contract):实现具体功能的模块化逻辑。
  3. 钻石存储(Diamond Storage):集中管理和存储合约状态。
  4. 钻石切割(Diamond Cut):允许动态添加、替换或移除逻辑合约。

实现代码

1. 钻石存储

使用库管理钻石存储,确保数据一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library DiamondStorageLib {
struct DiamondStorage {
mapping(bytes4 => address) facets; // 映射函数选择器到逻辑合约
address contractOwner; // 合约的所有者
}

bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");

function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
}

2. 钻石切割接口

1
2
3
4
5
6
7
8
9
10
interface IDiamondCut {
enum FacetCutAction {Add, Replace, Remove}
struct FacetCut {
address facetAddress; // 逻辑合约地址
FacetCutAction action; // 操作类型
bytes4[] functionSelectors; // 函数选择器数组
}

function diamondCut(FacetCut[] calldata _facetCuts) external;
}

3. 钻石合约

实现转发逻辑和动态管理。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import "./DiamondStorageLib.sol";
import "./IDiamondCut.sol";

contract Diamond {
using DiamondStorageLib for DiamondStorageLib.DiamondStorage;

constructor() {
DiamondStorageLib.DiamondStorage storage ds = DiamondStorageLib.diamondStorage();
ds.contractOwner = msg.sender; // 设置所有者
}

fallback() external payable {
DiamondStorageLib.DiamondStorage storage ds = DiamondStorageLib.diamondStorage();
address facet = ds.facets[msg.sig]; // 获取函数选择器对应的逻辑合约
require(facet != address(0), "Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}

receive() external payable {}

function diamondCut(IDiamondCut.FacetCut[] calldata _facetCuts) external {
require(msg.sender == DiamondStorageLib.diamondStorage().contractOwner, "Only owner");
DiamondStorageLib.DiamondStorage storage ds = DiamondStorageLib.diamondStorage();

for (uint256 i = 0; i < _facetCuts.length; i++) {
IDiamondCut.FacetCut memory facetCut = _facetCuts[i];
if (facetCut.action == IDiamondCut.FacetCutAction.Add) {
for (uint256 j = 0; j < facetCut.functionSelectors.length; j++) {
ds.facets[facetCut.functionSelectors[j]] = facetCut.facetAddress;
}
} else if (facetCut.action == IDiamondCut.FacetCutAction.Replace) {
for (uint256 j = 0; j < facetCut.functionSelectors.length; j++) {
ds.facets[facetCut.functionSelectors[j]] = facetCut.facetAddress;
}
} else if (facetCut.action == IDiamondCut.FacetCutAction.Remove) {
for (uint256 j = 0; j < facetCut.functionSelectors.length; j++) {
delete ds.facets[facetCut.functionSelectors[j]];
}
} else {
revert("Invalid FacetCutAction");
}
}
}
}

4. 示例逻辑合约(Facet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 一个示例逻辑合约
contract CounterFacet {
uint256 public counter;

function increment() external {
counter += 1;
}

function decrement() external {
counter -= 1;
}

function getCounter() external view returns (uint256) {
return counter;
}
}

5. 部署与使用

  1. 部署 Diamond 合约。
  2. 部署 CounterFacet 等逻辑合约。
  3. 使用 diamondCut 方法将 CounterFacet 的函数选择器(increment 和 decrement)添加到钻石合约。
  4. 通过调用 Diamond 合约的相应选择器,执行 CounterFacet 的逻辑。

总结

  • 优点:钻石模式允许智能合约动态扩展功能,避免超出合约大小限制。
  • 缺点:设计复杂,调用逻辑多了一层代理,可能增加一定的 Gas 开销。

这种模式非常适合需要持续扩展功能的大型项目,例如模块化 DeFi 协议或复杂的 DAO 系统。

使用Go的官方以太坊实现go-ethereum来和以太坊区块链进行交互。Go-ethereum,也被简称为Geth,是最流行的以太坊客户端。因为它是用Go开发的,当使用Golang开发应用程序时,Geth提供了读写区块链的一切功能。

环境

  • go version go1.23.0 darwin/arm64
  • github.com/ethereum/go-ethereum v1.14.12
  • github.com/gin-gonic/gin v1.10.0

代码示例

完整代码请见:https://github.com/zhoubofsy/web3_golang/tree/main/gin

Balance查看

account.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type OpAccount struct {
client *blockchain.Client
}

func (op *OpAccount) GetBalance(accountID string, blkNum *big.Int) (*big.Int, error) {
if accountID == "" {
return nil, ErrAccountIDRequired
}
// 实现获取余额
return op.client.Eth.BalanceAt(context.Background(), common.HexToAddress(accountID), blkNum)
}

func (op *OpAccount) GetPendingBalance(accountID string) (*big.Int, error) {
if accountID == "" {
return nil, ErrAccountIDRequired
}
return op.client.Eth.PendingBalanceAt(context.Background(), common.HexToAddress(accountID))
}

Block查询

block.go

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type OpBlock struct {
client *blockchain.Client
}

func NewOpBlock(client *blockchain.Client) *OpBlock {
return &OpBlock{
client: client,
}
}

func (op *OpBlock) GetBlockNumber() (uint64, error) {
return op.client.Eth.BlockNumber(context.Background())
}

func (op *OpBlock) GetBlockInfo(number uint64) (BlockInfo, error) {
blkInfo, err := op.client.Eth.BlockByNumber(context.Background(), big.NewInt(int64(number)))
if err != nil {
return BlockInfo{}, err
}
return BlockInfo{
Hash: blkInfo.Hash().Hex(),
Height: blkInfo.Number().Uint64(),
Timestamp: blkInfo.Time(),
Difficulty: blkInfo.Difficulty().Uint64(),
Nonce: blkInfo.Nonce(),
Miner: blkInfo.Coinbase().Hex(),
TransCount: uint64(len(blkInfo.Transactions())),
}, err
}

func (op *OpBlock) ListBlocks(from, to uint64) ([]BlockInfo, error) {
blkMaxNum, err := op.GetBlockNumber()
if err != nil {
return nil, err
}
start := max(from, 0)
end := min(to, blkMaxNum)

blocks := make([]BlockInfo, 0)
for i := start; i <= end; i++ {
bi, err := op.GetBlockInfo(i)
if err != nil {
continue
}
blocks = append(blocks, bi)
}
return blocks, nil
}

交易查询

trans.go

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
type OpTrans struct {
client *blockchain.Client
}

func NewOpTrans(bcClient *blockchain.Client) *OpTrans {
return &OpTrans{client: bcClient}
}

func (op *OpTrans) Transfer(to string, value uint64) (string, error) {
// 1. 使用私钥生成 ECDSA 密钥对
privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
if err != nil {
return "", err
}
// 2. 从私钥中获取公钥
pubKey, ok := privateKey.Public().(*ecdsa.PublicKey)
if !ok {
return "", errors.New("failed to get public key")
}
// 3. 从公钥中获取From地址
fromAddress := crypto.PubkeyToAddress(*pubKey)
toAddress := common.HexToAddress(to)

// 4. 获取当前账户(From地址)的nonce值
nonce, err := op.client.Eth.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
return "", err
}
// 5. 设置转账金额
val := big.NewInt(int64(value * 1000000000000000000)) // in wei (1 eth)
// 6. 设置gasLimit
//gasLimit := uint64(21000) // in units
gasLimit, err := op.client.Eth.EstimateGas(context.Background(), ethereum.CallMsg{
From: fromAddress,
To: &toAddress,
Value: val,
Data: nil,
})
if err != nil {
return "", err
}
// 7. 获取当前推荐的gasPrice
gasPrice, err := op.client.Eth.SuggestGasPrice(context.Background())
if err != nil {
return "", err
}
// 8. 获取当前网络的chainID
chainId, err := op.client.Eth.NetworkID(context.Background())
if err != nil {
return "", err
}
// 9. 创建交易
tx := types.NewTransaction(nonce, toAddress, val, gasLimit, gasPrice, nil)
// 10. 签名交易
signTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(chainId.Int64())), privateKey)
if err != nil {
return "", err
}
// 11. 发送交易
err = op.client.Eth.SendTransaction(context.Background(), signTx)
if err != nil {
return "", err
}
return signTx.Hash().Hex(), err
}

func (op *OpTrans) GetHeaderTransactionCount() (uint, error) {
headerBlockNum, err := op.client.Eth.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
blockInfo, err := op.client.Eth.BlockByNumber(context.Background(), big.NewInt(int64(headerBlockNum.Number.Int64())))
if err != nil {
log.Fatal(err)
}
return op.client.Eth.TransactionCount(context.Background(), blockInfo.Hash())
}

func (op *OpTrans) ListTX(blkHash string) ([]TXInfo, error) {
block, err := op.client.Eth.BlockByHash(context.Background(), common.HexToHash(blkHash))
if err != nil {
log.Fatal(err)
return nil, err
}
var txInfos []TXInfo

for _, tx := range block.Transactions() {
txHash := tx.Hash()
receipt, err := op.client.Eth.TransactionReceipt(context.Background(), txHash)
if err != nil {
continue
}
chainId := tx.ChainId()
from, err := types.Sender(types.NewEIP155Signer(chainId), tx)
if err != nil {
continue
}
txInfo := TXInfo{
TxHash: txHash.Hex(),
TxValue: tx.Value().Uint64(),
TxGas: tx.Gas(),
TxGasPrice: tx.GasPrice().Uint64(),
TxNonce: tx.Nonce(),
TxData: tx.Data(),
TxTo: tx.To().Hex(),
TxReceipt: uint8(receipt.Status),
TxFrom: from.Hex(),
}
txInfos = append(txInfos, txInfo)
}
return txInfos, nil
}

合约的部署

首先编写一个Solidity合约。

mytoken.sol

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract MyToken {
string public constant name = "Bob's Token";
string public constant symbol = "BBK";
uint8 public constant decimals = 18;
uint16 private constant increase = 1000;
uint256 public constant totalLimit = 27000000 * (10 ** decimals);
uint256 public totalSupply = 0;
address private owner;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);

modifier checkAddress(address _addr) {
require(address(_addr) != address(0), "Invalid address.");
_;
}

modifier checkBalanceOf(address _addr, uint256 _value) {
require(balanceOf[_addr] >= _value, "Insufficient balance");
_;
}

modifier checkOwner() {
require(msg.sender == owner, "Not owner.");
_;
}

constructor() {
owner = msg.sender;
}

function mint(address _to) public checkOwner returns (uint256) {
uint256 mintValue = increase * (10 ** decimals);
require(totalLimit >= (totalSupply + mintValue), "Out of limit.");
totalSupply += mintValue;
balanceOf[_to] += mintValue;
return balanceOf[_to];
}

function transfer(address _to, uint256 _value) public checkAddress(_to) checkBalanceOf(msg.sender, _value) returns (bool success){
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}

function transferFrom(address _from, address _to, uint256 _value) public checkAddress(_to) checkBalanceOf(_from, _value) returns (bool success) {
require(allowance[_from][msg.sender] >= _value, "No enough approve value.");
allowance[_from][msg.sender] -= _value;
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
emit Transfer(_from, _to, _value);
return true;
}

function approve(address _spender, uint256 _value) public returns (bool success){
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
}

然后使用工具abigen将这个合约编译并导出成mytoken.go的源代码文件。

1
$ abigen --bin ./MyToken.bin --abi MyToken.abi --out ./mytoken.go --pkg mytoken

最后在部署的时候调用引入这个包并调用其中的部署方法完成部署。

contract.go

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
type contract struct {
client *blockchain.Client
}

func NewContract(client *blockchain.Client) *contract {
return &contract{client: client}
}

func (c *contract) DeployContract() (string, string, error) {
privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
if err != nil {
return "", "", err
}
chainId, err := c.client.Eth.ChainID(context.Background())
if err != nil {
return "", "", err
}
txOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainId)
if err != nil {
return "", "", err
}
bk := backend.NewMyTokenCB(c.client.Eth)
// TODO: 部署合约
contractAddress, txHash, _, err := mytoken.DeployMytoken(txOpts, *bk)
return contractAddress.Hex(), txHash.Hash().Hex(), err
}

关于abigen工具的编译安装

克隆go-ethereum代码

1
$ git clone git@github.com:ethereum/go-ethereum.git ethereum/go-ethereum

编译go-ethereum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ make
go run build/ci.go install ./cmd/geth
>>> /usr/local/go/bin/go build -ldflags "--buildid=none -X github.com/ethereum/go-ethereum/internal/version.gitCommit=f861535f1ecc59ad279c35f77f3962efc14dcf98 -X github.com/ethereum/go-ethereum/internal/version.gitDate=20241219 -s" -tags urfave_cli_no_docs,ckzg -trimpath -v -o /Users/zhoub/Labs/ethereum/go-ethereum/build/bin/geth ./cmd/geth
internal/goarch
internal/profilerecord
internal/unsafeheader
internal/race
internal/goexperiment
internal/coverage/rtcov
internal/byteorder
...
github.com/cockroachdb/pebble
github.com/ethereum/go-ethereum/ethdb/pebble
github.com/ethereum/go-ethereum/node
github.com/ethereum/go-ethereum/ethstats
github.com/ethereum/go-ethereum/graphql
github.com/ethereum/go-ethereum/eth
github.com/ethereum/go-ethereum/eth/catalyst
github.com/ethereum/go-ethereum/cmd/utils
github.com/ethereum/go-ethereum/cmd/geth
Done building.
Run "./build/bin/geth" to launch geth.
1
2
3
4
5
6
7
8
9
10
11
12
$ make devtool
env GOBIN= go install golang.org/x/tools/cmd/stringer@latest
go: downloading golang.org/x/tools v0.28.0
env GOBIN= go install github.com/fjl/gencodec@latest
go: downloading github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e
go: downloading github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61
go: downloading golang.org/x/tools v0.0.0-20191126055441-b0650ceb63d9
env GOBIN= go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go: downloading google.golang.org/protobuf v1.36.1
env GOBIN= go install ./cmd/abigen
solc is /opt/homebrew/bin/solc
protoc is /opt/homebrew/bin/protoc

合约的调用

contract.go

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
func (c *contract) Call(addr string, category string, params interface{}) (interface{}, error) {
var resp interface{}
var err error

instance, err := mytoken.NewMytoken(common.HexToAddress(addr), c.client.Eth)
if err != nil {
return nil, err
}
switch category {
case "BalanceOf":
callOpts := &bind.CallOpts{
Pending: false,
Context: context.Background(),
}
if account, ok := params.(string); ok {
bBlance, err := instance.BalanceOf(callOpts, common.HexToAddress(account))
if err != nil {
return nil, err
}
resp = bBlance.String()
} else {
err = errors.New("invalid params")
}
case "Transfer":
// 使用私钥生成 ECDSA 密钥对
privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
if err != nil {
return nil, errors.New("failed to get private key")
}
// 获取当前账户的地址
pubAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
// 获取当前账户的nonce值
nonce, err := c.client.Eth.PendingNonceAt(context.Background(), pubAddr)
if err != nil {
return nil, errors.New("failed to get nonce")
}
// 获取当前推荐的gasPrice
gasPrice, err := c.client.Eth.SuggestGasPrice(context.Background())
if err != nil {
return nil, errors.New("failed to get gas price")
}
gasLimit := uint64(30000000)
txOpts := &bind.TransactOpts{
From: pubAddr,
Nonce: big.NewInt(int64(nonce)),
Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
chainId, err := c.client.Eth.ChainID(context.Background())
if err != nil {
return nil, errors.New("failed to get chain id")
}
return types.SignTx(tx, types.NewEIP155Signer(chainId), privateKey)

},
Value: nil,
GasPrice: gasPrice,
GasLimit: gasLimit,
Context: context.Background(),
}
if txParams, ok := params.(TransactParams); ok {
resp, err = instance.TransferFrom(txOpts, common.HexToAddress(txParams.TxFrom),
common.HexToAddress(txParams.TxTo), big.NewInt(int64(txParams.TxValue)))
} else {
return nil, errors.New("invalid params")
}

default:
return nil, errors.New("unsupported category")
}
return resp, err
}

合约的事件

智能合约具有在执行期间“发出”事件的能力。 事件在以太坊中也称为“日志”。 事件的输出存储在日志部分下的事务处理中。 事件已经在以太坊智能合约中被广泛使用,以便在发生相对重要的动作时记录,特别是在代币合约(即ERC-20)中,以指示代币转账已经发生。

event.go

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
func (e *ContractEvent) Run(contractAddr string) {
query := ethereum.FilterQuery{
Addresses: []common.Address{common.HexToAddress(contractAddr)},
}

ch := make(chan types.Log)
sp, err := e.Client.SubscribeFilterLogs(context.Background(), query, ch)
if err != nil {
fmt.Printf("SubscribeFilterLogs err: %v\n", err)
return
}
for {
select {
case err := <-sp.Err():
fmt.Printf("sp err: %v\n", err)
return
case vLog := <-ch:
logJSON, err := json.Marshal(vLog)
if err != nil {
fmt.Printf("json.Marshal err: %v\n", err)
return
}
fmt.Printf("vLog: %s\n", logJSON)
}
}
}

event.go

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func (e *ContractEvent) ListWithBlkId(contractAddr string, fromBlock, toBlock uint64) ([]LogInfo, error) {
if fromBlock > toBlock {
return nil, fmt.Errorf("fromBlock > toBlock")
}
var fBlock *big.Int
var tBlock *big.Int
if fromBlock > 0 {
fBlock = big.NewInt(int64(fromBlock))
}
if toBlock > 0 {
tBlock = big.NewInt(int64(toBlock))
}

query := ethereum.FilterQuery{
Addresses: []common.Address{common.HexToAddress(contractAddr)},
FromBlock: fBlock,
ToBlock: tBlock,
}

logs, err := e.Client.FilterLogs(context.Background(), query)
if err != nil {
return nil, err
}

contractABI, err := abi.JSON(strings.NewReader(mytoken.MytokenABI))
if err != nil {
return nil, err
}
var logsInfo []LogInfo
var logType string
for _, vLog := range logs {
if topic, ok := e.TopicMap[vLog.Topics[0].Hex()]; !ok {
fmt.Printf("topic not found: %s\n", vLog.Topics[0].Hex())
continue
} else {
logType = topic
}
parseData, err := contractABI.Unpack(logType, vLog.Data)
if err != nil {
if logJSON, err := json.Marshal(vLog); err == nil {
fmt.Printf("Unpack err: %v\n %s", err, logJSON)
} else {
fmt.Printf("Unpack err: %v\n %v", err, vLog)
}
continue
}
var strData string
switch logType {
case TransferTopic:
strData = parseData[0].(*big.Int).String()
case ApproveTopic:
strData = parseData[0].(*big.Int).String()
default:
fmt.Printf("Unknow parse data: %v\n", parseData)
strData = "unknow"
}
logsInfo = append(logsInfo, LogInfo{
//Log: vLog,
LogType: logType,
FromAddress: common.HexToAddress(vLog.Topics[1].Hex()).Hex(),
ToAddress: common.HexToAddress(vLog.Topics[2].Hex()).Hex(),
ParseData: strData,
})
}
return logsInfo, nil
}

在以太坊中,存在一个铸造销毁机制的平衡,这样可以有效控制以太坊的供应量,防止通货膨胀或通货紧缩,同时促进ETH的价值保持稳定。这个平衡的关键是:

  • 铸造过程:指通过出块奖励为验证者铸造新的ETH。
  • 销毁过程:通过交易中 baseFee 的销毁来减少ETH的总供应量。

铸造过程:新区块奖励

在以太坊的PoS(权益证明)模式下,每当一个新区块被生产时,系统会给予验证者一定数量的ETH作为奖励。这些奖励是通过以太坊网络的共识机制铸造出来的,是凭空创造的。

奖励计算公式

以太坊2.0的验证者奖励包括了基础奖励(由区块生产产生)和根据**baseFee** 的销毁以及网络活动(例如gas价格)带来的额外奖励。

基础奖励(Base Reward)

每个验证者在每个 Epoch 中的基础奖励基于质押总量和总验证者数量计算。

公式:

$$\text{Base Reward} = \frac{\text{Effective Balance} \times R}{\text{Total Balance}}$$

  • Effective Balance: 验证者的有效质押余额(最多 32 ETH,过多部分不计入)。
  • R: 奖励因子,与网络的总质押量(Total Balance)相关。
  • Total Balance: 网络中所有验证者的质押总量。

R 的计算

$$R = \frac{\text{Base Reward Per Increment}}{\sqrt{\text{Total Active Balance}}}$$

  • Base Reward Per Increment: 固定参数,当前为 64(具体值可能随协议更新变化)。
  • Total Active Balance: 所有验证者的活跃质押余额总和。

简化理解:总质押越高,每个验证者的奖励越低;总质押越低,每个验证者的奖励越高。

出块奖励(Proposer Reward)

出块的验证者会获得额外的奖励,通常来自网络中的交易小费(Tip)。
公式:

$$\text{Proposer Reward} = \frac{1}{8} \times \text{Total Attestation Rewards}$$

  • Total Attestation Rewards: 当前区块中其他验证者的总奖励。

小费(Tips)

验证者还可以通过出块收取交易小费,作为额外的激励。
公式:

$$\text{Tips} = \text{maxPriorityFeePerGas} \times \text{GasUsed}$$

  • 小费是交易者直接支付给验证者的费用,主要用于提升交易优先级。

奖励的分配

出块奖励:主要分配给验证者。具体的奖励数量和验证者质押的ETH数量有关。

验证者奖励(年化):网络中的所有验证者按质押ETH的比例来分享总的奖励池,基于年化利率进行分配。

  1. 基础奖励分配
    验证者参与网络共识(如投票或提议新区块)时,按比例分配基础奖励。未按要求参与时将减少奖励。

  2. 附加奖励分配
    验证者根据其行为(如投票是否及时、正确)获得额外奖励。
    行为奖励类型:

    • 同步贡献奖励:如果验证者正确参与区块提议和投票。
    • 罚没机制:恶意行为(如双签名)导致奖励减少或直接罚没部分质押。
  3. 动态调整年化收益率

    • 质押总量(网络参与率)影响验证者奖励:
      $$\text{Yearly Yield} \propto \frac{1}{\sqrt{\text{Total Stake}}}$$
    • 如果质押率较低,验证者的年化收益率上升,激励更多人质押。

触发铸造的条件

  • 每生产一个新区块,系统会根据当前的奖励机制铸造出新的ETH,并将其奖励给验证者。
  • 该过程的触发条件就是区块的产生与验证,且与链上交易的数量、复杂度等因素无关。

举例计算

假设:

  • 网络总质押量:10,000,000 ETH
  • 验证者质押:32 ETH
  • 基础奖励因子:64
  1. 基础奖励
    $$
    \text{Base Reward} = \frac{32 \times 64}{\sqrt{10,000,000}} \approx 0.64 , \text{ETH / Epoch}
    $$
    每 6.4 分钟获得约 0.64 ETH。

  2. 年化收益率
    $$
    \text{Yearly Yield} = 0.64 \times 365 \times 24 \times 60 / 6.4 \approx 8.76 % , \text{年化收益率}
    $$


销毁过程:baseFee 的销毁

为了防止ETH的通货膨胀,EIP-1559 引入了销毁机制,旨在通过销毁baseFee 来减少ETH的总供应量,从而对抗铸造过程带来的通货膨胀。

baseFee 销毁机制

baseFee是由网络中的区块大小决定的基础费用,动态调整,以保持网络的区块容量稳定。这个费用的一部分会被销毁,而不是奖励给验证者。 在以太坊中,baseFee 是交易费用的一部分,定义了在一个区块中执行交易所需支付的最低费用。在 EIP-1559 提案中,baseFee 引入了一个动态调整机制,该机制根据网络的负载和拥堵情况自动调整 baseFee 的值。这个调节机制的设计目标是优化交易费用的透明性和可预测性,并避免交易费用波动过大。

1. baseFee 机制的基本概念

baseFee 是每笔交易的最低费用,并且是由网络自动调节的,基于以下几个因素:

  • 网络当前的 负载(即当前区块的使用情况)。
  • 网络 目标的区块大小(即区块目标的 gas 限额)。

EIP-1559 改变了传统的以太坊费用模型,使得交易费用变得更加可预测,并且将一部分费用 销毁,而不是支付给矿工。baseFee 作为这部分费用的核心部分,其动态调整机制起到了关键作用。

2. baseFee 的动态调节原理

baseFee 是根据上一块区块的拥堵情况进行调整的。调整的规则如下:

  • 目标区块大小:以太坊的目标区块大小是 15,000,000 gas(以太坊网络上的一个理想值)。区块的实际大小会决定 baseFee 的调整。
  • 如果区块的 gas 使用量高于目标(即区块接近 15,000,000 gas)baseFee增加,以此来降低交易的需求,并避免网络拥堵。
  • 如果区块的 gas 使用量低于目标(即区块未满)baseFee减少,以鼓励更多交易的提交,提升区块的利用率。

具体的调整规则是基于区块内的 gas 使用量(gasUsed)与目标值(gasTarget)之间的差异来计算:

  • 每个区块的 baseFee 会根据以下公式调整:

$$
\text{new baseFee} = \text{old baseFee} + \text{adjustment}
$$

其中,adjustment 是根据以下条件计算的:

  • 如果区块的 gas 使用量高于目标baseFee 会增加。增加的幅度为 baseFee1/8
  • 如果区块的 gas 使用量低于目标baseFee 会减少。减少的幅度同样是 baseFee1/8

即:

$$
\text{adjustment} = \text{baseFee} \times \frac{1}{8} \times \left( \frac{\text{gasUsed}}{\text{gasTarget}} - 1 \right)
$$

这个公式确保了 baseFee 逐渐适应区块的实际使用情况,避免了过大的波动。

  • 调整的上限和下限
    • baseFee 的调整是逐步的,每个区块的 baseFee 相对于上一块最多只能增加或减少 1/8
    • 这意味着即使网络负载突然增加或减少,baseFee 也不会立即发生剧烈变化,避免了费用过高或过低的极端情况。

3. baseFeemaxFeePerGasgasLimitmaxPriorityFeePerGas 的关系

在以太坊的EIP-1559 交易费用模型中,**maxFeePerGas**, maxPriorityFeePerGas, baseFee, 和 gas limit 是关键参数,它们共同决定了用户支付的总交易费用以及矿工/验证者的收入。

定义与含义

  • **maxFeePerGas**:

    • 用户愿意为交易支付的每单位gas的最大费用(总上限)。
    • 这是用户设定的费用上限,用于限制支付的最高费用。
    • 单位:Gwei。
  • **maxPriorityFeePerGas**:

    • 用户希望额外支付给矿工/验证者的每单位gas的小费(奖励)。
    • 这个值是用户主动提供的,通常用于鼓励矿工/验证者优先处理自己的交易。
    • 单位:Gwei。
    • 如果设置为0,则交易没有小费。
  • **baseFee**:

    • 每单位gas的基础费用,由网络动态调整,决定交易执行所需的最小费用。
    • 由协议规定,所有交易的baseFee部分都会被销毁,而不是分配给矿工/验证者。
    • 单位:Gwei。
    • 动态调整规则
      • 如果区块的实际gas使用量超过目标(区块gas limit的一半),则baseFee会增加。
      • 如果使用量低于目标,则baseFee会减少。
  • **gas limit**:

    • 交易中可以消耗的最大gas数量,用户设定的上限。
    • 确保用户不会为超出需求的计算支付费用。
    • 区块级的gas limit决定了整个区块内最多可消耗的gas数量。

计算逻辑与关系

用户实际支付的费用(effective gas fee)
用户为每单位gas实际支付的费用可以表示为:
$$
\text{Effective Gas Fee} = \text{baseFee} + \min(\text{maxPriorityFeePerGas}, (\text{maxFeePerGas} - \text{baseFee}))
$$

  • 解释
    • **baseFee**:这是网络最低要求的费用,每笔交易都必须支付。
    • 小费部分:用户支付的实际小费是maxPriorityFeePerGasmaxFeePerGas - baseFee的较小值。
    • 如果maxFeePerGas不足以覆盖baseFee,交易将被拒绝。

总交易费用(total transaction fee)
用户支付的总费用计算为:
$$
\text{Total Transaction Fee} = \text{Effective Gas Fee} \times \text{Gas Used}
$$

  • **Gas Used**:是交易实际消耗的gas量,通常小于或等于用户设置的gas limit

  • baseFeemaxFeePerGas 的关系

    • 如果用户设定的maxFeePerGas小于当前的baseFee,交易无法提交。
    • 用户支付的实际费用不会超过maxFeePerGas
  • maxPriorityFeePerGasmaxFeePerGas 的关系

    • maxPriorityFeePerGas 决定用户愿意支付的小费,实际的小费不能超过maxFeePerGas - baseFee
    • 如果baseFee接近maxFeePerGas,小费部分会自动减少。
  • gas limit 的作用

    • 用户设定的gas limit决定了交易的最大gas消耗。
    • 用户支付的总费用与实际消耗的gas数量相关,未使用的gas会退还。

举例说明

假设参数如下:

  • maxFeePerGas = 50 Gwei
  • maxPriorityFeePerGas = 10 Gwei
  • baseFee = 30 Gwei
  • gas limit = 21,000

计算步骤:

  1. 有效单价(Effective Gas Fee)
    $$
    \text{Effective Gas Fee} = \text{baseFee} + \min(\text{maxPriorityFeePerGas}, (\text{maxFeePerGas} - \text{baseFee}))
    $$
    $$
    \text{Effective Gas Fee} = 30 + \min(10, (50 - 30)) = 30 + 10 = 40 , \text{Gwei}
    $$

  2. 总费用(Total Transaction Fee)
    $$
    \text{Total Transaction Fee} = \text{Effective Gas Fee} \times \text{Gas Used}
    $$
    如果实际使用的gas为21,000:
    $$
    \text{Total Transaction Fee} = 40 \times 21,000 = 840,000 , \text{Gwei} = 0.00084 , \text{ETH}
    $$

  3. 销毁的ETH

    • 销毁部分仅包含baseFee

      $$\text{Burned Fee} = \text{baseFee} \times \text{Gas Used}$$
      $$\text{Burned Fee} = 30 \times 21,000 = 630,000 , \text{Gwei} = 0.00063 , \text{ETH}$$

  4. 验证者收益(小费部分)

    • 小费是maxPriorityFeePerGas
      $$\text{Tip} = 10 \times 21,000 = 210,000 , \text{Gwei} = 0.00021 , \text{ETH}$$

4. 销毁的机制与 baseFee

在 EIP-1559 机制中,baseFee 不会直接支付给矿工,而是被销毁(burned),从而减少 ETH 的总供应量。

  • 销毁的 ETH 数量是根据每个区块的 baseFee 和区块的 gas 使用量(gasUsed)来决定的。
  • 销毁的 ETH 量等于每个交易的 baseFee 与该交易消耗的 gas 量的乘积:

$$
\text{burned ETH} = \text{baseFee} \times \text{gasUsed}
$$

随着网络交易量的增加,销毁的 ETH 数量也会增多,从而可能导致 ETH 的供应量减少,产生通货紧缩的效果。

销毁过程:每当发生交易时,系统会根据交易中的baseFee来销毁一定数量的ETH。这意味着**baseFee** 部分的ETH会永久消失,从而减少市场上的ETH总供应量。

baseFee 的销毁机制会被触发的情况

  • 当用户发起交易时,交易费用中包含的 baseFee 会被销毁,而不是奖励给矿工或验证者。
  • baseFee 会随着网络的拥堵情况动态调整,当区块满时,baseFee 增加;反之,当区块空闲时,baseFee 会减少。
  • 销毁的baseFee 每个区块的具体数量是通过算法自动计算的,并且每个区块的baseFee会根据网络的拥堵状况自动变化。

触发销毁的过程

  • 触发销毁的主要过程是用户发起交易并提交到网络中。这时,系统会根据交易的gasPrice(包括baseFeemaxPriorityFeePerGas)来计算销毁的ETH数量。
  • 除了交易之外,某些智能合约的操作也会触发baseFee的销毁。例如,执行合约调用时,合约内部的交易(如ERC-20代币转账)也会根据相应的baseFee销毁ETH。

良性循环:铸造和销毁的平衡

铸造与销毁形成平衡

  • 铸造过程:每个新区块奖励一定数量的ETH给验证者,增加市场的ETH供应量。
  • 销毁过程:通过销毁baseFee,减少市场的ETH供应量,避免过度膨胀。

如何保持ETH供应量稳定?

  • 在一个健康的以太坊网络中,铸造和销毁机制会共同作用,实现供需平衡,防止过度的通货膨胀。
  • 销毁机制对供应量的影响:通过销毁baseFee,ETH的供应量会得到控制。具体销毁多少ETH,取决于网络的交易量和拥堵状况。
  • 网络需求与奖励:交易量越高,baseFee 就越高,销毁的ETH就越多,这可以有效减缓通货膨胀的速度。
  • 验证者奖励:验证者通过出块得到的奖励是新增的ETH,在网络正常运行时,这部分奖励与销毁的ETH保持平衡,避免ETH的数量过快增加。

合适的销毁与铸造机制

  • 区块奖励与销毁的关系:销毁baseFee的ETH,有时可能会比新增奖励的ETH更多,这就可能导致长期内ETH的总供应量减少,进而形成某种程度的通货紧缩
  • 在交易需求高、baseFee 较高的情况下,销毁的ETH数量可能大于验证者奖励的ETH,从而实现一个负增长或稳定的供应量。
  • 在交易需求低、baseFee 较低的情况下,新增的ETH数量可能高于销毁量,导致ETH的供应量逐渐增加。

背景

Sample合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Sample {
address public owner;
constructor() {
owner = msg.sender;
}

function getOwner() external view returns (address) {
return owner;
}

function setOwner(address _owner) public returns (bool) {
owner = _owner;
return true;
}
}

使用命令 solc --ir -o ./ sample.sol 将sol文件编译成 Sample.yul 文件,部分内容如下:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/// @use-src 0:"sample.sol"
object "Sample_35" {
code {
/// @src 0:58:358 "contract Sample {..."
mstore(64, memoryguard(128))
if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }

constructor_Sample_35()

let _1 := allocate_unbounded()
codecopy(_1, dataoffset("Sample_35_deployed"), datasize("Sample_35_deployed"))

return(_1, datasize("Sample_35_deployed"))

function allocate_unbounded() -> memPtr {
memPtr := mload(64)
}
function update_storage_value_offset_0_t_address_to_t_address(slot, value_0) {
let convertedValue_0 := convert_t_address_to_t_address(value_0)
sstore(slot, update_byte_slice_20_shift_0(sload(slot), prepare_store_t_address(convertedValue_0)))
}

/// @ast-id 12
/// @src 0:106:155 "constructor() {..."
function constructor_Sample_35() {

/// @src 0:106:155 "constructor() {..."

/// @src 0:138:148 "msg.sender"
let expr_8 := caller()
/// @src 0:130:148 "owner = msg.sender"
update_storage_value_offset_0_t_address_to_t_address(0x00, expr_8)
let expr_9 := expr_8

}
/// @src 0:58:358 "contract Sample {..."

}
/// @use-src 0:"sample.sol"
object "Sample_35_deployed" {
code {
/// @src 0:58:358 "contract Sample {..."
mstore(64, memoryguard(128))
if iszero(lt(calldatasize(), 4))
{
let selector := shift_right_224_unsigned(calldataload(0))
switch selector
case 0x13af4035
{
// setOwner(address)
external_fun_setOwner_34()
}
case 0x893d20e8
{
// getOwner()
external_fun_getOwner_20()
}
case 0x8da5cb5b
{
// owner()
external_fun_owner_3()
}
default {}
}
revert_error_42b3090547df1d2001c96683413b8cf91c1b902ef5e3cb8d9f6f304cf7446f74()

function allocate_unbounded() -> memPtr {
memPtr := mload(64)
}

function external_fun_setOwner_34() {

if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }
let param_0 := abi_decode_tuple_t_address(4, calldatasize())
let ret_0 := fun_setOwner_34(param_0)
let memPos := allocate_unbounded()
let memEnd := abi_encode_tuple_t_bool__to_t_bool__fromStack(memPos , ret_0)
return(memPos, sub(memEnd, memPos))

}

function abi_encode_t_address_to_t_address_fromStack(value, pos) {
mstore(pos, cleanup_t_address(value))
}

function abi_encode_tuple_t_address__to_t_address__fromStack(headStart , value0) -> tail {
tail := add(headStart, 32)

abi_encode_t_address_to_t_address_fromStack(value0, add(headStart, 0))

}

function external_fun_getOwner_20() {
if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }
abi_decode_tuple_(4, calldatasize())
let ret_0 := fun_getOwner_20()
let memPos := allocate_unbounded()
let memEnd := abi_encode_tuple_t_address__to_t_address__fromStack(memPos , ret_0)
return(memPos, sub(memEnd, memPos))

}

function read_from_storage_split_dynamic_t_address(slot, offset) -> value {
value := extract_from_storage_value_dynamict_address(sload(slot), offset)

}

/// @ast-id 3
/// @src 0:80:100 "address public owner"
function getter_fun_owner_3() -> ret {

let slot := 0
let offset := 0

ret := read_from_storage_split_dynamic_t_address(slot, offset)

}
/// @src 0:58:358 "contract Sample {..."

function external_fun_owner_3() {

if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }
abi_decode_tuple_(4, calldatasize())
let ret_0 := getter_fun_owner_3()
let memPos := allocate_unbounded()
let memEnd := abi_encode_tuple_t_address__to_t_address__fromStack(memPos , ret_0)
return(memPos, sub(memEnd, memPos))

}

function update_byte_slice_20_shift_0(value, toInsert) -> result {
let mask := 0xffffffffffffffffffffffffffffffffffffffff
toInsert := shift_left_0(toInsert)
value := and(value, not(mask))
result := or(value, and(toInsert, mask))
}

function update_storage_value_offset_0_t_address_to_t_address(slot, value_0) {
let convertedValue_0 := convert_t_address_to_t_address(value_0)
sstore(slot, update_byte_slice_20_shift_0(sload(slot), prepare_store_t_address(convertedValue_0)))
}

/// @ast-id 34
/// @src 0:248:356 "function setOwner(address _owner) public returns (bool) {..."
function fun_setOwner_34(var__owner_22) -> var__25 {
/// @src 0:298:302 "bool"
let zero_t_bool_1 := zero_value_for_split_t_bool()
var__25 := zero_t_bool_1

/// @src 0:322:328 "_owner"
let _2 := var__owner_22
let expr_28 := _2
/// @src 0:314:328 "owner = _owner"
update_storage_value_offset_0_t_address_to_t_address(0x00, expr_28)
let expr_29 := expr_28
/// @src 0:345:349 "true"
let expr_31 := 0x01
/// @src 0:338:349 "return true"
var__25 := expr_31
leave

}

/// @ast-id 20
/// @src 0:161:242 "function getOwner() external view returns (address) {..."
function fun_getOwner_20() -> var__15 {
/// @src 0:204:211 "address"
let zero_t_address_3 := zero_value_for_split_t_address()
var__15 := zero_t_address_3

/// @src 0:230:235 "owner"
let _4 := read_from_storage_split_offset_0_t_address(0x00)
let expr_17 := _4
/// @src 0:223:235 "return owner"
var__15 := expr_17
leave

}
/// @src 0:58:358 "contract Sample {..."

}

data ".metadata" hex"a2646970667358221220c3c7bfea64093019fdb0e0aaf2f6130bb86c98f28ea301dcc40713fb9f64853964736f6c634300081c0033"
}

}

原理

生成的 Yul 文件是基于以太坊中间表示(Intermediate Representation, IR)的代码,它介于 Solidity 源代码和 EVM 字节码之间。Yul 不是 JavaScript,而是一种低级、高效的语言,用于表示以太坊智能合约逻辑。Yul 的目标是更贴近 EVM 的执行逻辑,同时保留一定的可读性。

顶层结构

Yul 文件包含两个主要对象:

  1. Sample_35: 表示主合约的部署代码。
  2. Sample_35_deployed: 表示部署后的运行时代码。

1. Sample_35(合约部署阶段)

  • code:
    • 执行合约的部署逻辑,将运行时代码加载到链上。
    • 包含一些辅助函数,如内存分配、错误处理等。
1
2
3
object "Sample_35" {
code {
mstore(64, memoryguard(128))
`mstore(64, memoryguard(128))`: 初始化内存位置,指向位置 `0x40`,这是 Solidity 合约中默认的内存分配起点。
  • 检查调用值
1
if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }
执行 `constructor_Sample_35()`,这是对应 Solidity 构造函数 `constructor` 的实现。
  • 加载运行时代码
1
2
3
let _1 := allocate_unbounded()
codecopy(_1, dataoffset("Sample_35_deployed"), datasize("Sample_35_deployed"))
return(_1, datasize("Sample_35_deployed"))
将 `Sample_35_deployed` 对象的代码加载到内存中,并将其返回以存储在链上。

2. Sample_35_deployed(运行时阶段)

  • 代码入口
1
2
3
4
5
6
7
object "Sample_35_deployed" {
code {
/// @src 0:58:358 "contract Sample {..."
mstore(64, memoryguard(128))

if iszero(lt(calldatasize(), 4))
{
- 如果传入的调用数据长度(`calldatasize()`)大于或等于 4,则认为是函数调用。
- 读取调用数据的前 4 个字节(`calldataload(0)`),解析为函数选择器(`selector`)。
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
let selector := shift_right_224_unsigned(calldataload(0))
switch selector

case 0x13af4035
{
// setOwner(address)

external_fun_setOwner_34()
}

case 0x893d20e8
{
// getOwner()

external_fun_getOwner_20()
}

case 0x8da5cb5b
{
// owner()

external_fun_owner_3()
}

default {}

使用 switch 根据函数选择器跳转到对应的外部函数实现:

- `0x13af4035`: 对应 `setOwner(address)`。
- `0x893d20e8`: 对应 `getOwner()`。
- `0x8da5cb5b`: 对应 `owner()`。
- 如果没有匹配的函数选择器,默认执行空操作。

构造函数(constructor_Sample_35

1
2
3
4
let expr_8 := caller()
/// @src 0:130:148 "owner = msg.sender"
update_storage_value_offset_0_t_address_to_t_address(0x00, expr_8)
let expr_9 := expr_8
  • 获取合约部署者地址(caller())并存储到 owner 槽位(存储槽位 0)。
  • update_storage_value_offset_0_t_address_to_t_address 是一个存储更新的通用函数。

外部函数实现

setOwner(address)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function external_fun_setOwner_34() {

if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }

/// 将地址参数 `_owner` 从调用数据中解码。
let param_0 := abi_decode_tuple_t_address(4, calldatasize())

/// 更新存储
let ret_0 := fun_setOwner_34(param_0)
let memPos := allocate_unbounded()
let memEnd := abi_encode_tuple_t_bool__to_t_bool__fromStack(memPos , ret_0)
return(memPos, sub(memEnd, memPos))

}

更新存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function fun_setOwner_34(var__owner_22) -> var__25 {
/// @src 0:298:302 "bool"
let zero_t_bool_1 := zero_value_for_split_t_bool()
var__25 := zero_t_bool_1

/// @src 0:322:328 "_owner"
let _2 := var__owner_22
let expr_28 := _2
/// @src 0:314:328 "owner = _owner"
/// 将 `_owner` 更新到 `owner` 的存储槽位(0)。
update_storage_value_offset_0_t_address_to_t_address(0x00, expr_28)
let expr_29 := expr_28
/// @src 0:345:349 "true"
let expr_31 := 0x01
/// @src 0:338:349 "return true"
var__25 := expr_31
leave

}
- 返回布尔值 `true`,表示操作成功。

getOwner()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        function external_fun_getOwner_20() {

if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() }
abi_decode_tuple_(4, calldatasize())

/// 从存储槽位读取 `owner`
let ret_0 := fun_getOwner_20()

let memPos := allocate_unbounded()
let memEnd := abi_encode_tuple_t_address__to_t_address__fromStack(memPos , ret_0)
/// 将结果编码为 ABI 格式并返回。
return(memPos, sub(memEnd, memPos))

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fun_getOwner_20() -> var__15 {
/// @src 0:204:211 "address"
let zero_t_address_3 := zero_value_for_split_t_address()
var__15 := zero_t_address_3

/// @src 0:230:235 "owner"
/// 读取存储槽位 0 的值,即当前合约的 `owner` 地址。
let _4 := read_from_storage_split_offset_0_t_address(0x00)
let expr_17 := _4
/// @src 0:223:235 "return owner"
var__15 := expr_17
leave

}

辅助函数

  • allocate_unbounded: 分配内存。
  • abi_decode 和 abi_encode: 解码和编码 ABI 数据。
  • cleanup: 用于数据清理,确保值符合地址或布尔格式。
  • shift: 位操作,用于提取或存储特定位数据。

总结

  • 该 Yul 文件表示了 Sample 合约的部署和运行时逻辑,分为部署代码和运行时代码。
  • 核心逻辑如存储更新、函数选择、参数解码/编码都通过低级 Yul 操作实现。
  • Yul 提供了更高效、更贴近 EVM 的执行语义,同时保留了逻辑清晰度。

什么是 ABI (Application Binary Interface)?

在 Solidity 中,ABI(应用二进制接口)是智能合约与外界(包括其他合约和用户界面)交互的桥梁。它定义了智能合约中函数的编码方式、输入参数和返回值的格式,以便在区块链中进行数据的传输和解析。

ABI 的作用

  1. 数据编码与解码:在区块链中,所有的数据都以二进制形式存储和传输。ABI 提供了标准化的编码规则,使得智能合约的函数调用可以正确地解释输入和输出数据。
  2. 合约交互:外部程序(如 DApp)需要 ABI 文件来与合约交互,了解合约的函数和参数。
  3. 兼容性:通过 ABI,智能合约之间可以互操作,即使它们是由不同的开发人员编写的。

ABI 的重要性

  • 统一性:ABI 使得智能合约与外部交互的过程标准化,避免了自定义协议带来的复杂性。
  • 跨语言支持:无论是前端、后端,还是区块链中的其他合约,都可以基于 ABI 与合约进行交互。

ABI的原理

ABI 的结构

ABI 通常是以 JSON 格式生成的文件,描述了智能合约的函数、事件及其参数。

示例合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.8.0;

contract Example {
uint256 public value;

event ValueChanged(uint256 newValue);

function setValue(uint256 _value) public {
value = _value;
emit ValueChanged(_value);
}

function getValue() public view returns (uint256) {
return value;
}
}

ABI 的 JSON 表示

运行 solc --abi Example.sol 会生成以下 ABI 文件:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newValue",
"type": "uint256"
}
],
"name": "ValueChanged",
"type": "event"
},
{
"inputs": [],
"name": "getValue",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
}
],
"name": "setValue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "value",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]

ABI 的关键字段

  1. type
    定义元素的类型:
    • function:表示合约函数。
    • event:表示事件。
    • constructor:表示构造函数。
    • fallback:表示回退函数。
    • receive:表示接收以太币的特殊函数。
  2. name
    函数或事件的名称(不适用于匿名函数)。
  3. inputs
    表示函数或事件的输入参数列表。每个参数包含:
    • internalType:Solidity 内部类型。
    • name:参数名称。
    • type:外部使用的类型(如 uint256)。
  4. outputs
    表示函数的返回值列表,格式与 inputs 相同。
  5. stateMutability
    表示函数的状态:
    • view:只读,不修改状态。
    • pure:不读写状态。
    • nonpayable:不允许发送以太币。
    • payable:允许发送以太币。
  6. anonymous
    仅适用于事件,表示事件是否匿名。

ABI 编码规则

ABI 定义了调用合约函数时参数的编码方式。以下是编码的关键点:

编码示例

调用 setValue(42) 的编码过程如下:

  1. 函数签名的 Keccak 哈希:
    1
    setValue(uint256) -> 0x55241077
  2. 参数的编码:
    1
    42 -> 000000000000000000000000000000000000000000000000000000000000002a
  3. 完整的编码:
    1
    0x55241077000000000000000000000000000000000000000000000000000000000000002a

ABI 解码规则

返回值的解码也遵循 ABI 的规则。
例如,getValue() 返回 42 时:

  1. 返回数据:
    1
    0x000000000000000000000000000000000000000000000000000000000000002a
  2. 解码为 42

ABI 的使用

使用场景 1: 智能合约调用

场景描述:

通过 Golang 使用 ABI 文件调用智能合约上的函数,例如调用只读函数或发送交易调用状态变更函数。

代码示例:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"context"
"fmt"
"log"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
)

func main() {
// 1. 连接到 Ethereum 节点
client, err := ethclient.Dial("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID")
if err != nil {
log.Fatalf("Failed to connect to Ethereum node: %v", err)
}

// 2. 合约地址和 ABI 定义
contractAddress := "0xYourContractAddress"
contractABI := `[{"constant":true,"inputs":[],"name":"getValue","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]`

// 3. 解析 ABI
parsedABI, err := abi.JSON(strings.NewReader(contractABI))
if err != nil {
log.Fatalf("Failed to parse ABI: %v", err)
}

// 4. 构造合约调用
callOpts := &bind.CallOpts{Context: context.Background()}
data, err := parsedABI.Pack("getValue")
if err != nil {
log.Fatalf("Failed to pack data: %v", err)
}

// 5. 调用合约
msg := ethereum.CallMsg{
To: &contractAddress,
Data: data,
}
result, err := client.CallContract(context.Background(), msg, nil)
if err != nil {
log.Fatalf("Failed to call contract: %v", err)
}

// 6. 解析返回值
var value *big.Int
err = parsedABI.UnpackIntoInterface(&value, "getValue", result)
if err != nil {
log.Fatalf("Failed to unpack result: %v", err)
}
fmt.Printf("Value: %s\n", value.String())
}

关键点:

  1. 使用 abi.JSON 解析 ABI。
  2. 使用 Pack 将函数调用和参数编码。
  3. 通过 CallContract 调用合约。
  4. 使用 UnpackIntoInterface 解码返回值。

使用场景 2: 监听合约事件

场景描述:

通过 Golang 和 ABI 监听合约事件的发生。

代码示例:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"context"
"log"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/ethclient"
)

func main() {
// 1. 连接到 Ethereum 节点
client, err := ethclient.Dial("wss://mainnet.infura.io/ws/v3/YOUR_INFURA_PROJECT_ID")
if err != nil {
log.Fatalf("Failed to connect to Ethereum node: %v", err)
}

// 2. 合约地址和事件的 ABI 定义
contractAddress := "0xYourContractAddress"
eventABI := `[{"anonymous":false,"inputs":[{"indexed":false,"name":"newValue","type":"uint256"}],"name":"ValueChanged","type":"event"}]`

// 3. 解析事件 ABI
parsedABI, err := abi.JSON(strings.NewReader(eventABI))
if err != nil {
log.Fatalf("Failed to parse ABI: %v", err)
}

// 4. 设置日志查询过滤器
query := ethereum.FilterQuery{
Addresses: []common.Address{common.HexToAddress(contractAddress)},
}
logs := make(chan types.Log)
sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
log.Fatalf("Failed to subscribe to logs: %v", err)
}

// 5. 监听事件
for logEvent := range logs {
var event struct {
NewValue *big.Int
}
err := parsedABI.UnpackIntoInterface(&event, "ValueChanged", logEvent.Data)
if err != nil {
log.Printf("Failed to unpack event: %v", err)
} else {
log.Printf("New Value: %s", event.NewValue.String())
}
}

_ = sub // Handle subscription lifecycle as needed
}

关键点:

  1. 使用 FilterQuery 设置日志过滤器。
  2. 通过 SubscribeFilterLogs 监听事件。
  3. 使用 ABI 的 UnpackIntoInterface 解码事件数据。

安装Docker

https://docs.docker.com/manuals/

安装Kurtosis

以macos为例,安装Kurtosis

1
2
3
4
5
$ brew install kurtosis-tech/tap/kurtosis-cli
$ kurtosis version
CLI Version: 1.4.3

To see the engine version (provided it is running): kurtosis engine status

部署Ethereum测试网

使用下面命令部署Ethereum测试网络

1
$ kurtosis --enclave local-eth-testnet run github.com/ethpandaops/ethereum-package

以下是部署过程的输出信息:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
INFO[2024-12-11T15:08:23+08:00] Creating a new enclave for Starlark to run inside...
INFO[2024-12-11T15:08:25+08:00] Enclave 'local-eth-testnet' created successfully

Container images used in this run:
> ethpandaops/lighthouse:stable - remotely downloaded
> ethpandaops/ethereum-genesis-generator:3.4.2 - remotely downloaded
> ethereum/client-go:latest - remotely downloaded
> python:3.11-alpine - remotely downloaded
> protolambda/eth2-val-tools:latest - remotely downloaded
> badouralix/curl-jq - remotely downloaded

......

Starlark code successfully run. Output was:
{
"all_participants": [
{
"cl_context": {
"beacon_grpc_url": "",
"beacon_http_url": "http://172.16.4.11:4000",
"beacon_service_name": "cl-1-lighthouse-geth",
"cl_nodes_metrics_info": [
{
"config": {
"labels": null,
"scrape_interval": "15s"
},
"name": "cl-1-lighthouse-geth",
"path": "/metrics",
"url": "172.16.4.11:5054"
}
],
"client_name": "lighthouse",
"enr": "enr:-Mm4QGoJFz_8BxhUo1_qcU99AlMtQg08SE_lrbH0B3cVOdoiFyHNA12IrI2DwAsZG7AvSeocDB9TcFad33Om5sd9Gc0Dh2F0dG5ldHOIAAAAAMAAAACDY3NjBIRldGgykE-3JeFgAAA4AOH1BQAAAACCaWSCdjSCaXCErBAEC4RxdWljgiMpiXNlY3AyNTZrMaEDwNCUN06NbFU4oqKnXHkTMdcdNTlr_xwC33QSFfCdcL6Ic3luY25ldHMAg3RjcIIjKIN1ZHCCIyg",
"http_port": 4000,
"ip_addr": "172.16.4.11",
"multiaddr": "/ip4/172.16.4.11/tcp/9000/p2p/16Uiu2HAmRdf6aDAhx764sqQkD9BYQxFmyAXoCaGETKCwb49MTLgm",
"peer_id": "16Uiu2HAmRdf6aDAhx764sqQkD9BYQxFmyAXoCaGETKCwb49MTLgm",
"snooper_enabled": false,
"snooper_engine_context": null,
"supernode": false,
"validator_keystore_files_artifact_uuid": "1-lighthouse-geth-0-63"
},
"cl_type": "lighthouse",
"el_context": {
"client_name": "geth",
"el_metrics_info": [
{
"config": {
"labels": null,
"scrape_interval": "15s"
},
"name": "el-1-geth-lighthouse",
"path": "/debug/metrics/prometheus",
"url": "172.16.4.10:9001"
}
],
"engine_rpc_port_num": 8551,
"enode": "enode://e273007e4a0cc96fb52d02e132643dcdc7dabee68bdf65e7a538f01e33682ae137c0f25ca51301f99e9ac2ba2ca625af9caa6c692246597872f9d0bac4d6c3ab@172.16.4.10:30303",
"enr": "enr:-Ki4QEtrZL_kOZOzlrm5bzYHqXuAExc82OpFTVlzlWpvfkbAThI2xjDaR44kByHuwzs0Lg4QpasiuBLWFzvq0Tuk7cuGAZO0jpf1g2V0aMzLhKMFvEuFCVgqux6CaWSCdjSCaXCErBAEColzZWNwMjU2azGhA-JzAH5KDMlvtS0C4TJkPc3H2r7mi99l56U48B4zaCrhhHNuYXDAg3RjcIJ2X4N1ZHCCdl8",
"ip_addr": "172.16.4.10",
"rpc_http_url": "http://172.16.4.10:8545",
"rpc_port_num": 8545,
"service_name": "el-1-geth-lighthouse",
"ws_port_num": 8546,
"ws_url": "ws://172.16.4.10:8546"
},
"el_type": "geth",
"ethereum_metrics_exporter_context": null,
"remote_signer_context": null,
"remote_signer_type": "web3signer",
"snooper_beacon_context": null,
"snooper_engine_context": null,
"vc_context": {
"client_name": "lighthouse",
"metrics_info": {
"config": {
"labels": null,
"scrape_interval": "15s"
},
"name": "vc-1-geth-lighthouse",
"path": "/metrics",
"url": "172.16.4.12:8080"
},
"service_name": "vc-1-geth-lighthouse"
},
"vc_type": "lighthouse",
"xatu_sentry_context": null
}
],
"final_genesis_timestamp": "1733901086",
"genesis_validators_root": "0xd61ea484febacfae5298d52a2b581f3e305a51f3112a9241b968dccf019f7b11",
"network_id": "3151908",
"network_params": {
"additional_preloaded_contracts": {},
"altair_fork_epoch": 0,
"bellatrix_fork_epoch": 0,
"capella_fork_epoch": 0,
"churn_limit_quotient": 65536,
"custody_requirement": 4,
"data_column_sidecar_subnet_count": 128,
"deneb_fork_epoch": 0,
"deposit_contract_address": "0x4242424242424242424242424242424242424242",
"devnet_repo": "ethpandaops",
"eip7594_fork_epoch": 100000002,
"eip7594_fork_version": "0x60000038",
"ejection_balance": 16000000000,
"electra_fork_epoch": 100000000,
"eth1_follow_distance": 2048,
"fulu_fork_epoch": 100000001,
"genesis_delay": 20,
"genesis_gaslimit": 30000000,
"max_blobs_per_block": 6,
"max_per_epoch_activation_churn_limit": 8,
"min_validator_withdrawability_delay": 256,
"network": "kurtosis",
"network_id": "3151908",
"network_sync_base_url": "https://snapshots.ethpandaops.io/",
"num_validator_keys_per_node": 64,
"prefunded_accounts": {},
"preregistered_validator_count": 0,
"preregistered_validator_keys_mnemonic": "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete",
"preset": "mainnet",
"samples_per_slot": 8,
"seconds_per_slot": 12,
"shard_committee_period": 256
},
"pre_funded_accounts": [
{
"address": "0x8943545177806ED17B9F23F0a21ee5948eCaa776",
"private_key": "bcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31"
},
{
"address": "0xE25583099BA105D9ec0A67f5Ae86D90e50036425",
"private_key": "39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d"
},
{
"address": "0x614561D2d143621E126e87831AEF287678B442b8",
"private_key": "53321db7c1e331d93a11a41d16f004d7ff63972ec8ec7c25db329728ceeb1710"
},
{
"address": "0xf93Ee4Cf8c6c40b329b0c0626F28333c132CF241",
"private_key": "ab63b23eb7941c1251757e24b3d2350d2bc05c3c388d06f8fe6feafefb1e8c70"
},
{
"address": "0x802dCbE1B1A97554B4F50DB5119E37E8e7336417",
"private_key": "5d2344259f42259f82d2c140aa66102ba89b57b4883ee441a8b312622bd42491"
},
{
"address": "0xAe95d8DA9244C37CaC0a3e16BA966a8e852Bb6D6",
"private_key": "27515f805127bebad2fb9b183508bdacb8c763da16f54e0678b16e8f28ef3fff"
},
{
"address": "0x2c57d1CFC6d5f8E4182a56b4cf75421472eBAEa4",
"private_key": "7ff1a4c1d57e5e784d327c4c7651e952350bc271f156afb3d00d20f5ef924856"
},
{
"address": "0x741bFE4802cE1C4b5b00F9Df2F5f179A1C89171A",
"private_key": "3a91003acaf4c21b3953d94fa4a6db694fa69e5242b2e37be05dd82761058899"
},
{
"address": "0xc3913d4D8bAb4914328651C2EAE817C8b78E1f4c",
"private_key": "bb1d0f125b4fb2bb173c318cdead45468474ca71474e2247776b2b4c0fa2d3f5"
},
{
"address": "0x65D08a056c17Ae13370565B04cF77D2AfA1cB9FA",
"private_key": "850643a0224065ecce3882673c21f56bcf6eef86274cc21cadff15930b59fc8c"
},
{
"address": "0x3e95dFbBaF6B348396E6674C7871546dCC568e56",
"private_key": "94eb3102993b41ec55c241060f47daa0f6372e2e3ad7e91612ae36c364042e44"
},
{
"address": "0x5918b2e647464d4743601a865753e64C8059Dc4F",
"private_key": "daf15504c22a352648a71ef2926334fe040ac1d5005019e09f6c979808024dc7"
},
{
"address": "0x589A698b7b7dA0Bec545177D3963A2741105C7C9",
"private_key": "eaba42282ad33c8ef2524f07277c03a776d98ae19f581990ce75becb7cfa1c23"
},
{
"address": "0x4d1CB4eB7969f8806E2CaAc0cbbB71f88C8ec413",
"private_key": "3fd98b5187bf6526734efaa644ffbb4e3670d66f5d0268ce0323ec09124bff61"
},
{
"address": "0xF5504cE2BcC52614F121aff9b93b2001d92715CA",
"private_key": "5288e2f440c7f0cb61a9be8afdeb4295f786383f96f5e35eb0c94ef103996b64"
},
{
"address": "0xF61E98E7D47aB884C244E39E031978E33162ff4b",
"private_key": "f296c7802555da2a5a662be70e078cbd38b44f96f8615ae529da41122ce8db05"
},
{
"address": "0xf1424826861ffbbD25405F5145B5E50d0F1bFc90",
"private_key": "bf3beef3bd999ba9f2451e06936f0423cd62b815c9233dd3bc90f7e02a1e8673"
},
{
"address": "0xfDCe42116f541fc8f7b0776e2B30832bD5621C85",
"private_key": "6ecadc396415970e91293726c3f5775225440ea0844ae5616135fd10d66b5954"
},
{
"address": "0xD9211042f35968820A3407ac3d80C725f8F75c14",
"private_key": "a492823c3e193d6c595f37a18e3c06650cf4c74558cc818b16130b293716106f"
},
{
"address": "0xD8F3183DEF51A987222D845be228e0Bbb932C222",
"private_key": "c5114526e042343c6d1899cad05e1c00ba588314de9b96929914ee0df18d46b2"
},
{
"address": "0xafF0CA253b97e54440965855cec0A8a2E2399896",
"private_key": "4b9f63ecf84210c5366c66d68fa1f5da1fa4f634fad6dfc86178e4d79ff9e59"
}
]
}

⭐ us on GitHub - https://github.com/kurtosis-tech/kurtosis
INFO[2024-12-11T15:11:10+08:00] ==========================================================
INFO[2024-12-11T15:11:10+08:00] || Created enclave: local-eth-testnet ||
INFO[2024-12-11T15:11:10+08:00] ==========================================================
Name: local-eth-testnet
UUID: b134e2cd4ffa
Status: RUNNING
Creation Time: Wed, 11 Dec 2024 15:08:23 CST
Flags:

========================================= Files Artifacts =========================================
UUID Name
eeac0b67fe11 1-lighthouse-geth-0-63
0ca90fbaddae el_cl_genesis_data
02724177f009 final-genesis-timestamp
6579d6313c44 genesis-el-cl-env-file
6236b016ccf1 genesis_validators_root
d65d356d27a3 jwt_file
77d5e8aa3bf1 keymanager_file
f873ac882893 prysm-password
7f0f603a0154 validator-ranges

========================================== User Services ==========================================
UUID Name Ports Status
38a34f4ff219 cl-1-lighthouse-geth http: 4000/tcp -> http://127.0.0.1:54095 RUNNING
metrics: 5054/tcp -> http://127.0.0.1:54096
tcp-discovery: 9000/tcp -> 127.0.0.1:54097
udp-discovery: 9000/udp -> 127.0.0.1:60155
714c863fd14f el-1-geth-lighthouse engine-rpc: 8551/tcp -> 127.0.0.1:54056 RUNNING
metrics: 9001/tcp -> http://127.0.0.1:54057
rpc: 8545/tcp -> 127.0.0.1:54059
tcp-discovery: 30303/tcp -> 127.0.0.1:54058
udp-discovery: 30303/udp -> 127.0.0.1:54093
ws: 8546/tcp -> 127.0.0.1:54055
ef18a854d243 validator-key-generation-cl-validator-keystore <none> RUNNING
0f4da064ec7f vc-1-geth-lighthouse metrics: 8080/tcp -> http://127.0.0.1:54168 RUNNING
  • cl-1-lighthouse-geth
    以太坊 2.0 的 Beacon Chain 节点(共识层)。负责管理 PoS 共识机制、验证区块、协调验证者任务、维护 Beacon Chain 状态。
  • el-1-geth-lighthouse
    以太坊 1.x 的执行层客户端。它负责处理智能合约、账户状态更新、交易验证等功能。它的主要职责是提供执行层的支持,与以太坊 2.0 的共识层(如 Lighthouse)协作。
  • validator-key-generation-cl-validator-keystore
    以太坊 2.0(即 Ethereum 2.0 或 Eth2)验证者(validator)管理的工具集。这个工具集为以太坊 2.0 的验证者提供了多种支持功能,帮助他们更加高效、安全地参与以太坊的 权益证明(Proof of Stake, PoS) 共识机制。通过这些工具,验证者可以生成必要的密钥、管理其验证者身份、监控验证者的状态以及优化验证过程。
  • vc-1-geth-lighthouse
    验证者客户端(Validator Client)。验证者客户端专门管理验证者账户、执行验证签名职责。

操作ethereum

使用命令kurtosis service shell local-eth-testnet el-1-geth-lighthouse登录el-1-geth-lighthouse的容器

1
2
3
$ kurtosis service shell local-eth-testnet el-1-geth-lighthouse
No bash found on container; dropping down to sh shell...
/ #

然后就可以使用geth这个命令了。

1
2
3
4
5
6
7
8
9
10
/ # geth --datadir /data/geth/execution-data/ attach
Welcome to the Geth JavaScript console!

instance: Geth/v1.14.13-unstable-330190e4-20241210/linux-arm64/go1.23.4
at block: 1293 (Thu Dec 12 2024 02:22:26 GMT+0000 (UTC))
datadir: /data/geth/execution-data
modules: admin:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit
>

接下来就可以输入JS代码

Metamask添加网络

kurtosis_metamask_add_network

根据Kurtosis部署过程中输出的信息添加Metamask网络

EVM 的整个空间被逻辑上划分为不同的区域,分别为栈(Stack)、内存(Memory)、存储(Storage)、代码(Code)和常量区域。

栈区域(Stack)区域

  • 特点:
    • 栈是一个 固定大小 的数据区域,最大为 1024 个字(每个字 32 字节)。
    • 用于存储操作数、临时值,以及函数调用的返回地址。
    • 数据只能通过 push 和 pop 操作访问。
  • 作用: 函数内部的临时变量和操作数首先存储在栈中。

内存 (Memory)区域

  • 功能: 用于临时存储数据,生命周期仅在交易或调用期间有效。
  • 地址范围: 逻辑地址从0开始,可以按需扩展,理论上可以扩展到 2^256 - 1
  • 特点:
    • 按 32 字节(256 位)对齐。
    • 初始化时为空(零值)。
    • 按需分配时会增加 gas 消耗。
  • 作用: 用于存储函数的局部变量、动态数组、字符串、函数参数等。

数据调用区域(Calldata)

  • 特点:
    • 是不可修改的只读区域,用于传递外部函数调用的输入数据。
    • 效率高,Gas 消耗低。
  • 作用:效率高,Gas 消耗低。

存储 (Storage)区域

  • 地址范围:存储区域的地址范围是从 0 到 2^256 - 1。
  • 存储单元:每个存储槽(storage slot)大小为 32 字节(256 位)。
  • 访问方式:存储按照 键值对的形式 存储,每个键为 256 位的存储槽地址,每个值为 256 位的存储内容。
  • 作用:存储合约的全局状态。

Storage Layout (存储布局)

存储状态布局涉及 合约的存储变量如何映射到 EVM 的存储空间。EVM 的存储空间是一个巨大的 256 位地址空间,采用键值对形式。每个存储槽(slot)存储 32 字节数据。

EVM 的存储区域采用 哈希映射结构 存储数据:

  • 简单变量:直接按照顺序存储,每个变量占用一个或部分存储槽。
  • 复杂数据结构:
    • 数组:第一个槽存储长度,数据存储在 keccak256(slot) 地址开始的连续存储槽中。
    • 映射:数据存储在 keccak256(key, slot) 地址处,key 是映射键,slot 是映射变量在合约中的存储槽编号。
    • 结构体:将结构体的成员顺序存储,每个成员分配单独的存储槽。

存储分配规则

  1. 基础规则

    • 变量顺序存储: 每个状态变量按声明顺序分配存储槽,优先填充当前槽剩余空间。
    • 一个槽的大小为 32 字节。
    • 如果变量类型大小超过槽大小(如 uint256),则占据一个完整槽。
    • 较小变量(如 uint8、bool)在同一槽中紧密打包,但不能跨槽。
  2. 映射(Mapping)

    • 映射的键值对(key-value)不会紧密打包在槽中。
    • 每个键(key)对应的值(value)存储在一个唯一的槽中,其地址由keccak256(key, slot) 计算
    • 键(key)不会存储在存储区域中,键值(key-value)对中的“键(key)”并没有单独存储的槽。所以无法直接遍历Mapping
  3. 动态数组与字符串

    • 元数据存储:动态数组的主槽(主存储槽)中存储的是数组的长度。
    • 数据存储:数组的实际数据存储在与主槽分开的连续存储槽中。
      • 数据槽的起始地址由 keccak256(slot) 计算得出,slot 是动态数组的主槽编号。
      • 数组中的每个元素按照顺序依次存储在数据槽中。
    • 字符串与动态数组处理相似。
  4. 结构体(Struct)

    • 结构体的变量按顺序存储。
    • 结构体变量紧密打包(类似单个槽中存储的变量)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.8.0;

contract StateLayoutExample {
uint256 public number; // Slot 0
bool public flag; // Slot 1 (packed with `smallValue` if space allows)
uint16 public smallValue; // Slot 1
mapping(uint256 => uint256) public map; // Data stored at keccak256(key, 2)
uint256[] public dynamicArray; // Slot 3: array length; data starts at keccak256(3)
struct MyStruct {
uint256 largeValue; // Slot 4
uint8 smallPart; // Slot 4 (packed with other struct fields)
}
MyStruct public myStruct; // Struct occupies its own slots
}

执行命令 solc --storage-layout --transient-storage-layout StateLayoutExample.sol 查看合约存储布局

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
{
"storage":[
{"astId":3,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"number","offset":0,"slot":"0","type":"t_uint256"},
{"astId":5,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"flag","offset":0,"slot":"1","type":"t_bool"},
{"astId":7,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"smallValue","offset":1,"slot":"1","type":"t_uint16"},
{"astId":11,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"map","offset":0,"slot":"2","type":"t_mapping(t_uint256,t_uint256)"},
{"astId":14,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"dynamicArray","offset":0,"slot":"3","type":"t_array(t_uint256)dyn_storage"},
{"astId":22,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"myStruct","offset":0,"slot":"4","type":"t_struct(MyStruct)19_storage"}
],
"types":{
"t_array(t_uint256)dyn_storage":{"base":"t_uint256","encoding":"dynamic_array","label":"uint256[]","numberOfBytes":"32"},
"t_bool":{"encoding":"inplace","label":"bool","numberOfBytes":"1"},
"t_mapping(t_uint256,t_uint256)":{"encoding":"mapping","key":"t_uint256","label":"mapping(uint256 => uint256)","numberOfBytes":"32","value":"t_uint256"},
"t_struct(MyStruct)19_storage":{
"encoding":"inplace",
"label":"struct StateLayoutExample.MyStruct",
"members":[
{"astId":16,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"largeValue","offset":0,"slot":"0","type":"t_uint256"},
{"astId":18,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"smallPart","offset":0,"slot":"1","type":"t_uint8"}
],
"numberOfBytes":"64"
},
"t_uint16":{"encoding":"inplace","label":"uint16","numberOfBytes":"2"},
"t_uint256":{"encoding":"inplace","label":"uint256","numberOfBytes":"32"},
"t_uint8":{"encoding":"inplace","label":"uint8","numberOfBytes":"1"}
}
}

storage

描述每个状态变量的存储位置:
- slot: 存储槽号。
- offset: 偏移量,表示变量在槽内的起始位置。
- type: 变量的类型标识,与 types 部分中的描述对应。

types

定义每种数据类型的详细信息:
- encoding: 数据的存储方式(如 inplace 或 dynamic_array)。
- numberOfBytes: 占用的字节数。
- members: 如果是结构体,列出所有成员及其存储位置。
- base: 如果是数组,表示基础类型。

Memory Layout (内存布局)

在 Solidity 中,内存(Memory) 是 EVM 提供的一种线性存储结构,主要用于在函数调用过程中存储临时变量、局部变量和中间计算结果。内存是按需分配的,不同于存储(Storage),内存是 瞬时的,在调用结束后会被释放,且读写成本较低。

内存是一个 按字节寻址的线性空间,地址范围从 0x00 到无限大(理论上为 2^256 - 1)。但在实际操作中,内存的分配是动态增长的,EVM 会根据需要分配内存,并且以 32 字节(一个字)为单位扩展。

- 每个内存地址存储一个字节。
- 每 32 个字节组成一个字(word),这是 EVM 操作的基本单位。

内存布局的设计遵循以下原则:

  1. 按需动态分配:
    • Solidity 会在需要时从内存中分配新的空间。
    • 起始空闲内存地址存储在 0x40,可通过 mload(0x40) 获取。
  2. 按 32 字节对齐:
    • 内存分配是以 32 字节为单位对齐的,即使是 bool 等小数据类型也会占用 32 字节。
  3. 变量存储位置:
    • 固定大小类型直接存储其值。
    • 动态类型存储一个指针,指向实际数据的内存地址。

固定分配区域

Solidity保留了四个32字节的插槽,具体的字节范围(包括端点)使用如下:

  • 0x00 ~ 0x3f(64Byte)
    • 用于哈希方法的临时空间
    • 临时空间可以在语句之间使用(即在内联汇编之中)。
  • 0x40 ~ 0x5f (Free memory pointer 32Byte):
    • mstore(0x40, …) 用于指向当前空闲内存的起始地址。
    • Solidity 使用内存时,会从该地址开始分配。
    • 开发者不需要手动管理,但在使用汇编时,需要注意维护 0x40 的正确值。
  • 0x60 ~ 0x7f(Zero pointer 32Byte):
    • 保留一块全零的内存区域,通常用于返回零值。
    • 0值插槽则用来对动态内存数组进行初始化,且永远不会写入数据 (因而可用的初始内存指针为 0x80)。

Solidity 总会把新对象保存在空闲内存指针的位置, 所以这段内存实际上从来不会空闲(在未来可能会修改这个机制)。

动态分配区域

Solidity 的局部变量、动态数组和字符串都从 0x80 开始动态分配,并按照实际需要扩展。

分配规则:
- 内存分配以 32 字节为单位对齐。
- 临时变量直接写入内存对应的位置。
- 动态类型需要额外存储元数据(如长度和起始地址)。

内存中的数据类型布局

固定大小的类型

  • 包括 uint256、address、bool 等。
  • 直接按 32 字节对齐存储,例如:
    • uint256 占用 32 字节。
    • bool 也占用 32 字节(尽管实际值只需 1 字节)。

动态类型

动态类型(如 string、bytes 和动态数组)会分为两个部分:

  1. 指针部分:主变量存储的是指向实际数据的偏移量(相对于起始地址 0x80 的偏移量)。
  2. 数据部分:存储动态类型的元数据(如长度)和实际数据。
1
2
3
4
5
6
7
8
9
pragma solidity ^0.8.0;

contract MemoryExample {
function demo() public pure returns (bytes memory, string memory) {
bytes memory byteArray = new bytes(10); // 动态字节数组
string memory str = "Hello, Memory!";
return (byteArray, str);
}
}
  1. byteArray:
    • 主变量存储数据偏移量,例如 0x80。
    • 数据存储从 0x80 开始:
      • 长度(10 字节):存储在 0x80。
      • 实际内容(10 字节):从 0xA0 开始。
  2. str:
    • 主变量存储数据偏移量,例如 0xE0。
    • 数据存储从 0xE0 开始:
      • 长度(14 字节):存储在 0xE0。
      • 实际内容(”Hello, Memory!”):从 0x100 开始。

State Layout 和 Memory Layout 的区别

特性 状态布局 (State Layout) 内存布局 (Memory Layout)
存储位置 持久化存储在 EVM 存储 (Storage) 中 函数调用时临时存储在 EVM 内存 (Memory) 中
生命周期 持久化,合约销毁之前一直存在 临时,函数执行结束即释放
成本 写入存储需要高 Gas 成本 使用内存成本较低
用途 存储合约的持久化数据 处理临时计算或函数的中间结果

Gas 优化建议

  • 状态变量优化
    • 紧密打包变量,减少存储槽的使用。
    • 避免在循环中频繁读写存储。
  • 内存优化
    • 使用临时变量代替状态变量进行中间计算。
    • 适时释放不再使用的内存。

了解 Solidity 的状态与内存布局,能帮助开发者高效设计合约,同时优化 Gas 成本。

从设计原则、编码技巧、数据存储优化和具体示例四个方面,系统地讲解如何在 Solidity 合约中节省 Gas 费用。

设计原则

减少合约调用次数

  • 将多个逻辑步骤合并到单一函数中,避免多次外部调用。
  • 尽量减少合约之间的交互,因为每次外部调用都涉及高 Gas 消耗。

避免频繁修改状态变量

  • 修改状态变量(如 storage)比操作本地变量(如 memory)耗费更多 Gas。
  • 在逻辑中优先使用本地变量,计算完后一次性更新状态变量。

简化复杂计算

  • 将复杂计算移到链下处理,通过链上存储结果代替。
  • 在链下生成哈希值等,只存储结果以减少链上计算开销。

编码技巧

使用 view 和 pure 函数

view 函数不修改状态,pure 函数不依赖链上状态,调用它们不消耗 Gas。

1
2
3
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}

精简数据类型

  • 使用适合的数据类型,比如 uint8 而非 uint256(仅当值范围确定小于 256)。
  • 避免布尔值,因为布尔操作实际存储在 256 位。

利用 calldata 代替 memory

函数的外部输入参数可用 calldata,节省内存分配成本。

1
2
3
function processData(uint[] calldata data) external {
// calldata 参数直接使用,无需复制
}

事件替代状态变量

使用 emit 事件记录某些数据,而非存储状态变量。事件存储在日志中,成本低于 storage。

1
event DataProcessed(address indexed user, uint amount);

合理使用 immutable 和 constant

  • immutable:部署后值不可更改,但比普通变量读取快。
  • constant:在编译时决定值,不占用存储空间。
1
2
uint256 public immutable deployTime = block.timestamp;
uint256 public constant FEE_RATE = 100;

数据存储优化

批量更新状态变量

将多次状态变量更新合并为一次。

1
2
3
4
5
6
7
// 不推荐
balance = balance + 10;
counter = counter + 1;

// 推荐
balance += 10;
counter += 1;

使用结构体或映射存储数据

将多个相关变量合并到一个 struct 或映射中。

1
2
3
4
5
struct User {
uint256 balance;
uint256 lastUpdated;
}
mapping(address => User) users;

减少存储变量数量

优化存储布局,减少存储槽的使用(每个槽 32 字节)。多个变量可以合并到一个存储槽中。

1
2
3
4
5
6
7
8
9
10
// 不推荐
uint128 public var1;
uint128 public var2;

// 推荐
struct Combined {
uint128 var1;
uint128 var2;
}
Combined public data;

删除不必要的状态变量

使用 delete 删除已用完的存储变量,释放存储空间。

1
delete users[userAddress];

其他优化建议

  1. 代码复用
  • 将重复逻辑封装为内部函数,减少代码重复和部署大小。
  • 使用库(library)减少代码复制。
  1. 优化循环
  • 避免大规模循环操作,因为每次迭代都会增加 Gas。
  • 可以分阶段处理数据,减少单次调用的负载。
  1. Gas 预言机
  • 部署前使用工具(如 Remix、Hardhat)模拟和估算 Gas。
  • 优化部署的 Gas 成本。

在 Solidity 中,一个合约可以通过多种方式在另一个合约中创建和管理另一个合约。

合约内部部署(创建一个新合约

Solidity 允许你在一个合约内部部署另一个合约。这通常是通过在合约中创建一个新的合约实例来实现的。你可以在构造函数或其他函数中动态部署一个新合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract NewContract {
address public owner;

constructor(address _owner) {
owner = _owner;
}

function getOwner() public view returns (address) {
return owner;
}
}

contract CreatorContract {
address public lastCreatedContract;

function createNewContract() public {
NewContract newContract = new NewContract(msg.sender);
lastCreatedContract = address(newContract);
}
}

解释:

  • CreatorContract 中有一个 createNewContract 函数,它使用 new 关键字创建了一个 NewContract 实例。NewContract 合约的构造函数接收 msg.sender 作为参数,意味着新合约的 owner 将是调用 createNewContract 函数的账户。
  • 创建的新合约地址会存储在 lastCreatedContract 中。

合约工厂模式(Factory Pattern)

另一种常见的做法是使用工厂模式,即一个合约作为工厂来部署多个合约实例。工厂合约允许你根据需要创建新合约,并管理这些合约实例。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Item {
uint public id;
address public creator;

constructor(uint _id, address _creator) {
id = _id;
creator = _creator;
}
}

contract ItemFactory {
Item[] public items;

function createItem(uint _id) public {
Item newItem = new Item(_id, msg.sender);
items.push(newItem);
}

function getItems() public view returns (Item[] memory) {
return items;
}
}

解释:

  • ItemFactory 是一个工厂合约,它可以创建多个 Item 合约的实例,每个 Item 合约的创建者是调用工厂合约的账户。
  • createItem 函数部署了一个新的 Item 合约,并将它存储在 items 数组中。

调用外部合约(外部合约部署后实例化)

有时,合约不需要在自己内部部署其他合约,而是可以通过调用已经部署的外部合约的地址来与其交互。这不是“创建”合约,但却是一种与其他合约交互的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IExternalContract {
function getOwner() external view returns (address);
}

contract ContractInteraction {
IExternalContract public externalContract;

constructor(address _externalContractAddress) {
externalContract = IExternalContract(_externalContractAddress);
}

function getExternalOwner() public view returns (address) {
return externalContract.getOwner();
}
}

解释:

  • ContractInteraction 合约通过接口与外部合约交互,调用了 IExternalContract 中的 getOwner 函数。
  • 外部合约已经部署并被传入构造函数,ContractInteraction 合约与它交互。

使用 create2 来部署合约(指定地址部署)

create2 是一种更高级的部署方法,它允许你通过指定合约地址来部署合约。使用 create2 可以确保合约在指定的地址上部署,前提是你知道合约的字节码和盐值。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FactoryWithCreate2 {
event ContractCreated(address contractAddress);

function createNewContract(bytes32 salt) public {
address newContractAddress;
bytes memory bytecode = type(NewContract).creationCode;

assembly {
newContractAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}

emit ContractCreated(newContractAddress);
}
}

contract NewContract {
address public creator;

constructor() {
creator = msg.sender;
}
}

解释:

  • createNewContract 函数使用 create2 来部署一个新的 NewContract 合约。
  • create2 允许你在部署时指定一个 salt,确保你可以预测到部署合约的地址,前提是知道合约的字节码。

扩展信息

在 Solidity 中,type(NewContract).creationCode 是一种用于获取合约字节码的方法,它表示合约的创建代码。合约的字节码(bytecode)是它部署到区块链上的机器代码,其中包含了合约的逻辑和部署时所需的初始化数据。

合约字节码的组成部分

合约字节码可以分为以下几个主要部分:

  1. 合约构造函数的初始化字节码
  2. 部署过程中的合约代码
  3. 合约的状态变量初始值

合约构造函数的初始化字节码

  • 构造函数的字节码:每个合约在部署时都会执行其构造函数。creationCode 中包含了构造函数的代码部分,它负责初始化合约的状态,通常包括合约的状态变量赋值、事件初始化等操作。
  • 初始化参数:如果构造函数带有参数(例如合约需要初始化的状态变量),这些参数也会包含在 creationCode 中。部署合约时需要传递这些参数。

creationCode:合约的“创建代码”,是用于部署合约的字节码,包含了构造函数的执行内容以及初始化步骤。在合约部署时,链上会执行这段创建代码,初始化合约,并且部署合约的 runtimeCode

type(NewContract).creationCode 提供了一个方法来获取合约的 创建代码(即部署合约时使用的字节码),这段代码由合约的构造函数(constructor)以及合约的初始化部分组成,执行时会将合约的运行时代码存储在区块链上。

creationCode 是合约创建时所需的字节码,包括构造函数的初始化部分、合约的部署逻辑等。

部署过程中的合约代码

  • 字节码本体:合约部署的代码(通常是 runtime code)会在合约创建时存储在链上。这个字节码本体包括了所有的业务逻辑和函数实现。它包含了所有合约的行为和功能,例如状态修改、函数调用等。

runtimeCode:合约部署后剩余的代码,包含了合约的所有业务逻辑。合约一旦部署完成,它的 runtimeCode 就是唯一存在于区块链上的代码,而 creationCode 则只在合约创建时有作用。

合约的状态变量初始值

  • 初始化存储:合约在部署时,通常会有一些状态变量。它们的初始值会作为部署合约时的一部分被编码到创建代码中(如果有默认值)。这些值将在合约部署后存储到合约的状态中。

create 和 create2

在 Solidity 中,create 和 create2 是用于部署合约的两种不同方法。二者的主要区别在于如何计算合约地址及其对地址预测的能力。理解这两者的原理及其差异对于优化合约部署和管理合约地址是非常重要的。

create 的原理

create 是最常见的合约部署方式,它通过在 Ethereum 网络上发送一笔交易来部署一个新的合约。

  • 部署过程:合约的字节码通过交易提交到区块链网络,然后在区块链中分配一个新的地址。
  • 地址计算:create 部署的合约的地址由 创建者的地址(msg.sender) 和 创建交易的nonce 决定。

合约地址计算公式:

1
address = keccak256(rlp_encode(sender, nonce))[12:]
  • sender:部署合约的账户地址(msg.sender)。
  • nonce:msg.sender 的交易计数,即发送交易的次数。
1
address create(uint256 value, bytes memory bytecode, uint256 bytecodeSize)
  1. value(uint256):该参数表示向新合约发送的以太币数量。在合约部署时,你可以指定一个 value,这部分以太币将转移到新部署的合约中。若不想发送以太币,value 为 0。
    value 是一个 uint256 类型,表示你要发送到新合约的以太币金额。
  2. bytecode(bytes memory):即合约的字节码,它是合约代码的二进制形式。这是通过 type(Contract).creationCode 或其他类似的方法获取的合约字节码。
    这段字节码包含了合约的构造函数以及合约的逻辑代码。可以通过 type(MyContract).creationCode 获取。add(bytecode, 0x20) 字节码的存储位置,跳过前 32 字节,指向字节码的实际数据。
  3. bytecodeSize(uint256) : 它表示合约字节码的长度(通常是 32 字节)。

使用create

create 是标准的合约部署方式,适用于常规场景。通常情况下,您无需关心合约的地址,只需让合约被部署到区块链即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.8.0;

contract NewContract {
uint256 public value;

constructor(uint256 _value) {
value = _value;
}
}

contract Factory {
address public deployedContract;

function deploy(uint256 _value, bytes32 _salt) external {
bytes memory bytecode = type(NewContract).creationCode; // 获取合约字节码
address contractAddress;

assembly {
contractAddress := create(0, add(bytecode, 0x20), mload(bytecode))
}

deployedContract = contractAddress; // 记录新合约地址
}
}

在这个例子中,Factory 合约会通过 create 部署一个新的 NewContract 合约。NewContract 的地址由 msg.sender 和 nonce 决定,因此无法在部署前预测。

create2 的原理

create2 是 Solidity 0.5.0 引入的一种新的合约部署方式,它为部署合约提供了额外的控制能力,特别是在可预测的合约地址生成方面。与 create 不同,create2 的合约地址不仅由部署者的地址和交易 nonce 决定,还由以下额外的内容决定:

  • 部署者的地址 (msg.sender)
  • salt:一个额外的任意字节值
  • 合约字节码的哈希:即部署的合约的字节码。

合约地址计算公式:

1
address = keccak256(0xff ++ sender ++ salt ++ keccak256(bytecode))[12:]
  • 0xff 是一个常量,用于确保合约地址计算的唯一性。
  • sender 是合约创建者的地址。
  • salt 是一个提供给 create2 的自定义值,可以是任意字节。
  • bytecode 是合约的字节码。

create2 使得合约的地址可以在合约部署之前预先计算出来,因此我们可以在部署合约之前预测合约的地址。

1
address create2(uint256 value, bytes memory bytecode, uint256 bytecodeSize, bytes32 salt)
  1. value(uint256):该参数表示向新合约发送的以太币数量。在合约部署时,你可以指定一个 value,这部分以太币将转移到新部署的合约中。若不想发送以太币,value 为 0。
    value 是一个 uint256 类型,表示你要发送到新合约的以太币金额。
  2. bytecode(bytes memory):即合约的字节码,它是合约代码的二进制形式。这是通过 type(Contract).creationCode 或其他类似的方法获取的合约字节码。
    这段字节码包含了合约的构造函数以及合约的逻辑代码。可以通过 type(MyContract).creationCode 获取。add(bytecode, 0x20) 字节码的存储位置,跳过前 32 字节,指向字节码的实际数据。
  3. bytecodeSize(uint256) : 它表示合约字节码的长度(通常是 32 字节)。
  4. salt(bytes32):salt 是一个自定义的 bytes32 类型值,它为合约的创建地址提供一个“随机数”。salt 用于确保合约地址计算的唯一性。通过修改 salt 的值,可以控制生成的合约地址,从而为每个合约生成一个独特的地址。
    salt 是唯一标识合约部署的一部分。你可以通过传入不同的 salt 值来生成不同的合约地址,即使合约字节码完全相同。

使用create2

create2 则允许你指定一个 salt 值,进而可以提前计算出部署后的合约地址。这对于某些应用非常有用,例如需要在合约创建之前就知道合约的地址,或者在某些情境下需要复用相同地址的合约。

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
pragma solidity ^0.8.0;

contract NewContract {
uint256 public value;

constructor(uint256 _value) {
value = _value;
}
}

contract Factory {
address public deployedContract;

function deploy(uint256 _value, bytes32 _salt) external {
bytes memory bytecode = type(NewContract).creationCode; // 获取合约字节码
bytes32 salt = _salt; // 使用用户传入的 salt 值
address contractAddress;

assembly {
contractAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}

deployedContract = contractAddress; // 记录新合约地址
}
}

在这个例子中,Factory 合约使用 create2 部署一个 NewContract 合约。通过传入 salt 值,create2 会计算出一个特定的合约地址。这样,你可以在合约部署之前预测合约的地址。合约地址的计算基于 bytecode 和 salt 的哈希,因此只要 salt 和 bytecode 不变,生成的地址也将是固定的。

create 和 create2 的主要区别

特性 create create2
地址生成 由 msg.sender 地址和交易 nonce 决定 由 msg.sender 地址、salt 和合约字节码的哈希决定
预测地址 无法预测合约地址(取决于交易 nonce 和区块时间) 可以在合约部署前预测合约地址
灵活性 地址不可控制,无法提前决定 可以通过不同的 salt 值控制地址并预先计算
安全性 地址依赖于交易 nonce,可能会有一定的不确定性 地址可由部署者提前确定,减少了竞争条件
适用场景 普通合约部署 需要提前知道合约地址的场景
使用复杂度 简单 需要手动计算字节码和 salt

参考&鸣谢

重入攻击(Reentrancy Attack)

重入攻击是指攻击者通过调用目标合约的某个函数,利用该函数在完成关键状态变量更新前调用外部合约的能力,从而反复调用目标合约的函数,最终实现非法的资金提取或状态操作。

漏洞合约

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
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vulnerable {
mapping(address => uint256) public balances;

// 存款函数
function deposit() public payable {
balances[msg.sender] += msg.value;
}

// 提款函数,存在重入攻击漏洞
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");

// 向用户发送资金
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");

// 更新用户余额
balances[msg.sender] -= amount;
}

// 合约余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}

这是一个容易受到重入攻击的合约,它允许用户存款并提款。

问题:

  • 在发送资金后,余额更新在后。
  • 攻击者可以通过调用自己的回调函数,在 withdraw 执行完成前再次调用 withdraw,从而重复提款。

攻击合约

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
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./Vulnerable.sol";

contract Attack {
Vulnerable public vulnerable;

constructor(address _vulnerableAddress) {
vulnerable = Vulnerable(_vulnerableAddress);
}

// 回退函数:用于重入攻击
fallback() external payable {
if (address(vulnerable).balance >= 1 ether) {
vulnerable.withdraw(1 ether); // 重复调用
}
}

// 攻击函数
function attack() public payable {
require(msg.value >= 1 ether, "Need at least 1 ether");
vulnerable.deposit{value: 1 ether}(); // 存款
vulnerable.withdraw(1 ether); // 首次提款
}

// 查看攻击合约余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}

攻击步骤:

  1. 攻击者部署 Attack 合约并调用 attack。
  2. 调用 vulnerable.withdraw(1 ether) 时触发回退函数 fallback。
  3. 回退函数再次调用 withdraw,在余额未更新之前反复提款。

解决方法

  • 状态变量优先更新:在执行外部调用前,先更新合约的状态。
  • 通过加锁来限制重入:通过给function加锁,来限制重入,以达到防止攻击的目的。

状态变量优先更新

将状态变量的更新移到外部调用之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vulnerable {
mapping(address => uint256) public balances;

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");

// 先更新余额,后转账
balances[msg.sender] -= amount;

(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}

function getBalance() public view returns (uint256) {
return address(this).balance;
}
}

通过加锁来限制重入

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
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vulnerable {
mapping(address => uint256) public balances;

// 通过加锁来限制重入
bool private locker = false;
modifier locked() {
require(!locker, "reentrant call");
locker = true;
_;
locker = false;
}

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) public locked {
require(balances[msg.sender] >= amount, "Insufficient balance");

(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}

function getBalance() public view returns (uint256) {
return address(this).balance;
}
}

也可通过使用OpenZeppelin中提供的ReentrancyGuard工具来防止攻击,ReentrancyGuard机制与锁机制相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Vulnerable is ReentrancyGuard {
mapping(address => uint256) public balances;

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");

(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}

function getBalance() public view returns (uint256) {
return address(this).balance;
}
}