0%

在编写 Solidity 智能合约时,安全性是最重要的考虑因素之一。以下是开发中常见的安全问题及其解决方法:

重入攻击(Reentrancy Attack)

https://zhoubofsy.github.io/2024/11/28/blockchain/ethereum/solidity-security-reentrancy-attack/

溢出与下溢(Overflow and Underflow)

溢出(Overflow)和下溢(Underflow)是指在处理整数运算时,当结果超出数据类型表示范围时,值会“环绕”到另一端。例如,对于 uint8 类型:

  • 溢出: uint8 的最大值是 255,如果执行 255 + 1,值会变为 0。
  • 下溢: uint8 的最小值是 0,如果执行 0 - 1,值会变为 255。

在早期的 Solidity 版本中,这种现象常导致严重漏洞,攻击者可以利用这种行为达到意想不到的目的。自 Solidity 0.8 开始,溢出和下溢会触发异常,但了解它们的历史以及如何防范仍然很重要。虽然Solidity 0.8开始增加了溢出检查,这也会导致Gas费用的增加。

在 Solidity 0.8+ 中,每次整数加减运算都会隐式包含溢出检查逻辑,而这种检查需要额外的操作指令,因此会增加运行时消耗的 gas。

Solidity 0.8+

1
2
3
4
5
6
7
pragma solidity ^0.8.0;

contract OverflowCheck {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // 含溢出检查
}
}
1
2
3
4
5
6
7
8
0x00    PUSH1  0x60
0x02 MSTORE
0x03 CALLDATALOAD
0x04 CALLDATALOAD
0x05 ADD // 加法
0x06 JUMPI // 检查溢出
0x07 REVERT // 如果溢出,回滚
0x08 RETURN

Solidity 0.7

1
2
3
4
5
6
7
pragma solidity ^0.7.0;

contract NoOverflowCheck {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // 无溢出检查
}
}
1
2
3
4
5
6
0x00    PUSH1  0x60
0x02 MSTORE
0x03 CALLDATALOAD
0x04 CALLDATALOAD
0x05 ADD // 直接加法
0x06 RETURN
  1. Solidity 0.8 的溢出检查增加了 ~50 gas 的开销。
  2. 增加的开销源于额外的汇编指令(JUMPI 和 REVERT)。
  3. 如果对 gas 成本敏感,可以使用 unchecked 块绕过检查,但需确保逻辑安全。

未检查的外部调用(Unchecked External Call)

“未检查的外部调用”漏洞(Unchecked External Call)是 Solidity 智能合约中的一种常见安全问题。如果智能合约与外部地址交互(比如转账或调用另一个合约的函数)时未检查调用的结果,可能会引发意外的后果和漏洞利用。

潜在问题

  1. 失败的调用未被检测
    如果调用失败但未进行检查,合约可能会继续执行后续逻辑,从而导致不一致状态或意外行为。
    示例问题:转账操作失败,但余额已经从发送方扣除。
  2. 错误处理被忽略
    外部合约调用可能由于异常(例如合约不存在或代码逻辑错误)而失败。如果不检查返回值,调用方将无法正确处理这些失败。
  3. 逻辑漏洞导致资金损失
    如果未检查调用结果的返回值,攻击者可以通过故意设计失败的合约逻辑,扰乱调用方合约的资金或状态管理。
  4. 影响合约的可组合性
    在 DeFi 等应用场景中,不同合约之间通常会进行复杂交互。如果未正确检查外部调用的结果,会影响合约间的协作,甚至破坏整个生态系统的安全性。

漏洞示例代码

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

contract UncheckedExternalCall {
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");

// 发送资金,但未检查返回值
payable(msg.sender).call{value: amount}("");

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

恶意示例代码
攻击者可以部署一个恶意合约,故意使 call 失败(例如,使用耗尽 gas 的回退函数),从而导致 balances 状态不一致。

1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Malicious {
fallback() external payable {
// 消耗所有 gas,故意让调用失败
while (true) {}
}
}

解决方案

显式检查 call 返回的布尔值,以确保调用成功:

1
2
3
4
5
6
7
8
9
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");

// 检查调用结果
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");

balances[msg.sender] -= amount;
}

随机数生成不安全

为什么Solidity 中随机数生成不安全?

在 Solidity 中,常见的随机数生成方式(如依赖区块哈希、时间戳等)容易被攻击者预测或操控:

  1. 区块哈希依赖(blockhash): 区块哈希是公开信息,矿工可以选择不挖某些区块,进而影响生成的随机数。
  2. 时间戳依赖(block.timestamp): 矿工可以小幅调整时间戳,使得随机数变为对其有利的值。
  3. 合约状态依赖(msg.sender, block.difficulty 等): 合约状态和交易上下文的信息可以被预测和操控。

这些方式在确定性(公开信息和上下文)和可操控性(矿工或攻击者干预)方面不安全。

如何安全生成随机数?

  1. Chainlink VRF:
    • 可证明公平且可验证的随机数生成器(RNG),它使智能合约能够在不影响安全性或可用性的情况下访问随机值。
    • 建立区块链游戏和NFT
    • 随机分配职责和资源。例如,随机分配法官到案件。
    • 为共识机制选择一个具有代表性的样本。
      用法:https://docs.chain.link/vrf/v2-5/getting-started
  2. Off-chain Randomness (链下随机数):
    • 使用链下生成随机数(如通过服务器或 API),然后将结果提交到链上。
    • 缺点:需要信任链下服务,可能会被操控。
    • 改进:通过多方签名或可信执行环境(如 Intel SGX)生成。
  3. Threshold Signature Schemes (TSS):
    • 多方合作生成随机数,每方只持有部分秘密信息。
    • 优势:随机数完全由多方参与生成,无法单点操控。
    • 使用场景:像 Chainlink 的去中心化预言机网络。
  4. VDF(Verifiable Delay Function):
    • 一种不可预测的随机数生成方式,需要一定时间来计算结果。
    • 优势:矿工无法提前操控随机数。
    • 实现:Ethereum 2.0 的随机数生成设计中引入了 VDF。

智能合约升级问题

智能合约在部署后通常无法直接修改,这可能导致升级和维护困难。如果设计不当,可能需要完全重启项目,从而浪费资源并导致用户信任危机。

解决方案 1:代理合约(Proxy Pattern)

工作原理

通过代理模式将合约分为两部分:

  1. 代理合约(Proxy): 存储状态变量,负责将用户请求委托给逻辑合约。
  2. 逻辑合约(Logic/Implementation): 包含实际的业务逻辑,可以升级和替换。

用户总是与代理合约交互,代理通过 delegatecall 将调用转发到逻辑合约,使用代理合约的存储保持状态一致。

具体实现

  1. UUPS Proxy
    • 使用 upgradeTo 方法直接升级逻辑合约。
    • 更加轻量,但需要实现升级逻辑。
  2. Transparent Proxy
    • 避免管理员调用代理时出现冲突。
    • 管理员可升级逻辑合约,用户调用时自动转发。
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Proxy {
uint256 public number;
address public implementation;
address public admin;

constructor(address _implementation) {
admin = msg.sender;
implementation = _implementation;
}

fallback() external {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}

function upgrade(address newImplementation) external {
require(msg.sender == admin, "Only admin can upgrade");
implementation = newImplementation;
}

function getData(uint256 _num) public pure returns (bytes memory) {
return abi.encodeWithSignature("setNumber(uint256)", _num);
}
}

// Logic contract (v1)
contract LogicV1 {
uint256 public number;
address public implementation;
address public admin;
uint256 public privateNum;

function setNumber(uint256 _number) public {
number = _number;
privateNum = _number;
}
}

// Logic contract (v2)
contract LogicV2 {
uint256 public number;
address public implementation;
address public admin;
uint256 public privateNum;

function setNumber(uint256 _number) public {
number = _number * 2; // Updated logic
privateNum = _number * 2;
}
}

优点

  • 无需重新部署存储状态,升级逻辑灵活。
  • 用户无感知升级操作。

缺点

  • 增加代码复杂度。

解决方案2:模块化合约(Modular Contract)

工作原理

将合约分解为多个独立模块,所有模块通过核心合约(Registry 或 Router)进行管理。每个模块可单独替换而不影响整体功能。

具体实现

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

interface ILOGIC {
function setNumber(uint256 _number) external ;
}

contract Router {
mapping(string => address) public modules;
address public admin;

constructor() {
admin = msg.sender;
}

function updateModule(string memory moduleName, address moduleAddress) external {
require(msg.sender == admin, "Only admin can update");
modules[moduleName] = moduleAddress;
}

function setNumber(string memory moduleName, uint256 _num) external {
address module = modules[moduleName];
require(module != address(0), "Module not found");
ILOGIC(module).setNumber(_num);
}
}

// Logic contract (v1)
contract LogicV1 is ILOGIC {
uint256 public number;
uint256 public privateNum;

function setNumber(uint256 _number) public {
number = _number;
privateNum = _number;
}
}

// Logic contract (v2)
contract LogicV2 is ILOGIC {
uint256 public number;
uint256 public privateNum;

function setNumber(uint256 _number) public {
number = _number * 2; // Updated logic
privateNum = _number * 2;
}
}

优点

  • 便于扩展新功能或模块。
  • 每个模块的更新不会影响其他部分。

缺点

  • 初始设计复杂度较高。
  • 需要额外存储模块地址。

解决方案 3:数据分离(Storage Contract)

工作原理

将状态数据和业务逻辑分离为两个独立合约:

  1. 存储合约(Storage Contract): 负责存储状态变量。
  2. 逻辑合约(Logic Contract): 包含具体逻辑,可单独升级。

具体实现

在上一个实现示例的基础上,进行存储合约和逻辑合约的分离。

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

interface ISTORE {
function setNumber(uint256 _number) external ;
}

contract Store is ISTORE {
uint256 public number;

function setNumber(uint256 _number) external {
number = _number;
}
}

interface ILOGIC {
function setNumber(uint256 _number, ISTORE store) external ;
}

contract Router {
mapping(string => address) public modules;
address public admin;
ISTORE public store;

constructor() {
admin = msg.sender;
store = new Store();
}

function updateModule(string memory moduleName, address moduleAddress) external {
require(msg.sender == admin, "Only admin can update");
modules[moduleName] = moduleAddress;
}

function setNumber(string memory moduleName, uint256 _num) external {
address module = modules[moduleName];
require(module != address(0), "Module not found");
ILOGIC(module).setNumber(_num, store);
}
}

// Logic contract (v1)
contract LogicV1 {
uint256 public privateNum;

function setNumber(uint256 _number, ISTORE store) public {
store.setNumber(_number);
privateNum = _number;
}
}

// Logic contract (v2)
contract LogicV2 is ILOGIC {
uint256 public privateNum;

function setNumber(uint256 _number, ISTORE store) public {
store.setNumber(_number * 2);
privateNum = _number * 2;
}
}

优点

  • 清晰的数据和逻辑分离。
  • 升级逻辑时不会影响存储。

缺点

  • 初次部署复杂。
  • 存储和逻辑合约间的交互增加调用成本。

ERC20中“approve”无限授权的问题

竞争环境

ERC20 的授权与竞争

假设场景

假设有一个用户 Alice 和一个智能合约 Token,以及一个攻击者 Mallory。Alice 想通过 approve 函数授权 Mallory 代她消费代币。

初始状态:

Alice 的账户余额为 1000 个 Token。
Mallory 尚未获得任何授权。
Token 合约中的 approve 函数允许 Alice 授权 Mallory一定额度的代币。
Alice 执行以下两个交易:

approve(Mallory, 100); —— 授权 100 个代币。
想撤销授权后重新授权:approve(Mallory, 200); —— 授权 200 个代币。

第一种竞争:在授权更新中被抢占

在区块链网络中,交易可能存在提交和打包的时间差。

  1. 交易流程:
    • Alice 提交第一笔交易 approve(Mallory, 100)。
    • 此时 Mallory 知道 Alice 即将授权 100,便迅速构造一个调用 transferFrom 的交易,试图转移代币。
    • 在 Alice 提交第二笔交易 approve(Mallory, 200) 之前,Mallory 的交易被矿工打包处理。
  2. 结果:
    • Mallory 调用了 transferFrom,并转移了 100 个代币。
    • Alice 的第二笔交易(授权 200)覆盖了第一笔交易。
    • 最终,Mallory不仅得到了 100,还能再次调用 transferFrom 转移额外的 200 个代币。

第二种竞争:替代攻击

以太坊允许用户在一笔交易未被确认前,用新的交易替换原交易(这被称为“交易替代”)。攻击者可以利用这种特性引发意外行为。

  1. 交易流程:
    • Alice 提交 approve(Mallory, 100)。
    • 在这笔交易确认前,Alice 提交了一笔新的高 gas 交易 approve(Mallory, 0)。
    • Mallory发送了一笔高优先级 transferFrom 交易,与 Alice 的两笔交易竞争。
  2. 结果:
    • 如果矿工按照特定顺序处理,Mallory 的 transferFrom 会先于 Alice 的 approve(Mallory, 0)。
    • 最终,Mallory 成功提取了授权的 100 个代币,尽管 Alice 试图撤销授权。

尽管区块链中交易的执行是严格顺序的,但“竞争”主要源于交易在网络中传播和排序的行为。以下是两个核心原因:

  1. 交易的传播和未确认状态:
    • 在用户提交交易后,交易需要传播到网络中,并等待矿工打包。
    • 在交易被打包前的这段时间,其他用户或智能合约可以通过观察未确认交易,针对交易中暴露的状态进行恶意操作。
  2. 矿工的交易排序权:
    • 矿工可以根据 gas 费优先级或其他策略排序交易。
    • 攻击者可以支付更高的 gas,让他们的交易先执行,从而“抢占”状态更新。

解决竞争

  1. 增加原子性操作:
    • 将 approve 和 transferFrom 的逻辑合并为一个原子操作,避免多笔交易间的时间窗口。

      1
      2
      3
      4
      function safeTransfer(address to, uint256 amount) public {
      approve(to, amount);
      transferFrom(msg.sender, to, amount);
      }
  2. 使用 increaseAllowance 和 decreaseAllowance:
    • 避免覆盖现有授权额度,消除修改竞态。
  3. 限时授权:
    • 设置一个时间锁,授权只能在一段时间后生效。

“approve”无限授权

ERC20中“approve”无限授权的问题,其本质并不是单纯由于区块链上的竞争,而是 ERC20标准的设计缺陷与区块链环境特性结合 导致的。这种问题由两个关键因素共同作用引发:

  1. ERC20 标准中的设计缺陷

    • 在 ERC20 的 approve 方法中,没有提供一种明确的原子操作机制来更新授权额度。
    • 在用户更改授权额度的过程中(如从 100 改为 200),攻击者可以利用时间窗口,通过 transferFrom 操作,在授权额度发生变化的过渡阶段恶意提取代币。

    这个问题根源在于 approve 和 transferFrom 的设计缺乏配合机制,允许在两个操作之间发生不一致的状态。

  2. 区块链上的竞争特性

    • 交易传播时间差:当用户试图提交新授权(如 approve(Mallory, 200)),旧授权(如 approve(Mallory, 100))仍然有效,且存在被利用的时间窗口。
    • 矿工排序自由:攻击者可以观察到用户提交的 approve 交易,通过支付更高的 gas 优先让他们的交易 transferFrom 被打包,抢在用户的状态更新之前执行。

    这两点特性导致 approve 无法原子更新的问题被放大,从而使攻击成为可能。

解决“approve”无限授权问题

  1. 增加 increaseAllowance 和 decreaseAllowance 方法

通过调整现有额度而不是直接覆盖额度,可以避免授权被覆盖的问题。

1
2
3
4
5
6
7
8
9
10
11
12
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
allowance[msg.sender][spender] += addedValue;
emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
return true;
}

function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
require(allowance[msg.sender][spender] >= subtractedValue, "Decreased allowance below zero");
allowance[msg.sender][spender] -= subtractedValue;
emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
return true;
}
  1. 原子操作结合转账

通过单一交易完成授权和转账,避免分离操作引入的时间窗口

1
2
3
4
5
6
function safeTransferFrom(address from, address to, uint256 amount) public {
require(allowance[from][msg.sender] >= amount, "Allowance exceeded");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
}

ERC20 中“approve”无限授权的问题,并不完全是由于区块链竞争特性,而是由 授权更新的非原子性设计 和 区块链上交易排序特性 共同导致的。优化方法可以从改进 ERC20 的逻辑(如 increaseAllowance),有效解决这一问题。

这是一段openzeppelin官方生成的代码

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
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract BobToken is ERC20, ERC20Pausable, AccessManaged, ERC20Permit {
constructor(address initialAuthority)
ERC20("BobToken", "BBK")
AccessManaged(initialAuthority)
ERC20Permit("BobToken")
{}

function pause() public restricted {
_pause();
}

function unpause() public restricted {
_unpause();
}

function mint(address to, uint256 amount) public restricted {
_mint(to, amount);
}

// The following functions are overrides required by Solidity.

function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}
}

这段代码定义了一个名为BobToken的智能合约,它继承了多个标准接口和合约,用于创建一个可暂停、可授权管理、可使用EIP-2612标准的许可(Permit)功能的代币。下面是对代码的详细解释:

继承的合约和接口

  1. ERC20: ERC20是一个标准接口,定义了代币的基本功能,如总供应量、余额查询、转账等。
  2. ERC20Pausable: 这个接口允许代币的转账操作被暂停,从而防止在特定情况下(如紧急情况)的代币转移。
  3. AccessManaged: 这个接口可能定义了访问控制功能,比如只有特定地址(如管理员)才能执行某些操作。
  4. ERC20Permit: 这个接口允许代币持有者通过签名授权来批准第三方转移代币,而不需要直接调用转账函数。

继承顺序

在Solidity中,合约的继承顺序是从左到右的,即最左边的合约是基类,最右边的合约是派生类。例如,在contract BobToken is ERC20, ERC20Pausable, AccessManaged, ERC20Permit {}中,BobToken继承了ERC20、ERC20Pausable、AccessManaged和ERC20Permit。

继承原则

  1. 从左到右的继承顺序:如上所述,继承顺序是从左到右的,这意味着基类在派生类之前被继承。
  2. 构造函数的调用:在派生类的构造函数中,必须显式地调用所有基类的构造函数。调用顺序必须遵循继承顺序,即从左到右。
  3. 函数覆盖:如果基类和派生类中有同名的函数,那么派生类的函数会覆盖基类的函数。在调用这些函数时,会优先调用派生类的函数。
  4. 库的使用:如果合约继承了一个库,那么库的函数会被覆盖。这是因为库的函数是内部函数,不能被覆盖。

构造函数

1
2
3
4
5
constructor(address initialAuthority)
ERC20("BobToken", "BBK")
AccessManaged(initialAuthority)
ERC20Permit("BobToken")
{}
  • initialAuthority: 初始化合约的管理权限地址。
  • 构造函数中调用了多个基类的构造函数,传递了必要的参数,如代币名称、符号、初始权限地址等。ERC20、AccessManaged和ERC20Permit的构造函数被显式地调用,并且按照继承顺序从左到右调用。

主要函数

  1. pause(): 暂停代币的转账操作。只有通过AccessManaged接口授权的地址才能调用。
  2. unpause(): 恢复代币的转账操作。同样,只有授权的地址才能调用。
  3. mint(address to, uint256 amount): 允许授权地址铸造新的代币,并将其发送给指定的地址。这通常用于代币发行。

内部函数

1
2
3
4
5
6
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}

_update函数是一个内部函数,用于更新代币的余额。它重写了ERC20ERC20Pausable中的同名函数,并在内部调用了super._update,确保了在更新余额时考虑了暂停状态。

注意事项

  • 访问控制: AccessManaged接口确保了只有授权的地址才能执行关键操作,如暂停、铸造等。
  • 暂停功能: ERC20Pausable接口提供了暂停和恢复转账操作的功能,这在处理紧急情况时非常有用。
  • EIP-2612 Permit标准: 通过实现ERC20Permit接口,用户可以授权第三方代币转移,而无需直接调用转账函数,这提高了安全性。

总的来说,这段代码实现了一个功能丰富的代币合约,结合了多种安全性和管理功能,适用于需要严格控制访问和操作的场景。接下来,我们按照继承顺序逐个来介绍一下。

Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}

function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}

function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}

这段代码定义了一个名为Context的抽象合约。这个合约主要用于提供关于当前执行上下文的信息,包括交易的发送者和数据。在处理元交易(meta-transactions)时,发送和支付执行费用的账户可能不是实际的应用程序发送者。

实现原理

  1. _msgSender()函数:这个函数返回当前交易的发送者地址。在大多数情况下,这可以通过msg.sender直接获取,但在处理元交易时,这个值可能不是实际的发送者。因此,这个函数提供了一个抽象层,允许子合约重写它以提供正确的发送者地址。

  2. _msgData()函数:这个函数返回当前交易的输入数据。同样,在大多数情况下,这可以通过msg.data直接获取,但在处理元交易时,这个值可能不是实际的输入数据。因此,这个函数提供了一个抽象层,允许子合约重写它以提供正确的输入数据。

  3. _contextSuffixLength()函数:这个函数返回一个长度值,表示上下文后缀的长度。这个函数在处理元交易时可能有用,但在这个合约中默认返回0,表示没有上下文后缀。

用途

Context合约的主要用途是为那些需要访问当前执行上下文信息的合约提供基础。例如,如果一个合约需要知道是谁发送了交易,或者交易中包含了哪些数据,它可以继承Context合约并使用_msgSender()_msgData()函数。

注意事项

  • 继承:任何需要使用Context合约功能的合约都应该继承它。如果子合约需要提供不同的发送者或数据,它应该重写_msgSender()_msgData()函数。
  • 元交易:处理元交易时,发送和支付执行费用的账户可能不是实际的发送者。因此,任何依赖于msg.sendermsg.data的合约都需要能够处理这种情况。
  • 安全性:在重写_msgSender()_msgData()函数时,需要确保提供的信息是安全的,以防止潜在的安全漏洞。

ERC20

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";

abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
...
}

定义了一个名为 ERC20 的抽象合约,它继承了四个接口:Context、IERC20、IERC20Metadata 和 IERC20Errors。

  1. abstract contract ERC20
  • abstract contract 表示这是一个抽象合约,不能直接实例化。它通常用于定义接口和共享逻辑,其他合约可以继承它。
  • ERC20 是合约的名称。
  1. is Context, IERC20, IERC20Metadata, IERC20Errors
  • Context 是一个包含上下文相关函数(如 msg.sender)的接口,通常用于简化合约编写。
  • IERC20 是 ERC20 标准的接口,定义了 ERC20 代币的基本功能,如 totalSupply、balanceOf、transfer 等。
  • IERC20Metadata 是 ERC20 标准的扩展接口,定义了代币的元数据,如 name、symbol 和 decimals。
  • IERC20Errors 是一个自定义的接口,通常用于定义合约中可能出现的错误,如 TransferFailedError、InsufficientBalanceError 等。

合约成员

1
2
3
4
5
6
7
8
mapping(address account => uint256) private _balances;

mapping(address account => mapping(address spender => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;
  1. _balances:

    • 这是一个映射(mapping),用于存储每个账户的代币余额。
    • address account 是账户地址,uint256 是该地址对应的代币数量。
    • private 关键字表示这个映射只能在合约内部访问,外部无法直接访问。
    • 这个映射用于跟踪每个账户的代币余额。
  2. _allowances:

    • 这是一个嵌套的映射,用于存储每个账户授权给其他账户的代币数量。
    • address account 是账户地址,mapping(address spender => uint256) 是一个映射,表示该账户授权给其他账户的代币数量。
    • address spender 是被授权的账户地址,uint256 是被授权的代币数量。
    • private 关键字表示这个映射只能在合约内部访问,外部无法直接访问。
    • 这个映射用于跟踪每个账户授权给其他账户的代币数量,通常用于实现代币的转账授权功能。
      -
  3. _totalSupply:

    • 这是一个无符号整数(uint256),用于存储代币的总供应量。
    • private 关键字表示这个变量只能在合约内部访问,外部无法直接访问。
    • 这个变量用于跟踪代币的总供应量。
      -
  4. _name:

    • 这是一个字符串变量,用于存储代币的名称。
    • private 关键字表示这个变量只能在合约内部访问,外部无法直接访问。
    • 这个变量用于存储代币的名称,例如 “MyToken”。
  5. _symbol:

    • 这是一个字符串变量,用于存储代币的符号。
    • private 关键字表示这个变量只能在合约内部访问,外部无法直接访问。
    • 这个变量用于存储代币的符号,例如 “MTK”。

transfer & transferFrom

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
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}

function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}

function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}

function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}

if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}

emit Transfer(from, to, value);
}
  • 所有的转让动作最终都由_transfer方法来完成。
  • _transfer方法中统一判断地址的有效性,使用revertrequire更节省gas。
  • ERC20InvalidSenderERC20InvalidReceiver都来自IERC20Errors接口。

_update

_update 为内部虚函数,用于更新代币合约中的余额和总供应量。它主要用于处理代币的转移操作,确保在转移过程中不会发生溢出或不足的情况。

  1. 参数说明

    • from:代币的发送者地址。
    • to:代币的接收者地址。
    • value:转移的代币数量。
  2. 更新总供应量

    • 如果 fromaddress(0),表示这是一个铸造操作(minting),那么 _totalSupply 将增加 value
      1
      2
      3
      4
      5
      6
      function _mint(address account, uint256 value) internal {
      if (account == address(0)) {
      revert ERC20InvalidReceiver(address(0));
      }
      _update(address(0), account, value);
      }
    • 如果 from 不是 address(0),表示这是一个转移操作(transfer),那么首先检查 from 的余额是否足够,如果足够,则从 from 的余额中减去 value
  3. 更新接收者的余额

    • 如果 toaddress(0),表示这是一个销毁操作(burning),那么 _totalSupply 将减少 value
      1
      2
      3
      4
      5
      6
      function _burn(address account, uint256 value) internal {
      if (account == address(0)) {
      revert ERC20InvalidSender(address(0));
      }
      _update(account, address(0), value);
      }
    • 如果 to 不是 address(0),表示这是一个转移操作,那么将 value 加到 to 的余额中。
  4. 防止溢出

    • 使用 unchecked 关键字来处理可能的溢出情况,因为代码逻辑已经确保了不会发生溢出。
    • 使用 unchecked 可以节省gas
  5. 触发事件

    • 最后,通过 emit Transfer(from, to, value); 触发一个 Transfer 事件,记录这次转移操作。

approve

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
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}

function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}

function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}

_approve函数

_approve函数用于更新一个账户对另一个账户的授权额度。如果emitEvent参数为true,还会触发一个Approval事件。

- `owner`:授权额度所有者的地址。
- `spender`:被授权的账户地址。
- `value`:授权的额度。
- `emitEvent`:一个布尔值,决定是否触发`Approval`事件。

流程

  1. 首先检查ownerspender是否为address(0),如果是,则抛出异常ERC20InvalidApproverERC20InvalidSpender
  2. 更新_allowances映射,将ownerspender的授权额度设置为value
  3. 如果emitEventtrue,则触发Approval事件。

_spendAllowance函数

_spendAllowance函数用于消耗ownerspender的授权额度。它不会更新授权额度,除非授权额度是有限的。如果授权额度不足,则会抛出异常。

- `owner`:授权额度所有者的地址。
- `spender`:被授权的账户地址。
- `value`:要消费的额度。

流程

  1. 获取ownerspender的当前授权额度。
  2. 如果当前授权额度不是无限(即不等于type(uint256).max),则检查当前授权额度是否足够消费value
  3. 如果足够,则更新ownerspender的授权额度,减去value,并触发Approval事件(如果emitEventtrue)。

decimals

1
2
3
function decimals() public view virtual returns (uint8) {
return 18;
}

默认值是18, 如果想改变,只能重写decimals()这个方法。这种方法直接返回固定值的相比于使用成员变量存储的方式更省gas。

ERC20Pausable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Pausable.sol)

pragma solidity ^0.8.20;

import {ERC20} from "../ERC20.sol";
import {Pausable} from "../../../utils/Pausable.sol";

abstract contract ERC20Pausable is ERC20, Pausable {
/**
* @dev See {ERC20-_update}.
*
* Requirements:
*
* - the contract must not be paused.
*/
function _update(address from, address to, uint256 value) internal virtual override whenNotPaused {
super._update(from, to, value);
}
}

ERC20Pausable 是个抽象合约,它继承了 ERC20Pausable 两个合约。ERC20 是一个标准的代币合约接口,而 Pausable 是一个用于暂停合约操作的合约。

实现原理

  1. 继承关系ERC20Pausable 继承了 ERC20Pausable 合约,这意味着它继承了 ERC20 的所有功能(如代币的创建、转移、余额查询等)以及 Pausable 的功能(如暂停和恢复合约操作)。

  2. 抽象合约ERC20Pausable 是一个抽象合约,因为它包含了一个未实现的函数 _update

  3. 函数重写_update 函数重写了 ERC20 合约中的 _update 函数。这个函数在执行代币转移操作之前,会检查合约是否处于暂停状态。如果合约处于暂停状态,则不允许执行转移操作。

  4. whenNotPaused 修饰器whenNotPausedPausable 合约中的一个修饰器,用于确保在调用被修饰的函数时,合约不是暂停状态。如果合约是暂停状态,调用将被 revert(回滚)。

用途

ERC20Pausable 合约的主要用途是提供一个可暂停的代币合约,允许合约所有者或管理员在紧急情况下暂停代币的转移操作,以防止潜在的滥用或攻击。

注意事项

  1. 实现 _update 函数:任何继承 ERC20Pausable 的合约都必须实现 _update 函数,以确保在执行代币转移操作时能够正确地检查合约是否处于暂停状态。

  2. 合约暂停:合约暂停后,所有与代币转移相关的操作都将被阻止,直到合约被恢复。因此,合约所有者或管理员在暂停合约时应谨慎操作,以避免造成不必要的损失。

  3. 安全性:虽然 ERC20Pausable 合约提供了一种暂停合约的方法,但它并不能防止所有潜在的安全问题。开发者仍需确保合约的其他部分(如访问控制、逻辑错误等)是安全的。

AccessManaged

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
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/manager/AccessManaged.sol)

pragma solidity ^0.8.20;

import {IAuthority} from "./IAuthority.sol";
import {AuthorityUtils} from "./AuthorityUtils.sol";
import {IAccessManager} from "./IAccessManager.sol";
import {IAccessManaged} from "./IAccessManaged.sol";
import {Context} from "../../utils/Context.sol";

abstract contract AccessManaged is Context, IAccessManaged {
address private _authority;

bool private _consumingSchedule;
constructor(address initialAuthority) {
_setAuthority(initialAuthority);
}

modifier restricted() {
_checkCanCall(_msgSender(), _msgData());
_;
}

/// @inheritdoc IAccessManaged
function authority() public view virtual returns (address) {
return _authority;
}

/// @inheritdoc IAccessManaged
function setAuthority(address newAuthority) public virtual {
address caller = _msgSender();
if (caller != authority()) {
revert AccessManagedUnauthorized(caller);
}
if (newAuthority.code.length == 0) {
revert AccessManagedInvalidAuthority(newAuthority);
}
_setAuthority(newAuthority);
}

/// @inheritdoc IAccessManaged
function isConsumingScheduledOp() public view returns (bytes4) {
return _consumingSchedule ? this.isConsumingScheduledOp.selector : bytes4(0);
}

function _setAuthority(address newAuthority) internal virtual {
_authority = newAuthority;
emit AuthorityUpdated(newAuthority);
}

function _checkCanCall(address caller, bytes calldata data) internal virtual {
(bool immediate, uint32 delay) = AuthorityUtils.canCallWithDelay(
authority(),
caller,
address(this),
bytes4(data[0:4])
);
if (!immediate) {
if (delay > 0) {
_consumingSchedule = true;
IAccessManager(authority()).consumeScheduledOp(caller, data);
_consumingSchedule = false;
} else {
revert AccessManagedUnauthorized(caller);
}
}
}
}

AccessManaged 是个抽象合约,它继承了 ContextIAccessManaged 接口。这个合约的主要目的是管理合约的访问权限,确保只有授权的地址才能调用特定的函数。下面是对代码的详细解释:

合约结构

  1. 状态变量

    • _authority:存储当前合约的授权地址。
    • _consumingSchedule:一个布尔值,用于指示是否正在执行预定的操作。
  2. 构造函数

    • constructor(address initialAuthority):初始化合约,设置初始的授权地址。
  3. 修饰器

    • modifier restricted():限制函数的访问,确保只有授权的地址才能调用。这个修饰器通过调用 _checkCanCall 函数来检查调用者是否有权限。
  4. 接口实现

    • function authority() public view virtual returns (address):返回当前的授权地址。
    • function setAuthority(address newAuthority) public virtual:设置新的授权地址。只有当前的授权地址才能调用这个函数。
    • function isConsumingScheduledOp() public view returns (bytes4):检查合约是否正在执行预定的操作。
  5. 内部函数

    • function _setAuthority(address newAuthority) internal virtual:设置新的授权地址。这个函数没有访问限制,允许直接设置新的授权地址。
    • function _checkCanCall(address caller, bytes calldata data) internal virtual:检查调用者是否有权限调用当前函数。如果调用者没有权限,则抛出异常。

实现原理

  • 权限管理:通过 _authority 状态变量和 restricted 修饰器实现。只有 _authority 指定的地址才能调用被 restricted 修饰的函数。
  • 授权地址的设置:通过 setAuthority 函数设置新的授权地址,只有当前的授权地址才能调用这个函数。
  • 预定操作:通过 _consumingScheduleisConsumingScheduledOp 函数管理预定的操作。如果合约正在执行预定的操作,isConsumingScheduledOp 函数会返回一个特定的函数选择器。

用途

这个合约的主要用途是提供一个框架,用于管理合约的访问权限。通过设置授权地址,可以确保只有授权的地址才能调用合约的特定函数。这对于需要严格控制访问权限的合约非常有用,例如智能合约钱包或权限管理合约。

注意事项

  • 安全性:在实现权限管理时,需要特别注意不要在 receive()fallback() 函数上使用 restricted 修饰器,因为这些函数的调用方式可能导致权限检查失败。
  • 权限转移:通过 setAuthority 函数可以转移合约的权限,但只有当前的授权地址才能执行这个操作,这确保了权限的转移是可控的。
  • 预定操作:通过 isConsumingScheduledOp 函数可以检查合约是否正在执行预定的操作,这对于需要同步执行多个操作的合约非常有用。

ERC20Permit

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
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Permit.sol)

pragma solidity ^0.8.20;

import {IERC20Permit} from "./IERC20Permit.sol";
import {ERC20} from "../ERC20.sol";
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
import {Nonces} from "../../../utils/Nonces.sol";

/**
@dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

/**
* @dev Permit deadline has expired.
*/
error ERC2612ExpiredSignature(uint256 deadline);

/**
* @dev Mismatched signature.
*/
error ERC2612InvalidSigner(address signer, address owner);

/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
*
* It's a good idea to use the same `name` that is defined as the ERC-20 token name.
*/
constructor(string memory name) EIP712(name, "1") {}

/**
* @inheritdoc IERC20Permit
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}

bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

bytes32 hash = _hashTypedDataV4(structHash);

address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) {
revert ERC2612InvalidSigner(signer, owner);
}

_approve(owner, spender, value);
}

/**
* @inheritdoc IERC20Permit
*/
function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}

/**
* @inheritdoc IERC20Permit
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
return _domainSeparatorV4();
}
}

ERC20Permit 是一个抽象合约,它结合了 ERC-20 标准和 ERC-2612 标准的功能。ERC-2612 标准允许使用签名来批准代币转移,而无需直接调用 approve 函数。下面是对代码的详细解释:

实现原理

  1. 继承关系

    • ERC20Permit 继承了 ERC20IERC20PermitEIP712Nonces 四个合约。
    • ERC20 实现了 ERC-20 标准的代币功能。
    • IERC20Permit 定义了 ERC-2612 标准的接口。
    • EIP712 用于生成和验证 EIP-712 签名。
    • Nonces 用于管理每个账户的签名计数器。
  2. 常量

    • PERMIT_TYPEHASH 是一个哈希值,用于在 EIP-712 签名中标识 Permit 结构。
  3. 错误

    • ERC2612ExpiredSignature:当签名过期时抛出。
    • ERC2612InvalidSigner:当签名者与账户不匹配时抛出。
  4. 构造函数

    • 初始化 EIP712 的域分隔符,使用传入的 name 参数,并设置版本号为 "1"
  5. permit 函数

    • 验证签名是否过期。
    • 生成 Permit 结构的哈希值。
    • 使用 EIP-712 签名验证机制恢复签名者地址。
    • 检查签名者是否与 owner 匹配。
    • 调用 _approve 函数批准代币转移。
  6. nonces 函数

    • 返回指定账户的签名计数器值。
  7. DOMAIN_SEPARATOR 函数

    • 返回 EIP-712 的域分隔符。

用途

ERC20Permit 合约允许用户通过签名来批准代币转移,而无需直接调用 approve 函数。这对于提高安全性(避免重放攻击)和用户体验(无需每次都手动批准)非常有用。

注意事项

  1. 安全性:确保正确实现 EIP-712 签名验证,以防止签名被滥用。
  2. 时间戳:使用 block.timestamp 来检查签名是否过期,确保安全性。
  3. nonce:使用 nonce 来防止重放攻击,确保每个签名只被使用一次。
  4. 兼容性:确保合约与所有相关接口和标准兼容,以避免潜在的问题。

ERC-2612标准

ERC-2612 标准提供了一种安全、高效的方式来授权代币转移,是 ERC-20 标准的一个扩展。它允许代币持有者通过签名授权第三方转移代币,而不需要直接调用 approve 函数。这个标准基于 EIP-712,它提供了一种使用签名来验证和执行操作的方法。

ERC-2612 标准的主要功能

  1. Permit 函数:允许代币持有者通过签名授权第三方转移代币。这个函数需要三个参数:owner(代币持有者的地址)、spender(被授权的地址)和 value(被授权的代币数量)。

  2. EIP-712 签名:使用 EIP-712 签名来验证 Permit 函数的调用。EIP-712 是一种用于生成和验证签名的标准,它允许在签名中包含额外的元数据,如链 ID 和域分隔符。

  3. Nonce:每个账户都有一个关联的 nonce 值,用于防止重放攻击。在 Permit 函数中,nonce 值会自动增加,确保每个签名只被使用一次。

ERC-2612 标准的实现

要实现 ERC-2612 标准,代币合约需要包含以下部分:

  1. Permit 函数:实现 Permit 函数,用于处理签名授权。

  2. EIP-712 签名:实现 EIP-712 签名生成和验证机制。

  3. Nonce:管理每个账户的 nonce 值。

  4. DOMAIN_SEPARATOR:生成 EIP-712 签名的域分隔符。

ERC-2612 标准的用途

ERC-2612 标准的主要用途是提高代币的安全性。通过使用签名来授权代币转移,可以防止重放攻击,并简化用户界面。此外,它还可以提高交易效率,因为用户不需要每次都手动批准代币转移。

什么是 ERC-20?

ERC-20 提出了一个同质化代币的标准,换句话说,它们具有一种属性,使得每个代币都与另一个代币(在类型和价值上)完全相同。 例如,一个 ERC-20 代币就像以太币一样,意味着一个代币会并永远会与其他代币一样。(From: 《ERC-20 代币标准》)

方法(Method)

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
function name() public view returns (string)
// 返回令牌的名称 - 例如 "MyToken"
// 可选 - 此方法可用于提高可用性,但接口和其他协定不得期望存在这些值。

function symbol() public view returns (string)
// 返回令牌的 symbol。例如“HIX”。
// 可选 - 此方法可用于提高可用性,但接口和其他协定不得期望存在这些值。

function decimals() public view returns (uint8)
// 返回代币使用的小数位数 - 例如 8,表示将代币数量除以 100000000 以获得其用户表示。
// 可选 - 此方法可用于提高可用性,但接口和其他协定不得期望存在这些值。

function totalSupply() public view returns (uint256)
// 返回总代币供应量。

function balanceOf(address _owner) public view returns (uint256 balance)
// 返回地址为 _owner 的另一个账户的账户余额。

function transfer(address _to, uint256 _value) public returns (bool success)
// 将 _value 数量的代币转移到 _to,并且必须触发 Transfer 事件。如果消息调用者的账户余额没有足够的代币可供花费,则函数应该throw。
// 注意值为 0 的传输必须被视为正常传输,并触发 Transfer 事件。

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
// 将 _value数量的代币从地址 _from 转移到地址 _to,并且必须触发 Transfer 事件。
// transferFrom 方法用于提现工作流程,允许合约代表您转移代币。例如,这可用于允许合约代表您转移代币和/或以子货币收取费用。除非 _from 账户通过某种机制故意授权了消息的发送者,否则该函数应该throw。

function approve(address _spender, uint256 _value) public returns (bool success)
// 允许_spender多次从您的账户提款,最高可达_value金额。如果再次调用此函数,它将用 _value 覆盖当前限额。
// 注意:客户端应该确保在创建用户界面时,先将限额设置为 0,然后再为同一花费者将其设置为另一个值。尽管 Contract 本身不应该强制执行它,以允许向后兼容之前部署的 Contract

function allowance(address _owner, address _spender) public view returns (uint256 remaining)
// 返回 _spender 仍允许从 _owner 中提取的金额。

事件(Event)

1
2
3
4
5
6
event Transfer(address indexed _from, address indexed _to, uint256 _value)
// 必须在代币转移时触发,包括零价值转移。
// 创建新代币的代币合约应该在创建代币时触发 Transfer 事件,并将 _from 地址设置为 0x0。

event Approval(address indexed _owner, address indexed _spender, uint256 _value)
// 必须在成功调用 approve(address _spender, uint256 _value) 时触发。

实现

简单示例

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;
}
}

注意事项

  • 由于本合约的编译版本为0.8.0,此版本具备整数溢出检查,所以可以不使用safeMath。

快速示例

使用OpenZeppelin快速创建ERC-20合约

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
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract BobToken is ERC20, ERC20Pausable, AccessManaged, ERC20Permit {
constructor(address initialAuthority)
ERC20("BobToken", "BBK")
AccessManaged(initialAuthority)
ERC20Permit("BobToken")
{}

function pause() public restricted {
_pause();
}

function unpause() public restricted {
_unpause();
}

function mint(address to, uint256 amount) public restricted {
_mint(to, amount);
}

// The following functions are overrides required by Solidity.

function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}
}

我们的合约通常通过继承来使用,在这里我们将 ERC20 重新用于基本标准实现以及 namesymboldecimals 可选扩展。此外,我们正在创建一个代币的 initialSupply,该代币将被分配给部署合约的地址。

在 Solidity 中,callstaticcalldelegatecall是强大的低级操作,用于与其他合约交互或在当前上下文中执行外部合约的逻辑。掌握它们的用法和区别对于智能合约开发非常重要。

本文将详细介绍它们的功能、适用场景、代码示例,以及使用时需要注意的潜在问题。

call

功能

call 是一种通用方法,用于调用另一个合约的函数或发送 ETH。它可以调用目标合约的任意函数,包括不存在的函数(在这种情况下不会抛出错误)。

特点

  • 可读写目标合约的状态。
  • 支持附带 ETH 发送。
  • 返回两个值:
    • 调用是否成功 (bool)。
    • 调用返回的数据 (bytes memory)。

使用场景

  • 调用外部合约的任意函数。
  • 向合约或外部账户发送 ETH。

代码示例

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

contract CallExample {
function callFunction(address target, uint256 value) external returns (bool, bytes memory) {
// 通过 call 调用目标合约的 setValue(uint)
(bool success, bytes memory data) = target.call(
abi.encodeWithSignature("setValue(uint256)", value)
);
require(success, "Call failed");
return (success, data);
}

function sendEth(address payable target) external payable {
// 使用 call 发送 ETH
(bool success, ) = target.call{value: msg.value}("");
require(success, "Send ETH failed");
}
}

staticcall

功能

staticcall是一种只读调用方法,用于调用目标合约的视图或纯函数。它不允许改变状态,因此更安全且节省 Gas

特点

  • 只能调用 view 或 pure 修饰的函数。
  • 无法修改状态或发送 ETH。
  • 返回两个值:
    • 调用是否成功 (bool)。
    • 调用返回的数据 (bytes memory)。

使用场景

  • 查询目标合约的状态。
  • 调用只读逻辑以避免状态改变。

代码示例

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

contract StaticCallExample {
function staticCallFunction(address target) external view returns (uint256) {
// 使用 staticcall 调用目标合约的 storedValue()
(bool success, bytes memory data) = target.staticcall(
abi.encodeWithSignature("storedValue()")
);
require(success, "Staticcall failed");
return abi.decode(data, (uint256));
}
}

delegatecall

功能

delegatecall 是一种在当前合约上下文中执行目标合约逻辑的方法。它会以调用合约的存储布局为准,执行目标合约中的代码。

特点

  • 使用调用合约的存储。
  • 使用调用合约的 msg.sender 和 msg.value。
  • 返回两个值:
    • 调用是否成功 (bool)。
    • 调用返回的数据 (bytes memory)。

使用场景

  • 实现合约代理(如升级逻辑)。
  • 在共享存储布局的上下文中执行外部逻辑。

代码示例

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

contract DelegateCallExample {
uint256 public storedValue;

function delegateCallFunction(address target, uint256 value) external {
// 使用 delegatecall 调用目标合约的 setValue(uint)
(bool success, ) = target.delegatecall(
abi.encodeWithSignature("setValue(uint256)", value)
);
require(success, "Delegatecall failed");
}
}

// 被调用合约 (Library)
contract Target {
uint256 public storedValue;

function setValue(uint256 value) external {
storedValue = value
}
}

call VS staticcall VS delegatecall

特性 call staticcall delegatecall
可读写状态 ❌(只读)
使用调用合约存储
使用调用合约msg.sender
支持发送ETH
调用不存在的函数 支持,返回失败 支持,返回失败 支持,返回失败

使用注意事项

call 的安全性

  • 使用 call 调用外部合约时,目标合约可能会执行恶意代码。需要额外检查返回值并限制权限。
  • 不要轻易使用 call 调用不可信的合约。

delegatecall 的存储风险

  • 调用目标合约时,目标合约必须与调用合约共享相同的存储布局,否则可能导致存储冲突。
  • 使用 delegatecall 需要确保调用的是受信任的逻辑。

Gas 消耗与返回值处理

  • 注意低级调用的 Gas 使用,避免由于 Gas 不足导致调用失败。
  • 低级调用(call、staticcall 和 delegatecall)不会自动抛出异常,需要手动处理返回值。

不要使用简写类型

无论是使用abi.encodeWithSelect 还是 abi.encodeWithSignature ,参数中方法名后的参数一定不要使用简写类型。

1
2
// 错误写法
(bool success, ) = target.call(abi.encodeWithSignature("setValue(uint)", value));

因为在编译的过程中,编译器会将简写类型(如:uint)转换成uint256,这将导致,你的签名或选择器不匹配,导致执行失败。

实践案例:实现代理合约

以下是使用 delegatecall 的代理合约的示例,用于实现合约逻辑的动态升级

代码示例

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 LogicContract {
uint256 public storedValue;

function setValue(uint256 value) external {
storedValue = value;
}
}

// 代理合约
contract ProxyContract {
address public logicContract;

constructor(address _logicContract) {
logicContract = _logicContract;
}

fallback() external payable {
(bool success, ) = logicContract.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
}

运行过程

  • 部署 LogicContract。
  • 部署 ProxyContract,将 LogicContract 的地址传递给其构造函数。
  • 通过代理合约调用 setValue 方法,storedValue 实际存储在 ProxyContract 中

结论

callstaticcalldelegatecall 是 Solidity 中的基础工具,可以用来实现灵活的合约交互和逻辑扩展。在使用这些低级调用时,需要仔细处理返回值、安全性以及存储一致性问题。

通过合理使用这些工具,您可以设计功能强大且可扩展的智能合约体系,同时避免潜在的安全漏洞。

环境

1
2
3
4
5
6
7
8
# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.4 LTS
Release: 22.04
Codename: jammy
# uname -a
Linux ubu22 5.15.0-100-generic #110-Ubuntu SMP Wed Feb 7 13:28:04 UTC 2024 aarch64 aarch64 aarch64 GNU/Linux

包安装

安装软件包

1
apt install -y bison flex build-essential git cmake make libelf-dev strace tar libfl-dev libssl-dev libedit-dev zlib1g-dev  python  python3-distutils libcap-dev llvm clang

安装 bcc

bcc 是一组用于 eBPF 开发的工具,它包括了一组用于编写和调试 eBPF 程序的库和命令行工具。使用 bcc,可以更加方便地开发和调试 eBPF 程序,提高开发效率和代码质量。

1
apt install bpfcc-tools

安装 bpftool

bpftool 是一个用于管理和调试 eBPF 代码的命令行工具。它允许你查看和分析系统中运行的 eBPF 程序和映射,以及与内核中的 eBPF 子系统进行交互。更多内容可以查看 bpftool Github

使用 bpftool,你可以执行以下操作:

  • 列出当前系统中所有加载的 eBPF 程序和映射
  • 查看指定 eBPF 程序或映射的详细信息,例如指令集、内存布局等
  • 修改 eBPF 程序或映射的属性,例如禁用一个程序或清空一个映射
  • 将一个 eBPF 程序或映射导出到文件中,以便在其他系统上重新导入
  • 调试 eBPF 程序,例如跟踪程序的控制流、访问内存等
1
apt install -y linux-tools-$(uname -r)

默认情况下 bpftool 命令会安装到/usr/local/sbin/ 下

1
2
3
4
5
6
7
8
# bpftool version -p
{
"version": "5.15.143",
"features": {
"libbfd": false,
"skeletons": false
}
}

安装linux源码

1
apt install linux-source-5.15.0

编译bpf sample

解压缩

1
2
cd /usr/src/linux-source-5.15.0/debian
tar -jxvf linux-source-5.15.0.tar.bz2

Copy config文件

1
2
3
cd linux-source-5.15.0
cp -v /boot/config-$(uname -r) .config
make oldconfig && make prepare

编译Samples

1
make -C samples/bpf

若遇到如下错误

1
2
3
4
5
6
7
8
......
CC /usr/src/linux-source-5.15.0/samples/bpf/bpftool/prog.o
LINK /usr/src/linux-source-5.15.0/samples/bpf/bpftool/bpftool
/usr/src/linux-source-5.15.0/samples/bpf/Makefile:369: *** Cannot find a vmlinux for VMLINUX_BTF at any of " /usr/src/linux-source-5.15.0/vmlinux", build the kernel or set VMLINUX_BTF or VMLINUX_H variable. Stop.
make[1]: *** [Makefile:1911: /usr/src/linux-source-5.15.0/samples/bpf] Error 2
make[1]: Leaving directory '/usr/src/linux-source-5.15.0'
make: *** [Makefile:275: all] Error 2
make: Leaving directory '/usr/src/linux-source-5.15.0/samples/bpf'

说明需要 vmlinux,可以指定vmlinux再进行编译

1
make VMLINUX_BTF=/sys/kernel/btf/vmlinux -C samples/bpf

or

1
make VMLINUX_BTF=/sys/kernel/btf/vmlinux M=samples/bpf

示例

示例需要在 usr/src/linux-source-5.15.0/samples/bpf目录下进行编译。samples/bpf 下的程序一般组成方式是 xxx_user.c 和 xxx_kern.c

  • xxx_user.c:为用户空间的程序用于设置 BPF 程序的相关配置、加载 BPF 程序至内核、设置 BPF 程序中的 map 值和读取 BPF 程序运行过程中发送至用户空间的消息等。
  • xxx_kern.c:为 BPF 程序代码,通过 clang 编译成字节码加载至内核中,在对应事件触发的时候运行,可以接受用户空间程序发送的各种数据,并将运行时产生的数据发送至用户空间程序。

新建两个文件:bob_user.cbob_kern.c

bob_kern.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <uapi/linux/bpf.h>
#include <linux/version.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(struct pt_regs *ctx)
{
char fmt[] = "bob %s !\n";
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk(fmt, sizeof(fmt), comm);

return 0;
}

char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;

bob_user.c

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
#include <stdio.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include "trace_helpers.h"

int main(int ac, char **argv)
{
struct bpf_link *link = NULL;
struct bpf_program *prog;
struct bpf_object *obj;
char filename[256];

snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
obj = bpf_object__open_file(filename, NULL);
if (libbpf_get_error(obj)) {
fprintf(stderr, "ERROR: opening BPF object file failed\n");
return 0;
}

prog = bpf_object__find_program_by_name(obj, "bpf_prog");
if (!prog) {
fprintf(stderr, "ERROR: finding a prog in obj file failed\n");
goto cleanup;
}

/* load BPF program */
if (bpf_object__load(obj)) {
fprintf(stderr, "ERROR: loading BPF object file failed\n");
goto cleanup;
}

link = bpf_program__attach(prog);
if (libbpf_get_error(link)) {
fprintf(stderr, "ERROR: bpf_program__attach failed\n");
link = NULL;
goto cleanup;
}

read_trace_pipe();

cleanup:
bpf_link__destroy(link);
bpf_object__close(obj);
return 0;
}

修改 samples/bpf/Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
tprogs-y += xdp_sample_pkts
tprogs-y += ibumad
tprogs-y += hbm
# 增加 user programs
tprogs-y += bob
...
ibumad-objs := ibumad_user.o
hbm-objs := hbm.o $(CGROUP_HELPERS)
# 增加 objs
bob-objs := bob_user.o $(TRACE_HELPERS)
...
always-y += hbm_edt_kern.o
always-y += xdpsock_kern.o
# 增加 kernel programs
always-y += bob_kern.o

编译

1
make VMLINUX_BTF=/sys/kernel/btf/vmlinux M=samples/bpf

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cd samples/bpf
# ./bob
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
<...>-3622529 [001] d...1 1751426.283602: bpf_trace_printk: bob sshd !


sshd-3622530 [001] d...1 1751426.288591: bpf_trace_printk: bob sshd !


sshd-3622531 [001] d...1 1751430.924951: bpf_trace_printk: bob sshd !


<...>-3622532 [000] d...1 1751430.929273: bpf_trace_printk: bob sshd !


bash-3622532 [000] d...1 1751430.930114: bpf_trace_printk: bob bash !


<...>-3622533 [001] d...1 1751430.934470: bpf_trace_printk: bob sshd !

参考&鸣谢

简介

DRBD(Distributed Replicated Block Device,分布式复制块设备)是一个用软件实现的、无共享的、服务器之间镜像块设备内容的存储复制解决方案。DRBD是镜像块设备,是按数据位镜像成一样的数据块。

安装

ubuntu环境

准备两个ubuntu系统,一个作为源端ubu1,一个作为目的端ubu2。

1
2
3
4
5
6
7
8
bob@ubu1:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.5 LTS
Release: 20.04
Codename: focal
bob@ubu1:~$ uname -a
Linux ubu1 5.4.0-139-generic #156-Ubuntu SMP Sat Jan 21 13:46:46 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux
1
2
3
4
5
6
7
8
bob@ubu2:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.5 LTS
Release: 20.04
Codename: focal
bob@ubu2:~$ uname -a
Linux ubu2 5.4.0-139-generic #156-Ubuntu SMP Sat Jan 21 13:46:46 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux

brbd.ko为linux内核自带,不需要另行安装

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
bob@ubu1:~$ lsmod | grep drbd
drbd 434176 3
lru_cache 20480 1 drbd
libcrc32c 16384 4 btrfs,xfs,drbd,raid456
bob@ubu1:~$ modinfo drbd
filename: /lib/modules/5.4.0-139-generic/kernel/drivers/block/drbd/drbd.ko
alias: block-major-147-*
license: GPL
version: 8.4.11
description: drbd - Distributed Replicated Block Device v8.4.11
author: Philipp Reisner <phil@linbit.com>, Lars Ellenberg <lars@linbit.com>
srcversion: B438804C5AE8C84C95D0411
depends: lru_cache,libcrc32c
intree: Y
name: drbd
vermagic: 5.4.0-139-generic SMP mod_unload modversions aarch64
sig_id: PKCS#7
signer: Build time autogenerated kernel key
sig_key: 43:64:51:B8:71:C1:53:C3:AE:29:64:EC:5F:B2:A3:30:54:25:F9:B5
sig_hashalgo: sha512
signature: 0F:16:6D:12:54:0F:95:51:A5:18:4A:0A:A9:7C:72:F6:2D:31:21:39:
44:D9:71:C8:1B:7C:3F:1B:4B:66:53:08:1A:63:92:C3:CE:19:19:F0:
13:9F:9D:D6:7B:0B:27:F8:54:19:D3:44:F9:CC:8F:6E:E1:82:07:7A:
30:B1:37:28:5E:60:92:5B:C9:26:B0:25:DD:50:9D:A5:2E:76:96:A8:
E2:58:A1:36:DC:AA:84:D2:40:EF:66:F1:82:E5:2B:51:D7:F8:49:62:
9A:62:22:DC:42:A9:01:EC:6B:DC:9F:C6:8E:DD:9D:4A:CD:93:9B:F9:
3B:E4:4A:B4:34:BD:12:1A:89:C0:2C:2B:96:4D:86:27:D5:93:3A:51:
D4:DD:C7:34:9D:2D:A0:6C:BC:57:D1:9F:54:C2:55:56:B2:60:4D:FB:
86:55:18:09:9C:A5:C1:80:54:F5:F5:35:97:7E:32:36:A9:9B:F9:B2:
69:5B:BD:83:A3:41:89:9B:4F:7D:BA:A9:F1:E2:28:6A:FD:D4:7A:94:
67:34:D1:3C:8D:39:18:F7:43:A7:5F:67:71:B6:3B:63:3C:10:16:B7:
44:EA:D2:D4:52:19:28:F3:60:31:35:69:6B:EA:25:3B:35:FC:32:05:
B9:5A:99:7E:61:27:59:52:A8:55:8C:B6:15:8E:77:26:0F:DC:61:93:
F9:10:EA:8F:FD:E1:E0:DF:60:2D:83:92:9E:33:D7:99:10:93:AF:03:
44:8B:A0:6A:E8:E7:08:71:77:1C:CC:35:96:8C:94:DB:62:CA:ED:64:
B8:17:43:24:FD:BF:FC:3B:D7:D7:03:1C:83:06:0E:B5:D3:82:2A:4B:
E4:A7:F9:C4:07:9C:0F:2B:3E:D4:EA:48:FF:6E:BD:79:2E:85:FB:EE:
2D:A2:70:96:38:77:9C:29:58:5C:45:B1:8A:29:D2:7A:87:55:A6:A3:
32:A2:FC:F5:A2:4F:9C:20:85:F0:55:E6:A6:01:D2:76:EE:27:2F:A0:
BC:67:F2:B9:9B:A4:5D:2D:3D:6F:E9:46:C0:EF:4A:0C:01:15:19:E7:
9C:61:A9:00:E8:1A:D3:7D:2E:86:81:FD:BA:53:44:85:17:19:D9:B3:
B8:E2:7B:39:01:28:3E:F3:DF:87:3C:C5:E4:37:89:C1:67:16:B0:2C:
C7:4D:C8:6F:71:97:55:19:24:63:7B:D6:08:A2:EC:EB:C3:AA:71:91:
35:B4:C6:47:B5:EA:2B:DF:E4:AC:9C:06:77:73:1B:33:F5:AF:EE:58:
AC:F2:C4:A0:C9:00:58:AB:3F:62:1B:E5:E5:C3:E1:39:98:5B:2A:C9:
BD:10:6B:AF:FC:44:B0:7D:57:07:B0:5B
parm: allow_oos:DONT USE! (bool)
parm: disable_sendpage:bool
parm: proc_details:int
parm: minor_count:Approximate number of drbd devices (1-255) (uint)
parm: usermode_helper:string

至于使用drbd的工具还是要安装的

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
bob@ubu1:~$ sudo apt-cache show drbd-utils
Package: drbd-utils
Architecture: arm64
Version: 9.11.0-1build1
Priority: extra
Section: admin
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Debian DRBD Maintainers <debian-ha-maintainers@lists.alioth.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 2062
Depends: lsb-base (>= 3.0-6), libc6 (>= 2.28), libgcc-s1 (>= 3.0), libstdc++6 (>= 5.2), init-system-helpers (>= 1.51)
Recommends: heirloom-mailx | mailx
Suggests: heartbeat
Breaks: drbd8-utils (<< 2:8.9.0)
Replaces: drbd8-utils (<< 2:8.9.0)
Filename: pool/main/d/drbd-utils/drbd-utils_9.11.0-1build1_arm64.deb
Size: 654128
MD5sum: 45a8962e941c9a87c7de423de34f616b
SHA1: 3734ee6c0611348c5b8c080feb713c05f9d84c4c
SHA256: ae63aa90d145faab00551f151c10588bafebe2673e64b137c980cce7c299bccf
Homepage: https://www.drbd.org/
Description-en: RAID 1 over TCP/IP for Linux (user utilities)
Drbd is a block device which is designed to build high availability
clusters by providing a virtual shared device which keeps disks in
nodes synchronised using TCP/IP. This simulates RAID 1 but avoiding
the use of uncommon hardware (shared SCSI buses or Fibre Channel).
It is currently limited to fail-over HA clusters.
.
This package contains the programs that will control the drbd kernel
module provided in the Linux kernel.
Description-md5: 7da3dade742b03d1a9c08b339123f93b

bob@ubu1:~$ apt install drbd-utils

两个节点ubu1和ubu2都需要安装这个drbd-utils

常规用法

配置

配置/etc/drbd.d/global_common.conf

1
2
3
4
5
6
7
8
9
global {
usage-count yes;
}

common {
net {
protocol C;
}
}

DRBD系统向虚拟块的镜像中写入数据时,支持三种协议

  • protocol A 数据一旦写入磁盘并发送到网络中就认为完成了写入操作
  • protocol B 收到接收确认就认为完成了写入操作
  • protocol C 收到写入确认就认为完成了写入操作

基于安全考虑我们一般选择protocol C

配置DRBD资源/etc/drbd.d/r0.res

1
2
3
4
5
6
7
8
9
10
11
12
13
14
resource r0 {
on ubu1 {
device /dev/drbd1;
disk /dev/sda;
address 192.168.64.2:7789;
meta-disk internal;
}
on ubu2 {
device /dev/drbd1;
disk /dev/sda;
address 192.168.64.4:7789;
meta-disk internal;
}
}

device指drbd映射出来的快设备名称,disk指用于存放数据的硬盘。

启动

分别在ubu1ubu2上创建DRBD资源元数据

1
bob@ubu1:~$ sudo drbdadm create-md r0
1
bob@ubu2:~$ sudo drbdadm create-md r0

然后再启动ubu1ubu2上的DRBD服务

1
bob@ubu1:~$ sudo systemctl start drbd
1
bob@ubu2:~$ sudo systemctl start drbd

启动r0资源,并设置ubu1为主设备

1
2
bob@ubu1:~$ sudo drbdadm up r0
bob@ubu1:~$ sudo drbdadm primary --force r0

可以使用drbdadm status r0查看状态,也可以用cat /proc/drbd查看同步进度。
peer-disk状态为UpToDate表示数据已经同步到最新状态。

eg:

1
2
3
4
5
bob@ubu1:~$ sudo drbdadm status r0
r0 role:Primary
disk:UpToDate
peer role:Secondary
replication:Established peer-disk:UpToDate

如需查询服务状态详细信息可以通过命令drbdsetup status r0 --verbose --statistics查询

文件系统常规使用

首先通过lsblk查看drbd1快设备是否存在,然后格式化成xfs文件系统,并挂载到/mnt目录上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bob@ubu1:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop1 7:1 0 49.1M 1 loop /snap/core18/2681
loop2 7:2 0 59.1M 1 loop /snap/core20/1826
loop3 7:3 0 60.7M 1 loop /snap/lxd/21843
loop4 7:4 0 43.2M 1 loop /snap/snapd/17954
loop5 7:5 0 91.9M 1 loop /snap/lxd/24065
loop6 7:6 0 57.5M 1 loop /snap/core20/1332
loop7 7:7 0 43.2M 1 loop /snap/snapd/18363
loop8 7:8 0 49.1M 1 loop /snap/core18/2701
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /
bob@ubu1:~$ mkfs.xfs /dev/drbd1
...
bob@ubu1:~$ mount /dev/drbd1 /mnt
bob@ubu1:~$ touch /mnt/hello

切换

切换操作,可以认为是切换primary节点操作。需要先将ubu1(primary节点)设置成secondary,再将ubu2(secondary节点)设置成primary,然后ubu2节点就可以挂载drbd1快设备了。

1
bob@ubu1:~$ sudo drbdadm secondary r0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bob@ubu2:~$ sudo drbdadm primary r0
bob@ubu2:~$ sudo mount /dev/drbd1 /mnt/
bob@ubu2:~$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 1.9G 0 1.9G 0% /dev
tmpfs 392M 1.4M 391M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 49G 7.3G 39G 16% /
tmpfs 2.0G 0 2.0G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup
/dev/vda2 974M 206M 701M 23% /boot
/dev/vda1 511M 5.3M 506M 2% /boot/efi
/dev/loop3 60M 60M 0 100% /snap/core20/1826
/dev/loop1 50M 50M 0 100% /snap/core18/2681
/dev/loop2 58M 58M 0 100% /snap/core20/1332
/dev/loop5 92M 92M 0 100% /snap/lxd/24065
/dev/loop4 61M 61M 0 100% /snap/lxd/21843
/dev/loop6 44M 44M 0 100% /snap/snapd/17954
tmpfs 392M 0 392M 0% /run/user/1000
/dev/drbd1 20G 175M 20G 1% /mnt

在将ubu1节点从primary状态设置成secondary状态时可能会遇到如下错误

1
2
3
bob@ubu1:~$ sudo drbdadm secondary r0
1: State change failed: (-12) Device is held open by someone
env: python: No such file or directory

这说明当前块设备brbd1正在被挂载或者被其他应用使用,可以使用lsof命令查看改设备的使用情况,并终止使用后再设置节点状态为secondary

硬盘容量大小不一致情况

当源和备硬盘容量大小不一致时,取最小容量做为drbd块设备对外提供的存储空间大小。

ubu1:

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
bob@ubu1:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 43.2M 1 loop /snap/snapd/17954
loop3 7:3 0 57.5M 1 loop /snap/core20/1332
loop4 7:4 0 43.2M 1 loop /snap/snapd/18363
loop5 7:5 0 60.7M 1 loop /snap/lxd/21843
loop6 7:6 0 91.9M 1 loop /snap/lxd/24065
loop7 7:7 0 59.1M 1 loop /snap/core20/1826
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 21G 0 disk
└─drbd2 147:2 0 21G 0 disk
sdc 8:32 0 31G 0 disk
└─drbd3 147:3 0 22G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /
bob@ubu1:~$ cat /etc/drbd.d/r1.res
resource r1 {
on ubu1 {
device /dev/drbd2;
disk /dev/sdb;
address 192.168.64.2:7792;
meta-disk internal;
}
on ubu2 {
device /dev/drbd2;
disk /dev/sdb;
address 192.168.64.4:7792;
meta-disk internal;
}
}
bob@ubu1:~$ cat /etc/drbd.d/r2.res
resource r2 {
on ubu1 {
device /dev/drbd3;
disk /dev/sdc;
address 192.168.64.2:7793;
meta-disk internal;
}
on ubu2 {
device /dev/drbd3;
disk /dev/sdc;
address 192.168.64.4:7793;
meta-disk internal;
}
}
bob@ubu1:~$ cat /proc/drbd
version: 8.4.11 (api:1/proto:86-101)
srcversion: B438804C5AE8C84C95D0411

1: cs:Connected ro:Secondary/Secondary ds:UpToDate/UpToDate C r-----
ns:0 nr:0 dw:0 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0
2: cs:Connected ro:Primary/Secondary ds:UpToDate/UpToDate C r-----
ns:22019388 nr:0 dw:0 dr:22021500 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0
3: cs:Connected ro:Primary/Secondary ds:UpToDate/UpToDate C r-----
ns:23067932 nr:0 dw:0 dr:23070036 al:8 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0

ubu2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bob@ubu2:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 57.5M 1 loop /snap/core20/1332
loop3 7:3 0 59.1M 1 loop /snap/core20/1826
loop4 7:4 0 60.7M 1 loop /snap/lxd/21843
loop5 7:5 0 43.2M 1 loop /snap/snapd/18363
loop6 7:6 0 43.2M 1 loop /snap/snapd/17954
loop7 7:7 0 91.9M 1 loop /snap/lxd/24065
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 32G 0 disk
└─drbd2 147:2 0 21G 1 disk
sdc 8:32 0 22G 0 disk
└─drbd3 147:3 0 22G 1 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /

硬盘扩容情况

在ubu1上使用lvm构建底层硬盘存储数据,方便后续进行扩容操作。先创建一个20G的lv,然后创建drbd块设备,待同步完数据后对lv进行扩容10G操作,最后再对drbd进行扩容操作。(此间省略lvm相关操作)

ubu2上使用一个30G的scsi设备,不配置lvm,与ubu1上的lvm块设备构成一个异构环境。

ubu1:

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
bob@ubu1:~/drbd_res$ cat /etc/drbd.d/r1.res
resource r1 {
on ubu1 {
device /dev/drbd2;
disk /dev/drbd-vg/drbdlv1;
address 192.168.64.2:7792;
meta-disk internal;
}
on ubu2 {
device /dev/drbd2;
disk /dev/sdb;
address 192.168.64.4:7792;
meta-disk internal;
}
}
bob@ubu1:~/drbd_res$ sudo drbdadm create-md r1
initializing activity log
initializing bitmap (640 KB) to all zero
Writing meta data...
New drbd meta data block successfully created.
success
bob@ubu1:~/drbd_res$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 43.2M 1 loop /snap/snapd/17954
loop3 7:3 0 57.5M 1 loop /snap/core20/1332
loop4 7:4 0 43.2M 1 loop /snap/snapd/18363
loop5 7:5 0 60.7M 1 loop /snap/lxd/21843
loop6 7:6 0 91.9M 1 loop /snap/lxd/24065
loop7 7:7 0 59.1M 1 loop /snap/core20/1826
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 21G 0 disk
└─drbd--vg-drbdlv1 253:1 0 20G 0 lvm
sdc 8:32 0 31G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /
bob@ubu1:~/drbd_res$ sudo drbdadm up r1
bob@ubu1:~/drbd_res$ sudo drbdadm primary --force r1
bob@ubu1:~/drbd_res$ cat /proc/drbd
version: 8.4.11 (api:1/proto:86-101)
srcversion: B438804C5AE8C84C95D0411

1: cs:Connected ro:Secondary/Secondary ds:UpToDate/UpToDate C r-----
ns:0 nr:0 dw:0 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0
2: cs:SyncSource ro:Primary/Secondary ds:UpToDate/Inconsistent C r-----
ns:14496 nr:0 dw:0 dr:16616 al:8 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:20956348
[>....................] sync'ed: 0.1% (20464/20476)M
finish: 2:21:35 speed: 2,416 (2,416) K/sec

ubu2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bob@ubu2:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 60.7M 1 loop /snap/lxd/21843
loop3 7:3 0 91.9M 1 loop /snap/lxd/24065
loop4 7:4 0 57.5M 1 loop /snap/core20/1332
loop5 7:5 0 59.1M 1 loop /snap/core20/1826
loop6 7:6 0 43.2M 1 loop /snap/snapd/17954
loop7 7:7 0 43.2M 1 loop /snap/snapd/18363
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 32G 0 disk
└─drbd2 147:2 0 20G 1 disk
sdc 8:32 0 22G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /

目前drbd2对外提供的是一个20G的硬盘,接下来在不停drbd复制链接的情况下对其扩容10G(同样,省略lvm相关操作)

ubu1:

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
bob@ubu1:~/drbd_res$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 43.2M 1 loop /snap/snapd/17954
loop3 7:3 0 57.5M 1 loop /snap/core20/1332
loop4 7:4 0 43.2M 1 loop /snap/snapd/18363
loop5 7:5 0 60.7M 1 loop /snap/lxd/21843
loop6 7:6 0 91.9M 1 loop /snap/lxd/24065
loop7 7:7 0 59.1M 1 loop /snap/core20/1826
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 21G 0 disk
└─drbd--vg-drbdlv1 253:1 0 30G 0 lvm
└─drbd2 147:2 0 20G 0 disk
sdc 8:32 0 31G 0 disk
└─drbd--vg-drbdlv1 253:1 0 30G 0 lvm
└─drbd2 147:2 0 20G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /
bob@ubu1:~/drbd_res$ sudo drbdadm status r1
r1 role:Primary
disk:UpToDate
peer role:Secondary
replication:SyncSource peer-disk:Inconsistent done:88.51
bob@ubu1:~/drbd_res$ sudo drbdadm resize r1
bob@ubu1:~/drbd_res$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 43.2M 1 loop /snap/snapd/17954
loop3 7:3 0 57.5M 1 loop /snap/core20/1332
loop4 7:4 0 43.2M 1 loop /snap/snapd/18363
loop5 7:5 0 60.7M 1 loop /snap/lxd/21843
loop6 7:6 0 91.9M 1 loop /snap/lxd/24065
loop7 7:7 0 59.1M 1 loop /snap/core20/1826
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 21G 0 disk
└─drbd--vg-drbdlv1 253:1 0 30G 0 lvm
└─drbd2 147:2 0 30G 0 disk
sdc 8:32 0 31G 0 disk
└─drbd--vg-drbdlv1 253:1 0 30G 0 lvm
└─drbd2 147:2 0 30G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /

ubu2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bob@ubu2:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 60.7M 1 loop /snap/lxd/21843
loop3 7:3 0 91.9M 1 loop /snap/lxd/24065
loop4 7:4 0 57.5M 1 loop /snap/core20/1332
loop5 7:5 0 59.1M 1 loop /snap/core20/1826
loop6 7:6 0 43.2M 1 loop /snap/snapd/17954
loop7 7:7 0 43.2M 1 loop /snap/snapd/18363
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 32G 0 disk
└─drbd2 147:2 0 30G 1 disk
sdc 8:32 0 22G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /

使用drbdadm resize r1完成对r1资源的扩容操作。

硬盘存在有效数据情况

ubu1和ubu2上分别创建了两个硬盘,一个20G存drbd的meta-data,一个30G用于存drbd的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bob@ubu1:~/drbd_res$ cat r2.res
resource r2 {
on ubu1 {
device /dev/drbd3;
disk /dev/sdc;
address 192.168.64.2:7793;
meta-disk /dev/sdb;
}
on ubu2 {
device /dev/drbd3;
disk /dev/sdb;
address 192.168.64.4:7793;
meta-disk /dev/sdc;
}
}

先对ubu1上的数据盘sdc创建文件系统,并写入数据。然后再做drbd pair。

ubu1:

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
bob@ubu1:~/drbd_res$ sudo mkfs.ext4 /dev/sdc
mke2fs 1.45.5 (07-Jan-2020)
Discarding device blocks: done
Creating filesystem with 8126464 4k blocks and 2031616 inodes
Filesystem UUID: 656f1b08-77a2-4488-acb1-f25ff616b257
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624

Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done

bob@ubu1:~/drbd_res$ sudo mount /dev/sdc /mnt/
bob@ubu1:~/drbd_res$ echo "Hi, bob" > /mnt/bob.txt
bob@ubu1:~/drbd_res$ sudo umount /mnt
bob@ubu1:~/drbd_res$ sudo drbdadm create-md r2
md_offset 0
al_offset 4096
bm_offset 36864

Found some data

==> This might destroy existing data! <==

Do you want to proceed?
[need to type 'yes' to confirm] yes

initializing activity log
initializing bitmap (672 KB) to all zero
Writing meta data...
New drbd meta data block successfully created.
success
bob@ubu1:~/drbd_res$ sudo drbdadm up r2
bob@ubu1:~/drbd_res$ sudo drbdadm primary --force r2
bob@ubu1:~/drbd_res$ cat /proc/drbd
version: 8.4.11 (api:1/proto:86-101)
srcversion: B438804C5AE8C84C95D0411

1: cs:Connected ro:Secondary/Secondary ds:UpToDate/UpToDate C r-----
ns:0 nr:0 dw:0 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0

3: cs:SyncSource ro:Primary/Secondary ds:UpToDate/Inconsistent C r-----
ns:130656 nr:0 dw:0 dr:131328 al:8 bm:0 lo:0 pe:4 ua:0 ap:0 ep:1 wo:f oos:32376640
[>....................] sync'ed: 0.5% (31616/31744)M
finish: 1:14:56 speed: 7,176 (7,176) K/sec

ubu2:

1
2
3
4
5
6
bob@ubu2:~$ sudo drbdadm create-md r2
initializing activity log
initializing bitmap (704 KB) to all zero
Writing meta data...
New drbd meta data block successfully created.
success

此时drbd pair已经创建好了,drbd3也已经创建完成,直接挂载drbd3查看数据是否依然还在

ubu1:

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
bob@ubu1:~/drbd_res$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 49.1M 1 loop /snap/core18/2681
loop1 7:1 0 49.1M 1 loop /snap/core18/2701
loop2 7:2 0 43.2M 1 loop /snap/snapd/17954
loop3 7:3 0 57.5M 1 loop /snap/core20/1332
loop4 7:4 0 43.2M 1 loop /snap/snapd/18363
loop5 7:5 0 60.7M 1 loop /snap/lxd/21843
loop6 7:6 0 91.9M 1 loop /snap/lxd/24065
loop7 7:7 0 59.1M 1 loop /snap/core20/1826
sda 8:0 0 20G 0 disk
└─drbd1 147:1 0 20G 1 disk
sdb 8:16 0 21G 0 disk
└─drbd3 147:3 0 31G 0 disk
sdc 8:32 0 31G 0 disk
└─drbd3 147:3 0 31G 0 disk
sr0 11:0 1 1.1G 0 rom
vda 252:0 0 100G 0 disk
├─vda1 252:1 0 512M 0 part /boot/efi
├─vda2 252:2 0 1G 0 part /boot
└─vda3 252:3 0 98.5G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 49.3G 0 lvm /
bob@ubu1:~/drbd_res$ sudo mount /dev/drbd3 /mnt/
bob@ubu1:~/drbd_res$ ls /mnt/
bob.txt lost+found
bob@ubu1:~/drbd_res$ cat /mnt/bob.txt
Hi, Bob

原理

DRBD(Distributed Replicated Block Device)是一个基于内核模块的高可用性(HA)软件,它在两个或多个节点之间复制块设备数据,使得在其中一个节点发生故障时,其他节点可以接管工作并保持服务可用性。

drbd-in-kernel.png

DRBD的工作原理:

  1. DRBD将每个块设备划分为数据和元数据两个区域。元数据包含块设备的状态信息和同步状态,而数据包含实际的用户数据。
  2. DRBD使用一个专用的网络通信通道(通常是TCP / IP)来在节点之间传输数据。当发生写操作时,数据被写入本地节点的数据区域,并通过网络传输到远程节点,然后在远程节点上写入远程节点的数据区域。
  3. DRBD还使用一种名为“生成标识符”(Generation Identifiers)的机制来检测节点间的数据同步状态。每个生成标识符对应一个生成版本,节点在进行数据同步时,使用生成标识符来判断版本是否相同。
  4. DRBD提供了多种数据同步策略,例如全同步,增量同步和快速同步,以便在不同的应用场景下获得最佳的性能和数据完整性。
  5. DRBD支持不同的工作模式,如协议A和协议C,用于适应不同的应用需求。协议A用于异步复制,适用于数据传输速度较慢的场景,而协议C则用于同步复制,适用于对数据完整性有较高要求的场景。

DRBD Metadata

DRBD元数据包括:

  • DRBD容量
  • GI(Generation Identifiers)
  • Activity Log
  • Quick-sync Bitmap

Internal meta data

资源配置成internal方式存储元数据,这意味着元数据将与底层实际存储的数据放在一起。它通过在设备末尾设置一个区域来专门存储元数据。

  • 优点
    由于元数据与实际数据密不可分,因此在硬盘发生故障时,管理员不需要采取任何特殊措施。元数据将随着实际数据丢失,并与实际数据一起恢复。

  • 缺点
    如果底层设备是单个物理硬盘(而不是 RAID 集),则内部元数据可能会对写入吞吐量产生负面影响。应用程序发起的写请求的性能可能会触发 DRBD 中元数据的更新。如果元数据存储在同一磁盘上,则写操作可能会导致硬盘的写入/读取头产生两次额外的移动。

如果您计划将内部元数据与已经有需要保留的数据的现有底层设备一起使用,您必须考虑 DRBD 元数据所需的空间。另外,在创建 DRBD 资源时,新创建的元数据可能会覆盖底层设备末尾的数据,从而可能破坏现有的文件。
为了避免这种情况发生,有三种解决方法:

  1. 扩容底层硬盘,保证其有足够的容量存储元数据。
  2. 缩小已存在于底层硬盘的数据的大小,这需要文件系统的支持。
  3. 使用external meta data 替代。

External meta data

External meta data 只是简单地存储在一个与包含生产数据的设备不同的、专用的块设备中。

  • 优势
    对于某些写操作,使用外部元数据可以产生略微改善的延迟行为。

  • 缺点
    元数据与实际生产数据并不密不可分。这意味着,在硬件故障仅破坏生产数据(但不破坏 DRBD 元数据)的情况下,需要手动干预,以实现从存活的节点对新替换磁盘的全面数据同步。

如果满足以下所有条件,使用外部元数据也是唯一可行的选择,您正在使用 DRBD 复制一个已经包含您希望保留的数据的现有设备,且该现有设备不支持扩大容量,且该设备上的现有文件系统不支持缩小容量。

Generation Identifiers

DRBD 使用Generation Identifiers(GIs)来标识“复制数据代”。数据同步是基于GI实现的。GI是一个递增的整数,每个DRBD设备都有自己的GI计数器,用于记录本地和远程节点上最新的GI。当DRBD设备从远程节点读取数据时,它会比较本地和远程节点上的GI值。如果本地GI值低于远程节点上的GI值,那么DRBD设备会自动启动数据同步,将远程节点上的数据复制到本地节点上。

该GI在DRBD的内部机制如下:

  • 确定这两个节点是否实际上是同一集群的成员(而不是意外连接的两个节点)
  • 确定后台重新同步的方向(如果有必要)
  • 确定是否需要进行完全重新同步或是否仅需要进行部分重新同步
  • 标识脑裂

分裂大脑是指当DRBD设备与另一个节点失去联系时发生的问题。在这种情况下,每个节点都可能认为自己是唯一的“活动”节点,并开始独立地写入数据。当两个节点再次连接时,它们可能会发现它们的数据不同步。GI用于解决这个问题。当两个节点重新连接时,它们比较彼此的GI值。如果GI值相同,则两个节点处于相同的“代”,可以继续同步数据。如果GI值不同,则两个节点处于不同的“代”,需要手动解决分裂大脑问题,通常需要将其中一个节点上的数据进行回滚。

数据代(Data generations)

以下情况下标记新的数据生成开始:

  1. 初始设备全量同步时
  2. 断开的资源切换到主要角色时
  3. 资源在主要角色下线时

因此,我们可以总结出,每当资源处于已连接的连接状态,并且两个节点的磁盘状态都为UpToDate时,两个节点上的当前数据生成相同。
反之亦然。请注意,当前实现使用最低位来编码节点的角色(主/辅)。因此,即使被认为具有相同的数据生成,不同节点上的最低位也可能不同。

每个新的数据生成由8字节的通用唯一标识符(UUID)标识。

GI元组(The generation identifier tuple)

BD在本地资源元数据中保存了有关当前和历史数据生成的四个信息:

  1. 当前UUID (Current UUID)
    这是从本地节点的视角看到的当前数据生成的生成标识符。当资源已连接并且同步完成时,当前UUID在节点之间是相同的。
  2. 位图UUID (Bitmap UUID)
    这是用于跟踪在断开连接模式下磁盘同步位图中的更改的生成的UUID。与磁盘同步位图本身一样,该标识符仅在断开连接模式下才相关。如果资源已连接,则此UUID始终为空(零)。
  3. 两个历史UUID (Historical UUIDs)
    它们是当前数据生成之前的两个数据生成的标识符。

GI如何变化

Start of a new data generation

gi-changes-newgen.png

当一个节点(无论是由于网络故障还是手动干预)失去与其对等节点的连接时,DRBD 以下方式修改其本地生成GI:

  1. 为新的数据生成创建一个新的 UUID。这成为主节点的新的 current UUID。
  2. 用之前的current UUID替换现在的Bitmap UUID,因此它成为主节点的新的Bitmap UUID。
  3. 在次要节点上,GI元组保持不变

Start of re-synchronization

gi-changes-syncstart.png

在重新同步初始化时,DRBD在本地GI执行这些修改:

  1. 同步源上的Current UUID保持不变。
  2. 同步源上的Bitmap UUID轮换到第一个历史UUID。
  3. 在同步源上生成一个新的Bitmap UUID。
  4. 此UUID成为同步目标上的新Current UUID。
  5. 同步目标上的Bitmap UUID和历史UUID保持不变。

Completion of re-synchronization

gi-changes-synccomplete.png

当重新同步完成,需要执行如下步骤:

  1. 同步源上的Current UUID保持不变
  2. 同步源上的Bitmap UUID轮转到第一个历史UUID,第一历史UUID替换第二历史UUID(任何已存在的第二历史UUID将被丢弃)
  3. 同步源上的Bitmap UUID将被清空(清零)
  4. 同步目标采用来自同步源的整个GI元组

DRBD如何使用GI

当两个节点之间建立连接时,它们会交换当前可用的生成标识符,并相应地进行操作。可能有许多可能的结果:

  • 两个节点的Current UUID都为空
    本地节点检测到它自己和对等节点的当前UUID都为空。这是刚刚配置好的资源的正常情况,尚未启动初始完全同步。不会进行同步,必须手动启动。
  • 一个节点的Current UUID为空
    本地节点检测到对等节点的Current UUID为空,而自己的不为空。这是刚刚配置好的资源的正常情况,初始全同步刚刚启动,本地节点被选为初始同步源。DRBD现在设置磁盘上同步位图中的所有位(意味着它认为整个设备不同步),并作为同步源开始同步。在相反的情况下(本地Current UUID为空,对等节点不为空),DRBD执行相同的步骤,只是本地节点变成同步目标。
  • Current UUID相等
    本地节点检测到其Current UUID和对等节点的Current UUID都不为空且相等。这是在资源处于从属角色时进入断开连接模式的资源的正常情况,且在断开连接期间没有在任一节点上进行晋升。不会进行同步,因为不需要同步。
  • Bitmap UUID与对等节点的Current UUID匹配
    本地节点检测到它的Bitmap UUID与对等节点的Current UUID匹配,而对等节点的Bitmap UUID为空。这是从属节点故障后的正常预期情况,本地节点处于主角色。这意味着对等节点在此期间从未变为主节点,并一直使用相同的数据生成基础。现在,DRBD会启动正常的后台重新同步,本地节点成为同步源。反之,如果本地节点检测到它的Bitmap UUID为空,并且对等节点的位图与本地节点的Current UUID匹配,那么这是本地节点故障后的正常预期情况。同样,DRBD现在启动正常的后台重新同步,本地节点成为同步目标。
  • Current UUID与对等节点的历史UUID匹配
    本地节点检测到它的Current UUID与对等节点的历史UUID之一匹配。这意味着尽管两个数据集共享一个共同的祖先,并且对等节点具有最新的数据,但在对等节点的位图中保留的信息已经过时且无法使用。因此,正常同步是不足够的。DRBD现在将整个设备标记为不同步,并启动完全后台重新同步,本地节点成为同步目标。在相反的情况(本地节点的历史 UUID 与对等节点的Current UUID匹配),DRBD 执行相同的步骤,只是本地节点成为同步源。
  • Bitmap UUID匹配,但Current UUID不匹配
    本地节点检测到其Current UUID与对等节点的Current UUID不同,但Bitmap UUID匹配。这是脑裂的情况,但数据生成具有相同的父级。这意味着如果已配置,DRBD 将调用脑裂自动恢复策略。否则,DRBD将断开连接并等待手动脑裂解决。
  • Current UUID 和Bitmap UUID 都不匹配
    本地节点检测到其Current UUID与对等节点的Current UUID不同,且Bitmap UUID也不匹配。这是不相关祖先代的脑裂,因此即使配置了自动恢复策略,也是无用的。DRBD断开连接并等待手动脑裂解决。
  • 没有 UUID 匹配
    如果DRBD未能在两个节点的GI元组中检测到任何匹配的元素,它会记录关于不相关数据的警告并断开连接。这是DRBD防止意外连接两个之前从未听说过对方的集群节点的安全保障。

Activity Log

在一个写操作期间,DRBD将写操作转发到本地后备块设备,但也通过网络发送数据块。这两个操作实际上都是同时进行的。随机的时序行为可能会导致这样一种情况:写操作已经完成,但网络传输尚未完成。如果此时活动节点失败并启动故障转移,则这个数据块在节点之间是不同步的——在崩溃之前已在失败的节点上写入,但复制尚未完成。因此,当节点最终恢复时,这个块必须在随后的同步期间从数据集中删除。否则,崩溃的节点将“领先于”存活的节点,这将违反复制存储的“全部或无”的原则。这不仅是DRBD所特有的问题,在实际上所有的复制存储配置中都存在这个问题。许多其他存储解决方案(就像DRBD本身在0.7版本之前)要求在活动节点失败后,该节点必须在恢复后进行完全同步。

DRBD自0.7版本之后。在元数据区域存储活动日志(Activity Log)跟踪了“最近”已经写入的那些块。俗称,这些区域被称为热区段。如果一个临时失败的处于活动模式的节点进行同步,只有在AL中突出显示的那些热区段需要进行同步,而不需要同步整个设备。这大大减少了在活动节点崩溃后进行同步所需的时间。

Activity extents

Activity Log由多个Activity extents组成。可通过资源配置文件中的activity-log-size参数进行配置,该参数表示Active extents数量,必须是2的幂,且范围通常在16到4096之间。每个Activity extents大小为4MiB。

保持大的活动日志可以提高写入吞吐量。每次激活新的扩展时,一个旧的扩展将被重置为非活动状态。这种转换需要向元数据区域写入一个操作。如果活动扩展数很高,旧的活动扩展很少被交换出,从而减少了元数据写操作,从而提高了性能。

保持小的活动日志可以缩短在主节点失败和随后的恢复后的同步时间。

选择一个合适Activity Log Size

al-extents.png

  • R是同步速度(单位:MB/s)
  • tsync是同步时间(单位:s)
  • E是Active extents数量

DRBD是可以控制同步带宽的,使用net配置选项来控制DRBD在网络上使用的带宽。这个选项可以在DRBD配置文件中的全局段中设置,也可以在资源段中设置。

eg:

1
2
3
4
5
6
global {
...
net {
max-rate 1G;
}
}

max-rate选项将同步带宽限制为1Gbit/s。您可以将该选项设置为所需的速率,以控制DRBD在网络上使用的带宽。

Quick-sync Bitmap

Quick-sync Bitmap是 DRBD 在每个资源上使用的内部数据结构,用于跟踪块是否同步(在两个节点上相同)或不同步。它仅在资源处于断开模式时才相关。在Quick-sync Bitmap中,一个比特表示一个 4KiB 的磁盘数据块。如果该位被清除,则意味着相应的块仍与对等节点同步。这意味着该块自断开连接以来尚未被写入。相反,如果该位被设置,则意味着该块已被修改,并且在连接再次可用时需要重新同步。

当 DRBD 检测到断开的设备上的写 I/O 时,因此开始在Quick-sync Bitmap中设置位,它在 RAM 中执行此操作,从而避免了昂贵的同步元数据 I/O 操作。只有当相应的块变得“冷”(从活动日志中过期)时,DRBD 才会在Quick-sync Bitmap的磁盘表示中进行适当的修改。同样,如果在断开连接的同时手动关闭了剩余节点上的资源,则 DRBD 将完整的Quick-sync Bitmap刷新到持久存储中。当对等节点恢复或重新建立连接时,DRBD 将结合两个节点的位图信息,确定必须重新同步的总数据集。同时,DRBD 检查生成标识符以确定同步方向。

充当同步源的节点然后将协商好的块传输到对等节点,在同步目标确认修改时清除同步位。如果重新同步现在被中断(例如,由于另一个网络故障),然后继续恢复,它将在离开时继续进行 - 当然,同时修改的任何其他块都将添加到重新同步数据集中。

参考&鸣谢

问:写一个以太坊智能合约helloworld总共分几步?
答:三步,第一步创建以太坊服务环境;第二步撰写、编译、部署helloworld智能合约;第三步运行智能合约

部署Ethereum环境

本人使用docker搭建Ethereum环境,拉取ubuntu:20.04镜像。

1
docker pull --platform linux/amd64 ubuntu:20.04

然后,创建容器并开始安装Ethereum

1
2
docker run -it --name eth_server --net host ubuntu:20.04 /bin/bash
docker run -it --net bridge -p 8545:8545 -p 8551:8551 --name eth_server ubuntu:20.04 /bin/bash

安装Ethereum

1
2
3
4
5
6
7
8
9
# apt update -y
...
# apt install -y software-properties-common
...
# add-apt-repository -y ppa:ethereum/ethereum
...
# apt update -y
...
# apt install -y ethereum

安装成功后,确认一下eth版本。

1
2
3
4
5
6
7
8
9
# geth version
Geth
Version: 1.10.26-stable
Git Commit: e5eb32acee19cc9fca6a03b10283b7484246b15a
Architecture: amd64
Go Version: go1.18.5
Operating System: linux
GOPATH=
GOROOT=go

初始化

创建配置文件

创建一个genesis.json的文件,填充如下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Config": {
"chainId": 110,
"homesteadBlock": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0
},
"difficulty": "0",
"gasLimit": "2100000",
"alloc": {}
}

初始化Ethereum数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# geth --datadir ./eth-data --allow-insecure-unlock --http --http.addr 172.17.0.2 --http.api "admin,debug,web3,eth,txpool,personal,ethash,miner,net" --http.corsdomain "*" --dev init genesis.json
INFO [11-07|16:04:14.139] Maximum peer count ETH=50 LES=0 total=50
INFO [11-07|16:04:14.151] Smartcard socket not found, disabling err="stat /run/pcscd/pcscd.comm: no such file or directory"
INFO [11-07|16:04:14.179] Set global gas cap cap=50,000,000
INFO [11-07|16:04:14.190] Allocated cache and file handles database=/root/eth-data/geth/chaindata cache=16.00MiB handles=16
INFO [11-07|16:04:14.224] Opened ancient database database=/root/eth-data/geth/chaindata/ancient/chain readonly=false
INFO [11-07|16:04:14.226] Writing custom genesis block
INFO [11-07|16:04:14.230] Persisted trie from memory database nodes=0 size=0.00B time="347µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [11-07|16:04:14.238] Successfully wrote genesis state database=chaindata hash=a697c6..9bf39b
INFO [11-07|16:04:14.238] Allocated cache and file handles database=/root/eth-data/geth/lightchaindata cache=16.00MiB handles=16
INFO [11-07|16:04:14.259] Opened ancient database database=/root/eth-data/geth/lightchaindata/ancient/chain readonly=false
INFO [11-07|16:04:14.259] Writing custom genesis block
INFO [11-07|16:04:14.261] Persisted trie from memory database nodes=0 size=0.00B time="21.917µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [11-07|16:04:14.262] Successfully wrote genesis state database=lightchaindata hash=a697c6..9bf39b

启动节点

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
# geth --datadir ./eth-data --networkid 110 --allow-insecure-unlock --http --http.addr 172.17.0.2 --http.api "admin,debug,web3,eth,txpool,personal,ethash,miner,net" --http.corsdomain "*" --dev
INFO [11-07|16:14:23.231] Starting Geth in ephemeral dev mode...
WARN [11-07|16:14:23.234] You are running Geth in --dev mode. Please note the following:

1. This mode is only intended for fast, iterative development without assumptions on
security or persistence.
2. The database is created in memory unless specified otherwise. Therefore, shutting down
your computer or losing power will wipe your entire block data and chain state for
your dev environment.
3. A random, pre-allocated developer account will be available and unlocked as
eth.coinbase, which can be used for testing. The random dev account is temporary,
stored on a ramdisk, and will be lost if your machine is restarted.
4. Mining is enabled by default. However, the client will only seal blocks if transactions
are pending in the mempool. The miner's minimum accepted gas price is 1.
5. Networking is disabled; there is no listen-address, the maximum number of peers is set
to 0, and discovery is disabled.
INFO [11-07|16:14:23.251] Maximum peer count ETH=50 LES=0 total=50
INFO [11-07|16:14:23.261] Smartcard socket not found, disabling err="stat /run/pcscd/pcscd.comm: no such file or directory"
INFO [11-07|16:14:23.291] Set global gas cap cap=50,000,000
INFO [11-07|16:14:23.649] Using developer account address=0xA9CB6DB62D6673ae5CD79D0d29796Dd9DF1d1A5e
INFO [11-07|16:14:23.652] Allocated cache and file handles database=/root/eth-data/geth/chaindata cache=512.00MiB handles=524,288 readonly=true
INFO [11-07|16:14:23.675] Opened ancient database database=/root/eth-data/geth/chaindata/ancient/chain readonly=true
INFO [11-07|16:14:23.691] Allocated trie memory caches clean=154.00MiB dirty=256.00MiB
INFO [11-07|16:14:23.691] Allocated cache and file handles database=/root/eth-data/geth/chaindata cache=512.00MiB handles=524,288
INFO [11-07|16:14:23.756] Opened ancient database database=/root/eth-data/geth/chaindata/ancient/chain readonly=false
INFO [11-07|16:14:23.763]
INFO [11-07|16:14:23.764] ---------------------------------------------------------------------------------------------------------------------------------------------------------
INFO [11-07|16:14:23.764] Chain ID: 110 (unknown)
INFO [11-07|16:14:23.764] Consensus: unknown
INFO [11-07|16:14:23.764]
INFO [11-07|16:14:23.765] Pre-Merge hard forks:
INFO [11-07|16:14:23.765] - Homestead: 0 (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/homestead.md)
INFO [11-07|16:14:23.765] - Tangerine Whistle (EIP 150): 0 (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/tangerine-whistle.md)
INFO [11-07|16:14:23.765] - Spurious Dragon/1 (EIP 155): 0 (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/spurious-dragon.md)
INFO [11-07|16:14:23.765] - Spurious Dragon/2 (EIP 158): 0 (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/spurious-dragon.md)
INFO [11-07|16:14:23.765] - Byzantium: <nil> (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/byzantium.md)
INFO [11-07|16:14:23.765] - Constantinople: <nil> (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/constantinople.md)
INFO [11-07|16:14:23.765] - Petersburg: <nil> (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/petersburg.md)
INFO [11-07|16:14:23.766] - Istanbul: <nil> (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/istanbul.md)
INFO [11-07|16:14:23.766] - Berlin: <nil> (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/berlin.md)
INFO [11-07|16:14:23.766] - London: <nil> (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/london.md)
INFO [11-07|16:14:23.766]
INFO [11-07|16:14:23.766] The Merge is not yet available for this network!
INFO [11-07|16:14:23.766] - Hard-fork specification: https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/paris.md
INFO [11-07|16:14:23.766] ---------------------------------------------------------------------------------------------------------------------------------------------------------
INFO [11-07|16:14:23.766]
INFO [11-07|16:14:23.768] Disk storage enabled for ethash caches dir=/root/eth-data/geth/ethash count=3
INFO [11-07|16:14:23.768] Disk storage enabled for ethash DAGs dir=/root/.ethash count=2
INFO [11-07|16:14:23.769] Initialising Ethereum protocol network=1337 dbversion=8
INFO [11-07|16:14:23.779] Loaded most recent local header number=0 hash=a697c6..9bf39b td=0 age=53y7mo1w
INFO [11-07|16:14:23.780] Loaded most recent local full block number=0 hash=a697c6..9bf39b td=0 age=53y7mo1w
INFO [11-07|16:14:23.780] Loaded most recent local fast block number=0 hash=a697c6..9bf39b td=0 age=53y7mo1w
INFO [11-07|16:14:23.786] Loaded local transaction journal transactions=0 dropped=0
INFO [11-07|16:14:23.786] Regenerated local transaction journal transactions=0 accounts=0
INFO [11-07|16:14:23.791] Gasprice oracle is ignoring threshold set threshold=2
WARN [11-07|16:14:23.798] Engine API enabled protocol=eth
WARN [11-07|16:14:23.798] Engine API started but chain not configured for merge yet
INFO [11-07|16:14:23.802] Starting peer-to-peer node instance=Geth/v1.10.26-stable-e5eb32ac/linux-amd64/go1.18.5
WARN [11-07|16:14:23.802] P2P server will be useless, neither dialing nor listening
INFO [11-07|16:14:23.843] New local node record seq=1,667,808,816,313 id=321fce2047223769 ip=127.0.0.1 udp=0 tcp=0
INFO [11-07|16:14:23.844] Started P2P networking self=enode://2d0246c1dd51623d6d8a8581095c033542366037b1e20ff815ad045af396de50df60f4aa9556148c2f4a89673bad5cab5c2ab22f3075d5bacba1b2fbebaf72e5@127.0.0.1:0
INFO [11-07|16:14:23.848] IPC endpoint opened url=/root/eth-data/geth.ipc
INFO [11-07|16:14:23.852] Loaded JWT secret file path=/root/eth-data/geth/jwtsecret crc32=0xef39c4cb
INFO [11-07|16:14:23.856] HTTP server started endpoint=172.17.0.2:8545 auth=false prefix= cors= vhosts=localhost
INFO [11-07|16:14:23.862] WebSocket enabled url=ws://127.0.0.1:8551
INFO [11-07|16:14:23.862] HTTP server started endpoint=127.0.0.1:8551 auth=true prefix= cors=localhost vhosts=localhost
INFO [11-07|16:14:23.868] Transaction pool price threshold updated price=0
INFO [11-07|16:14:23.868] Updated mining threads threads=0
INFO [11-07|16:14:23.868] Transaction pool price threshold updated price=1
INFO [11-07|16:14:23.868] Etherbase automatically configured address=0xA9CB6DB62D6673ae5CD79D0d29796Dd9DF1d1A5e
INFO [11-07|16:14:23.875] Commit new sealing work number=1 sealhash=6ca53b..19dc17 uncles=0 txs=0 gas=0 fees=0 elapsed=6.285ms
INFO [11-07|16:14:23.877] Commit new sealing work number=1 sealhash=6ca53b..19dc17 uncles=0 txs=0 gas=0 fees=0 elapsed=8.056ms

networkid

--networkid参数需要与genesis.json配置文件中的chainId值一致。

http.api

--http.api设置错误会出现如下错误

1
ERROR[11-07|16:13:36.346] Unavailable modules in HTTP API list     unavailable=[db] available="[admin debug web3 eth txpool personal ethash miner net]"

需要按照available中规定的内容进行配置--http.api参数。

attach交互

接下来需要attach到以太坊节点,在geth节点启动过程中有这样一条日志

1
INFO [11-07|16:14:23.848] IPC endpoint opened                      url=/root/eth-data/geth.ipc

没错,你猜对了,就是要用这个endpoint进行attach

1
2
3
4
5
6
7
8
9
10
11
# geth attach ipc:/root/eth-data/geth.ipc
Welcome to the Geth JavaScript console!

instance: Geth/v1.10.26-stable-e5eb32ac/linux-amd64/go1.18.5
coinbase: 0xa9cb6db62d6673ae5cd79d0d29796dd9df1d1a5e
at block: 0 (Thu Jan 01 1970 08:00:00 GMT+0800 (CST))
datadir: /root/eth-data
modules: admin:1.0 debug:1.0 engine:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit
>

接下来的创建用户挖矿等操作都需要在attach状态下进行。

创建用户

使用personal.newAccout创建用户,然后可以使用eth.accounts查看用户列表。

1
2
3
4
5
6
7
8
> eth.accounts
> ["0xa9cb6db62d6673ae5cd79d0d29796dd9df1d1a5e"]
> personal.newAccount()
> Passphrase:
> Repeat passphrase:
> "0x00e70f2bd5a644cdf4432f886bf25473cbe620ac"
> eth.accounts
> ["0xa9cb6db62d6673ae5cd79d0d29796dd9df1d1a5e", "0x00e70f2bd5a644cdf4432f886bf25473cbe620ac"]

若不解锁用户,部署会提示错误creation of HelloWorld errored: authentication needed: password or unlock。解锁用户

1
2
3
4
> personal.unlockAccount(eth.accounts[1])
> Unlock account 0x00e70f2bd5a644cdf4432f886bf25473cbe620ac
> Passphrase:
> true

挖矿

使用miner.start()开始挖矿;使用miner.stop()停止挖矿。

开始挖矿后,当出现如下日志信息时,说明挖到了。

1
2
3
4
5
6
7
8
9
10
11
12
INFO [11-07|17:59:54.021] Successfully sealed new block            number=1 sealhash=84eaaa..f4d2c5 hash=925c9a..d8cb75 elapsed=1h30m51.894s
INFO [11-07|17:59:54.022] 🔨 mined potential block number=1 hash=925c9a..d8cb75
INFO [11-07|17:59:54.026] Commit new sealing work number=2 sealhash=865003..0e32da uncles=0 txs=0 gas=0 fees=0 elapsed=1.991ms
INFO [11-07|17:59:54.027] Commit new sealing work number=2 sealhash=865003..0e32da uncles=0 txs=0 gas=0 fees=0 elapsed=3.070ms
INFO [11-07|17:59:54.428] Generating DAG in progress epoch=1 percentage=0 elapsed=3.563s
INFO [11-07|17:59:56.581] Successfully sealed new block number=2 sealhash=865003..0e32da hash=f1224d..9dc4b8 elapsed=2.554s
INFO [11-07|17:59:56.582] 🔨 mined potential block number=2 hash=f1224d..9dc4b8
INFO [11-07|17:59:56.584] Commit new sealing work number=3 sealhash=8de6d6..c8886e uncles=0 txs=0 gas=0 fees=0 elapsed="906.417µs"
INFO [11-07|17:59:56.585] Commit new sealing work number=3 sealhash=8de6d6..c8886e uncles=0 txs=0 gas=0 fees=0 elapsed=2.271ms
INFO [11-07|17:59:57.730] Successfully sealed new block number=3 sealhash=8de6d6..c8886e hash=42a018..4e7ef3 elapsed=1.146s
INFO [11-07|17:59:57.731] 🔨 mined potential block number=3 hash=42a018..4e7ef3
INFO [11-07|17:59:57.733] Commit new sealing work number=4 sealhash=26af07..609e57 uncles=0 txs=0 gas=0 fees=0 elapsed=1.165ms

此时使用eth.blockNumber可以查看到当前区块数量,使用eth.getBalance(eth.accounts[0])查看默认用户余额,同理使用eth.getBalance(eth.accounts[1])查看我们创建的用户的余额。

1
2
3
4
> eth.getBalance(eth.accounts[0])
> 25000000000000000000
> eth.getBalance(eth.accounts[1])
> 0

挖到的奖励都进了默认账户了,我们新建的账户里木有哦。来给我转账吧,嘿嘿嘿….

1
2
3
4
5
6
> eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:web3.toWei(10,'ether')})
> "0x244da771baf2bd65e5e040c33ee3047b57a2492b85d271564bc90ccd7cb4fa46"
> eth.getBalance(eth.accounts[1])
> 5000000000000000000
> > eth.getBalance(eth.accounts[0])
> 1.535e+21

哇哦,又挖到矿了。

智能合约

环境搭建好了,现在开始编写智能合约。

1
2
3
4
5
6
7
8
// compiler version must be greater than or equal to 0.8.13 and less than 0.9.0
// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;
contract HelloWorld {
function sayHelloWorld() public returns (string memory) {
return "Hello World";
}
}

Remix

在这里获Remix

编译

solidity_remix_compile

部署

点击部署,然后选择外部http provider,并配置我们搭建好的ethereum服务。

solidity_remix_deploy_setting_1.jpg

solidity_remix_deploy_setting_2.jpg

接下中,选择我们创建好的账户,然后进行部署。

solidity_remix_deploy.jpg

solidity_remix_deploy_log.jpg

执行智能合约

执行智能合约可以选择在Remix中执行,也可选择attach到eth_server控制台执行。

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
> abi =[{"inputs":[],"name":"sayHelloWorld","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"nonpayable","type":"function"}]
[{
inputs: [],
name: "sayHelloWorld",
outputs: [{
internalType: "string",
name: "",
type: "string"
}],
stateMutability: "nonpayable",
type: "function"
}]
> hello=eth.contract(abi).at('0xCB1B01B40CD752F5d42f5b8dCeE4BE2A637CaAf2')
{
abi: [{
inputs: [],
name: "sayHelloWorld",
outputs: [{...}],
stateMutability: "nonpayable",
type: "function"
}],
address: "0xCB1B01B40CD752F5d42f5b8dCeE4BE2A637CaAf2",
transactionHash: null,
allEvents: function bound(),
sayHelloWorld: function bound()
}
> hello.sayHelloWorld.call()
"Hello World"

参考&鸣谢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct object_t {
std::string name;

......

void encode(ceph::buffer::list &bl) const {
using ceph::encode;
encode(name, bl);
}
void decode(ceph::buffer::list::const_iterator &bl) {
using ceph::decode;
decode(name, bl);
}
};
WRITE_CLASS_ENCODER(object_t)

*** From: src/include/object.h ***

对于Ceph中的每一种需要存储的资源在进行存储前都要进行encode操作,然后再将其写入硬盘。对于读取同样,在从硬盘获取到数据后需要进行decode操作。而每种需要存储资源如何encodedecode当然要由资源自己来决定。所以在资源的classstruct中要实现encodedecode方法。

WRITE_CLASS_ENCODER(object_t)干了些啥呢。。。

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
// see denc.h for ENCODE_DUMP_PATH discussion and definition.
#ifdef ENCODE_DUMP_PATH
# define ENCODE_DUMP_PRE() \
unsigned pre_off = bl.length()
# define ENCODE_DUMP_POST(cl) \
do { \
static int i = 0; \
i++; \
int bits = 0; \
for (unsigned t = i; t; bits++) \
t &= t - 1; \
if (bits > 2) \
break; \
char fn[PATH_MAX]; \
snprintf(fn, sizeof(fn), ENCODE_STRINGIFY(ENCODE_DUMP_PATH) "/%s__%d.%x", #cl, getpid(), i++); \
int fd = ::open(fn, O_WRONLY|O_TRUNC|O_CREAT|O_CLOEXEC|O_BINARY, 0644); \
if (fd >= 0) { \
::ceph::bufferlist sub; \
sub.substr_of(bl, pre_off, bl.length() - pre_off); \
sub.write_fd(fd); \
::close(fd); \
} \
} while (0)
#else
# define ENCODE_DUMP_PRE()
# define ENCODE_DUMP_POST(cl)
#endif


#define WRITE_CLASS_ENCODER(cl) \
inline void encode(const cl& c, ::ceph::buffer::list &bl, uint64_t features=0) { \
ENCODE_DUMP_PRE(); c.encode(bl); ENCODE_DUMP_POST(cl); } \
inline void decode(cl &c, ::ceph::bufferlist::const_iterator &p) { c.decode(p); }

*** From: src/include/encoding.h ***

看了上面的代码应该能了解到WRITE_CLASS_ENCODER(object_t)是对encodedecode函数的重载。这是入口,然后再调用其资源自身encodedecode方法。

那么对于一些基础类型(如:int、string等)是如果encodedecode的呢?

int类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// int types

#define WRITE_INTTYPE_ENCODER(type, etype) \
inline void encode(type v, ::ceph::bufferlist& bl, uint64_t features=0) { \
ceph_##etype e; \
e = v; \
::ceph::encode_raw(e, bl); \
} \
inline void decode(type &v, ::ceph::bufferlist::const_iterator& p) { \
ceph_##etype e; \
::ceph::decode_raw(e, p); \
v = e; \
}

WRITE_INTTYPE_ENCODER(uint64_t, le64)
WRITE_INTTYPE_ENCODER(int64_t, le64)
WRITE_INTTYPE_ENCODER(uint32_t, le32)
WRITE_INTTYPE_ENCODER(int32_t, le32)
WRITE_INTTYPE_ENCODER(uint16_t, le16)
WRITE_INTTYPE_ENCODER(int16_t, le16)

*** From: src/include/encoding.h ***

int类型的encodedecode又调用了encode_rawdecode_raw。真是一层套一层啊~(俄罗斯套娃嘛)~

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
// base types

template<class T>
inline void encode_raw(const T& t, bufferlist& bl)
{
bl.append((char*)&t, sizeof(t));
}
template<class T>
inline void decode_raw(T& t, bufferlist::const_iterator &p)
{
p.copy(sizeof(t), (char*)&t);
}

#define WRITE_RAW_ENCODER(type) \
inline void encode(const type &v, ::ceph::bufferlist& bl, uint64_t features=0) { ::ceph::encode_raw(v, bl); } \
inline void decode(type &v, ::ceph::bufferlist::const_iterator& p) { ::ceph::decode_raw(v, p); }

WRITE_RAW_ENCODER(__u8)
#ifndef _CHAR_IS_SIGNED
WRITE_RAW_ENCODER(__s8)
#endif
WRITE_RAW_ENCODER(char)
WRITE_RAW_ENCODER(ceph_le64)
WRITE_RAW_ENCODER(ceph_le32)
WRITE_RAW_ENCODER(ceph_le16)

*** From: src/include/encoding.h ***

base比较简单,就是无论int几个字节,都是从低到高一个字节一个字节的写下去,再一个字节一个字节的读出来。。。

float类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define WRITE_FLTTYPE_ENCODER(type, itype, etype)                       \
static_assert(sizeof(type) == sizeof(itype)); \
static_assert(std::numeric_limits<type>::is_iec559, \
"floating-point type not using IEEE754 format"); \
inline void encode(type v, ::ceph::bufferlist& bl, uint64_t features=0) { \
ceph_##etype e; \
e = *reinterpret_cast<itype *>(&v); \
::ceph::encode_raw(e, bl); \
} \
inline void decode(type &v, ::ceph::bufferlist::const_iterator& p) { \
ceph_##etype e; \
::ceph::decode_raw(e, p); \
*reinterpret_cast<itype *>(&v) = e; \
}

WRITE_FLTTYPE_ENCODER(float, uint32_t, le32)
WRITE_FLTTYPE_ENCODER(double, uint64_t, le64)

*** From: src/include/encoding.h ***

float类型关键在于reinterpret_cast将一个浮点数转换为整数。更多关于reinterpret_cast的内容

string

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
// string
inline void encode(std::string_view s, bufferlist& bl, uint64_t features=0)
{
__u32 len = s.length();
encode(len, bl);
if (len)
bl.append(s.data(), len);
}
inline void encode(const std::string& s, bufferlist& bl, uint64_t features=0)
{
return encode(std::string_view(s), bl, features);
}
inline void decode(std::string& s, bufferlist::const_iterator& p)
{
__u32 len;
decode(len, p);
s.clear();
p.copy(len, s);
}

inline void encode_nohead(std::string_view s, bufferlist& bl)
{
bl.append(s.data(), s.length());
}
inline void encode_nohead(const std::string& s, bufferlist& bl)
{
encode_nohead(std::string_view(s), bl);
}
inline void decode_nohead(int len, std::string& s, bufferlist::const_iterator& p)
{
s.clear();
p.copy(len, s);
}

// const char* (encode only, string compatible)
inline void encode(const char *s, bufferlist& bl)
{
encode(std::string_view(s, strlen(s)), bl);
}

*** From: src/include/encoding.h ***

string的encodedecode分两种,一种是有“害的”(head),一种是无“害的”。有“害的”需要先记录string的长度,再记录string的内容;无“害的”直接记录内容,单再decode过程中需要制定长度。总之这个长度总要有个人来记。好鸡肋!


整个的encodedecode的过程用到了一个bufferlist类型,那么这个bufferlist又是个什么结构呢,详细请见ceph中的buffer

c++中stl涉及到的容器类(或struct)有很多,具体都是什么原理呢?

std::array

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
  template<typename _Tp, std::size_t _Nm>
struct __array_traits
{
typedef _Tp _Type[_Nm];
typedef __is_swappable<_Tp> _Is_swappable;
typedef __is_nothrow_swappable<_Tp> _Is_nothrow_swappable;

static constexpr _Tp&
_S_ref(const _Type& __t, std::size_t __n) noexcept
{ return const_cast<_Tp&>(__t[__n]); }

static constexpr _Tp*
_S_ptr(const _Type& __t) noexcept
{ return const_cast<_Tp*>(__t); }
};
......
template<typename _Tp, std::size_t _Nm>
struct array
{
typedef _Tp value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef value_type* iterator;
typedef const value_type* const_iterator;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

// Support for zero-sized arrays mandatory.
typedef _GLIBCXX_STD_C::__array_traits<_Tp, _Nm> _AT_Type;
typename _AT_Type::_Type _M_elems;

// No explicit construct/copy/destroy for aggregate type.
......

看这里typename _AT_Type::_Type _M_elems;再看这里typedef _Tp _Type[_Nm];。懂了吗,就是在栈上分配一个大小固定的数组。

std::vector

vector也是一个数组,只是不是分配在栈上的,而是分配在堆上的。

看这里有一个_Alloc默认使用的是std::allocator<_Tp>,这就是在堆上分配内存用的。

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
  /**
* @brief A standard container which offers fixed time access to
* individual elements in any order.
*
* @ingroup sequences
*
* @tparam _Tp Type of element.
* @tparam _Alloc Allocator type, defaults to allocator<_Tp>.
*
* Meets the requirements of a <a href="tables.html#65">container</a>, a
* <a href="tables.html#66">reversible container</a>, and a
* <a href="tables.html#67">sequence</a>, including the
* <a href="tables.html#68">optional sequence requirements</a> with the
* %exception of @c push_front and @c pop_front.
*
* In some terminology a %vector can be described as a dynamic
* C-style array, it offers fast and efficient access to individual
* elements in any order and saves the user from worrying about
* memory and size allocation. Subscripting ( @c [] ) access is
* also provided as with C-style arrays.
*/
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
{
#ifdef _GLIBCXX_CONCEPT_CHECKS
// Concept requirements.
typedef typename _Alloc::value_type _Alloc_value_type;
# if __cplusplus < 201103L
__glibcxx_class_requires(_Tp, _SGIAssignableConcept)
# endif
__glibcxx_class_requires2(_Tp, _Alloc_value_type, _SameTypeConcept)
#endif

#if __cplusplus >= 201103L
static_assert(is_same<typename remove_cv<_Tp>::type, _Tp>::value,
"std::vector must have a non-const, non-volatile value_type");
# if __cplusplus > 201703L || defined __STRICT_ANSI__
static_assert(is_same<typename _Alloc::value_type, _Tp>::value,
"std::vector must have the same value_type as its allocator");
# endif
#endif

typedef _Vector_base<_Tp, _Alloc> _Base;
typedef typename _Base::_Tp_alloc_type _Tp_alloc_type;
typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Alloc_traits;

public:
typedef _Tp value_type;
typedef typename _Base::pointer pointer;
typedef typename _Alloc_traits::const_pointer const_pointer;
typedef typename _Alloc_traits::reference reference;
typedef typename _Alloc_traits::const_reference const_reference;
typedef __gnu_cxx::__normal_iterator<pointer, vector> iterator;
typedef __gnu_cxx::__normal_iterator<const_pointer, vector>
const_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef _Alloc allocator_type;
......

再看这里定义了三个指针,用的是typedef typename _Base::pointer pointer;类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
_GLIBCXX_BEGIN_NAMESPACE_CONTAINER

/// See bits/stl_deque.h's _Deque_base for an explanation.
template<typename _Tp, typename _Alloc>
struct _Vector_base
{
typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
rebind<_Tp>::other _Tp_alloc_type;
typedef typename __gnu_cxx::__alloc_traits<_Tp_alloc_type>::pointer
pointer;

struct _Vector_impl_data
{
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage;

_Vector_impl_data() _GLIBCXX_NOEXCEPT
: _M_start(), _M_finish(), _M_end_of_storage()
{ }
......

指针分别叫:pointer _M_start;pointer _M_finish;pointer _M_end_of_storage; 之前不叫这个的,现在都改了。。。无所谓了,含义没变。。。

vector在堆上申请了一块内存,用于存放数组元素。用_M_start表示数组开始的位置,用_M_finish数据结束的位置,用_M_end_of_storage 表示这块内存容量结束的位置。

  • vector容量不足时,需要进行扩容,扩容会引发内存拷贝。
  • 当向vector插入、删除某些元素时,由于采用连续存储方式,也会引发内存拷贝。

扩容

刚刚提到了扩容,如果一个vector容量不足了需要扩容,应该扩多少呢?这个根据reserve()的值来决定。如果你没有设置的话,按你存入的数据量进行扩容。如果你设置了这个值,那么每次扩reserve()个。

list

To be countinue…

从一个CPP文件编译成ELF可执行文件过程中会把不同的变量和函数映射到不同的内存区域。这些不同的区域具有不同的访问权限,有的是只读的,有的是读写的,有的是可执行的。让我们举个简单的例子来了解一下。

示例代码

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
#include <stdio.h>

template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
return *static_cast<dst_type*>(static_cast<void*>(&src));
}

int global_uninit_var;
int global_init_var = 255;
const int const_global_int = 255;

void func() {
printf("Just a function\n");
}
static void static_func() {
}
inline void inline_func() {
}

typedef void (*pfunc_t)();
typedef int (*main_func_t)();
typedef void (*static_func_t)();

class Simple {
public:
static void Show() {
printf("I am Show...\n");
}
void localshow() {
printf("I am localshow...\n");
}
};

int main() {
static int static_var = 255;

int local_uninit_var;
int local_init_var = -1;

const int const_local_int = 127;

int* heap_int = new int();

pfunc_t pf = func;
main_func_t mf = main;
static_func_t sf = static_func;
static_func_t csf = Simple::Show;
void* cpf = pointer_cast<void*>(&Simple::localshow);
pfunc_t ipf = inline_func;

printf("global_uninit_var: 0x%x\n", &global_uninit_var);
printf("global_init_var: 0x%x\n", &global_init_var);
printf("static_var: 0x%x\n", &static_var);
printf("const_global_int: 0x%x\n", &const_global_int);
printf("local_uninit_var: 0x%x\n", &local_uninit_var);
printf("local_init_var: 0x%x\n", &local_init_var);
printf("const_local_int: 0x%x\n", &const_local_int);
printf("heap_int: 0x%x, 0x%x\n", &heap_int, heap_int);
printf("point_func: 0x%x, 0x%x\n", &pf, pf);
printf("point_main_func: 0x%x, 0x%x\n", &mf, mf);
printf("static_func: 0x%x, 0x%x\n", &sf, sf);
printf("class_static_func: 0x%x, 0x%x\n", &csf, csf);
printf("class_local_func: 0x%x, 0x%x\n", &cpf, cpf);
printf("inline_func: 0x%x, 0x%x\n", &ipf, ipf);

return 0;
}

由于ELF格式中有很多信息,我们只取readelf --sections相关信息
ELF结构如下:

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
[Nr] Name              Type             Address           Offset       Size              EntSize          Flags  Link  Info  Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002a8 000002a8 000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.bu[...] NOTE 00000000004002c4 000002c4 0000000000000024 0000000000000000 A 0 0 4
[ 3] .note.ABI-tag NOTE 00000000004002e8 000002e8 0000000000000020 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400308 00000308 000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400328 00000328 0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003b8 000003b8 000000000000007d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400436 00000436 000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400448 00000448 0000000000000040 0000000000000000 A 6 2 8
[ 9] .rela.dyn RELA 0000000000400488 00000488 0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004004a0 000004a0 0000000000000078 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000401000 00001000 000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000401020 00001020 0000000000000060 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000401080 00001080 0000000000000392 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000401414 00001414 0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000402000 00002000 000000000000019e 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 00000000004021a0 000021a0 0000000000000064 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000402208 00002208 00000000000001b8 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000403de8 00002de8 0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000403df0 00002df0 0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000403df8 00002df8 0000000000000200 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000403ff8 00002ff8 0000000000000008 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000404000 00003000 0000000000000040 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000404040 00003040 000000000000000c 0000000000000000 WA 0 0 4
[24] .bss NOBITS 000000000040404c 0000304c 000000000000000c 0000000000000000 WA 0 0 4
[25] .comment PROGBITS 0000000000000000 0000304c 000000000000005c 0000000000000001 MS 0 0 1
[26] .debug_aranges PROGBITS 0000000000000000 000030a8 0000000000000070 0000000000000000 0 0 1
[27] .debug_info PROGBITS 0000000000000000 00003118 000000000000031a 0000000000000000 0 0 1
[28] .debug_abbrev PROGBITS 0000000000000000 00003432 00000000000001d9 0000000000000000 0 0 1
[29] .debug_line PROGBITS 0000000000000000 0000360b 0000000000000114 0000000000000000 0 0 1
[30] .debug_str PROGBITS 0000000000000000 0000371f 0000000000000235 0000000000000001 MS 0 0 1
[31] .debug_ranges PROGBITS 0000000000000000 00003954 0000000000000060 0000000000000000 0 0 1
[32] .symtab SYMTAB 0000000000000000 000039b8 0000000000000750 0000000000000018 33 52 8
[33] .strtab STRTAB 0000000000000000 00004108 0000000000000298 0000000000000000 0 0 1
[34] .shstrtab STRTAB 0000000000000000 000043a0 0000000000000151 0000000000000000 0 0 1

将上述代码编译运行打印出的结果对应到elf格式中相应的区域如下:

variable

  • 所有代码无聊作何标记全部存储在.text段,换句话说从汇编的角度可以调用任何函数都可以被调用
  • 全局的const常量存放在.rodata
  • 局部的const常量则存储在栈空间内,而常量的右值127则存放在.text
  • 已经初始化的全局变量或static变量存放在.data
  • 未初始化的全局变量存放在bss