您正在查看: Ethereum-新手教程 分类下的文章

ETH2.0 技术调研和测试部署

1. ETH1.0和2.0的对比

1.1 共识机制

共识改进

  • 节能,比pow消耗更少能源
  • 低门槛,硬件相对较低
  • 降低中心化,pos更易节点参与
  • 增加攻击成本,违规惩罚

1.2 节点组成

2. ETH 2.0 方案组成

2.1 节点架构

  • 执行层->P2P1

    • 维护交易池,交易处理,gossip广播
    • 状态管理
    • 创建执行负载
    • 用户网关->提供对外RPC
  • 共识层->P2P2

    • 共享广播块和证明
    • 运行分叉选择算法
    • 跟踪链的头部
    • 管理信标状态(包含共识和执行信息)
    • 跟踪 RANDAO 中累积的随机性
    • 跟踪justification和finalization
    • 验证器
      • 提议区块
      • 创建证明
      • 累积奖励/惩罚

    客户端设置

2.2 执行与共识层通信

共识和执行层之间通过“Engine-API”的API发送指令。

共享一个 ENR(以太坊节点记录),其中包含(eth1 密钥和 eth2 密钥)。

层内广播协议:gossip

共识层非区块生产者:

共识层-->接收区块->预先验证块(有效sender)->区块中的交易->执行层->执行交易并验证块头中的状态(哈希匹配)->共识层->添加本地链头部->创建证明->广播证明

共识层为区块生产者时:

共识层->收到通知(下一个区块当选生产者)->调用执行层(create block)->交易内存池->打包区块->执行交易生成区块哈希<-共识层抓取交易和区块哈希->添加到信标块->广播区块

3. 环境测试

3.1 测试方案选配

根据客户端多样性分布
https://clientdiversity.org/

测试方案:Prysm棱镜 + Geth

部署参考文档(prylabs

3.2 测试环境

  • 系统: Ubuntu 20.04
    • CPU:4+ 核 @ 2.8+ GHz
    • 内存:16GB+
    • 存储:2TB+ SSD
    • 网络:8 MBit/秒
  • 测试网络: Goerli-Prater
  • 执行客户端: Geth 1.11.3
  • EN-BN 连接: HTTP-JWT

eth2.0测试网

  • Sepolia
    • 封闭验证器集
    • 验证者集受到限制,主要由客户和测试团队监督
    • 较小的区块链状态和历史
    • 同步速度快,需要更少的存储承诺
  • Goerli
    • 开放验证者集
    • 携带大状态并需要高存储承诺
    • 需要更长的时间才能与网络的当前状态同步

如果测试合约选Sepolia, 如果想测试参与节点治理选Goerli

3.3 测试环境部署

设置工作目录

export workdir=/mnt/d/eth2.0/ethereum

3.3.1 安装 Prysm

cd $workdir
mkdir consensus
mkdir execution
cd consensus
mkdir prysm && cd prysm
curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod +x prysm.sh

3.3.2 生成 JWT Secret

./prysm.sh beacon-chain generate-auth-secret

3.3.3 下载和运行执行客户端

下载

cd $workdir/execution
wget https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.3-5ed08c47.tar.gz
tar -zxvf ./geth-linux-amd64-1.11.3-5ed08c47.tar.gz
mv ./geth-linux-amd64-1.11.3-5ed08c47/geth ./
rm ./geth-linux-amd64-1.11.3-5ed08c47.tar.gz
rm -rf ./geth-linux-amd64-1.11.3-5ed08c47

运行

nohup ./geth --goerli --http --http.api eth,net,engine,admin --authrpc.jwtsecret $workdir/consensus/prysm/jwt.hex 2>> ./geth.log &

3.3.4 Prysm 运行信标节点

cd $workdir/consensus/prysm/

nohup ./prysm.sh beacon-chain --execution-endpoint=http://localhost:8551 --prater --jwt-secret=./jwt.hex --suggested-fee-recipient=0x10210572d6b4924Af7Ef946136295e9b209E1FA0 --accept-terms-of-use --checkpoint-sync-url=https://sync-goerli.beaconcha.in --genesis-beacon-api-url=https://sync-goerli.beaconcha.in 2>> ./prysm.log &

3.3.5 Prysm 运行验证器

cd $workdir/
git clone -b master --single-branch https://github.com/ethereum/staking-deposit-cli.git
cd staking-deposit-cli/
./deposit.sh new-mnemonic

按照提示,依次输入参数

Please choose your language ['1. العربية', '2. ελληνικά', '3. English', '4. Français', '5. Bahasa melayu', '6. Italiano', '7. 日本語', '8. 한국어', '9. Português do Brasil', '10. român', '11. Türkçe', '12. 简
体中文']:  [English]: 12
请选择您的助记符语言 ['1. 简体中文', '2. 繁體中文', '3. čeština', '4. English', '5. Italiano', '6. 한국어', '7. Português', '8. Español']:  [简体中文]: 4
请选择您想要运行多少验证者: 1
请选择(主网或测试网)网络名/链名 ['mainnet', 'ropsten', 'goerli', 'sepolia', 'zhejiang']:  [mainnet]: goerli
用来保护您密钥库的密码。您在设置您的 Ethereum PoS 验证者时需要重新输入密码以便解密它们。:
重复输入以确认:

最后生成助记词,保存并输入确认

cool disorder brand vast jaguar sponsor bamboo ...
您的密钥可以在这里找到: /mnt/d/eth2.0/ethereum/staking-deposit-cli/validator_keys

执行完将生成

  1. 新的助记词
  2. validator_keys文件夹
    1. deposit_data-*.json // 上传到以太坊启动板的存款数据
    2. keystore-m_*.json // 公钥和加密的私钥

复制validator_keysconsensus文件夹中

cp -r $workdir/staking-deposit-cli/validator_keys $workdir/consensus
导入密钥库
cd $workdir/consensus/prysm
./prysm.sh validator accounts import --keys-dir=$workdir/consensus/validator_keys --prater

出现以下提示代表成功

INFO accounts: Successfully created new wallet wallet-path=/home/surou/.eth2validators/prysm-wallet-v2
Successfully imported 1 accounts, view all of them by running `accounts list`

导入后钱包位置:$HOME/.eth2validators/prysm-wallet-v2

上传存款数据

转到Goerli-Prater Launchpad 的存款数据上传页面,将上面的deposit_data-*.json 上传,会提示您连接钱包。

此时会存入 32 GETH Prater 测试网的存款合约。

避免重复抵押

提交dapp已经做了的判断

查看发起的抵押交易

goerli.etherscan.io

查看链上合约

goerli.etherscan.io

/// @notice Submit a Phase 0 DepositData object.
    /// @param pubkey A BLS12-381 public key.
    /// @param withdrawal_credentials Commitment to a public key for withdrawals.
    /// @param signature A BLS12-381 signature.
    /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object.
    /// Used as a protection against malformed input.
    function deposit(
        bytes calldata pubkey,
        bytes calldata withdrawal_credentials,
        bytes calldata signature,
        bytes32 deposit_data_root
    ) override external payable {
        // Extended ABI length checks since dynamic types are used.
        require(pubkey.length == 48, "DepositContract: invalid pubkey length");
        require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length");
        require(signature.length == 96, "DepositContract: invalid signature length");

        // Check deposit amount
        require(msg.value >= 1 ether, "DepositContract: deposit value too low");
        require(msg.value % GWEI == 0, "DepositContract: deposit value not multiple of gwei");
        uint deposit_amount = msg.value / GWEI;
        require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high");

        // Emit `DepositEvent` log
        bytes memory amount = to_little_endian_64(uint64(deposit_amount));
        emit DepositEvent(
            pubkey,
            withdrawal_credentials,
            amount,
            signature,
            to_little_endian_64(uint64(deposit_count))
        );

        // Compute deposit data root (`DepositData` hash tree root)
        bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0)));
        bytes32 signature_root = sha256(abi.encodePacked(
            sha256(abi.encodePacked(signature[:64])),
            sha256(abi.encodePacked(signature[64:], bytes32(0)))
        ));
        bytes32 node = sha256(abi.encodePacked(
            sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)),
            sha256(abi.encodePacked(amount, bytes24(0), signature_root))
        ));

        // Verify computed and expected deposit data roots match
        require(node == deposit_data_root, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root");

        // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`)
        require(deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full");

        // Add deposit data root to Merkle tree (update a single `branch` node)
        deposit_count += 1;
        uint size = deposit_count;
        for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) {
            if ((size & 1) == 1) {
                branch[height] = node;
                return;
            }
            node = sha256(abi.encodePacked(branch[height], node));
            size /= 2;
        }
        // As the loop should always end prematurely with the `return` statement,
        // this code should be unreachable. We assert `false` just to be safe.
        assert(false);
    }

合约内没有限定质押地址,可以代为质押

交易参数如下
0   pubkey  bytes   0xb23fca698e5b57c2dd09f16e10b97d532a18b574a46e6539bd9d83e79c5922230bc8990def676e3e78534a3fd8cbe58d
1   withdrawal_credentials  bytes   0x004734640dd34942fc621f643910f86e52b4e5f03db42d9cf6e14863b1148529
2   signature   bytes   0xb46ac23ab45b798331d706b91a635f59b00019cff96462493fae91877dc18d2c6d30e2684197547cd48c079ad94265ff07f36ab48b61b94ef90e3e7e3eeaf96d52dbb012690113c4fe3bc42274a9eb72d82dbdcbea8aa08e2e00e88978c0de3e
3   deposit_data_root   bytes32 0x0c4b49597ef312f110596b87e333e4c76ef8381216dd52ca63f2c84ea2c152d8

信标链处理您的存款大约需要 16-24 小时。 一旦存入金额总计达到 32 ETH,该验证器就有资格被激活。

查看已抵押信息

beaconcha.in

当前测试抵押节点信息

beaconscan.com
质押生效后才能看到信息

运行验证器

nohup ./prysm.sh validator --wallet-dir=$HOME/.eth2validators/prysm-wallet-v2 --wallet-password-file ./password --prater 2>> ./validator.log &

检查节点和验证器状态

节点日志

追块过程中
  • Geth常规日志
INFO [03-11|16:46:14.726] Syncing: chain download in progress      synced=44.90% ...
INFO [03-11|16:46:15.318] Forkchoice requested sync to new head    number=8,633,606 hash=522f76..69cb70 finalized=8,633,551
INFO [03-11|16:46:15.859] Syncing: state download in progress      synced=10.27% s
  • prysm常规日志
[2023-03-11 16:47:40]  INFO blockchain: Called new payload with optimistic block payloadBlockHash=0xb7864552d082 slot=5166177
  • validator日志
[2023-03-11 16:48:55]  INFO validator: Waiting for beacon node to sync to latest chain head
追齐高度后
生效之前

validator日志显示

INFO validator: Waiting for deposit to be observed by beacon node pubKey=0xb23fca698e5b status=UNKNOWN_STATUS

prysm日志显示

 WARN rpc/validator: Not connected to ETH1. Cannot determine validator ETH1 deposit block number
生效之后

// 待补充

怠工惩罚

如果当前节点质押生效后,未能及时提议区块和证明,将被惩罚 -10680 GWei/epoch

4. 附加配置

开放必要端口

执行客户端 默认端口 共识客户端 默认端口
Geth 30303 TCP/UDP Teku 9000 TCP/UDP
Besu 30303 TCP/UDP Lighthouse 9000 TCP/UDP
Erigon 30303 TCP/UDP Nimbus 9000 TCP/UDP
Nethermind 30303 TCP/UDP Prysm 13000 TCP, 12000 UDP

配置时间同步

timedatectl // 检查是否NTP服务是积极的.
sudo timedatectl set-ntp on // 如果NTP服务不是积极的

5. 在线文档

节点设置

费用收件人

Lighthouse | Nimbus | Prysm | Teku

Discord

JWT认证

最新公告

Goerli-PraterMainnet

监控

Lighthouse | Nimbus | Prysm | Teku

安全最佳实践

https://docs.prylabs.network/docs/security-best-practices

Prysm社区

邮件列表Prysm Discord 服务器r/ethstakerEthStaker Discord 服务器

6. 节点运行架构

以太坊节点

公共检查点

https://notes.ethereum.org/@launchpad/checkpoint-sync

节点统计

https://www.ethernodes.org/

节点质押

goerli: https://goerli.launchpad.ethereum.org/zh

mainnet: https://launchpad.ethereum.org/zh

生产服务器配置

最低要求
  • 2 核以上的 CPU
  • 8 GB 内存
  • 700GB 可用磁盘空间
  • 10+ MBit/s 带宽
推荐规格
  • 具有 4 个以上内核的快速 CPU
  • 16 GB+ 内存
  • 1+TB 的快速 SSD
  • 25+ MBit/s 带宽

测试网

即插即用解决方案

https://docs.dappnode.io/user/quick-start/first-steps/

https://github.com/NiceNode/nice-node

https://eth-docker.net/

https://docs.sedge.nethermind.io/docs/intro

实例验证 - call delegatecall

实例验证下call,delegatecall,两种方式下msg.sender和数据storage存储位置的区别

合约A

pragma solidity ^0.8.19;

contract A {
    address public temp1;
    uint256 public temp2;

    function three_call(address addr) public {
        (bool success, bytes memory result) = addr.call(
            abi.encodeWithSignature("test()")
        ); // 1
        require(success, "The call to B contract failed");
    }

    function three_delegatecall(address addr) public {
        (bool success, bytes memory result) = addr.delegatecall(
            abi.encodeWithSignature("test()")
        ); // 2
        require(success, "The delegatecall to B contract failed");
    }
}

合约B

pragma solidity ^0.8.19;

contract B {
    address public temp1;
    uint256 public temp2;

    function test() public  {
        temp1 = msg.sender;
        temp2 = 100;
    }
}

call 测试

B合约地址:0x086866663330344C7D1C51Bf19FF981AF3cB5782
A合约地址:0x05715D87C062B9685DD877d307b584bAbec964Ed
交易发起地址:0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776

执行前数据

A B
temp1 0x0000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000
temp2 0 0

A合约执行three_call,参数为B合约地址
执行后数据

A B
temp1 0x0000000000000000000000000000000000000000 0x05715D87C062B9685DD877d307b584bAbec964Ed
temp2 0 100

delegatecall 测试

B合约地址:0x27153EDA2E085534811b040f6062f6528D6B80a1
A合约地址:0xd3C23F354Ca2160E6dC168564AB8954146cF35C9
交易发起地址:0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776

执行前数据

A B
temp1 0x0000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000
temp2 0 0

A合约执行three_delegatecall,参数为B合约地址
执行后数据

A B
temp1 address: 0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776 0x0000000000000000000000000000000000000000
temp2 100 0

总结

  • A发起地址->B合约call->C合约
    msg.sender为B合约地址,非原始A发起地址,数据保存在C合约中
  • A地址->B合约delegatecall->C合约
    msg.sender为A发起地址,数据保存在B合约中

注意

callcode 已经在solidity 0.5+废弃,被delegatecall取代,故不做分析测试

TypeError: "callcode" has been deprecated in favour of "delegatecall".

参考
https://hicoldcat.com/posts/web3/senior-track-5/

实例验证 - 访问私有数据

方案的具体讲解来自《智能合约安全审计入门篇 —— 访问私有数据》,本片文章将在QEasyWeb3测试链上进行数据验证

测试合约

contract Vault {
    uint256 public count = 123;
    address public owner = msg.sender;
    bool public isTrue = true;
    uint16 public u16 = 31;
    bytes32 private password;
    bytes32[3] public data;
    struct User {
        uint256 id;
        bytes32 password;
    }
    User[] private users;
    mapping(uint256 => User) private idToUser;

    constructor(bytes32 _password) {
        password = _password;
    }

    function addUser(bytes32 _password) public {
        User memory user = User({id: users.length, password: _password});
        users.push(user);
        idToUser[user.id] = user;
    }
}

部署合约

通过https://remix.ethereum.org/ 进行部署,具体操作不做详细讲解

网络配置

QEasyWeb3的网络配置,可以查看《QEasyChain 测试链信息》

类型 RPC
https https://qeasyweb3.com
http http://qeasyweb3.com

chain id: 9528

合约操作

合约部署时,构造函数参数bytes32 _password,需要传入初始化参数,用于写入测试状态变量password
例如:0x4141414242424343430000000000000000000000000000000000000000000001

然后通过addUser再添加两个测试数据

  • 0x4141414242424343430000000000000000000000000000000000000000000002
  • 0x4141414242424343430000000000000000000000000000000000000000000003

执行完成后,开始slot的读取测试。

读取私有slot数据

为了简化操作,我们直接使用python3 + web3py进行测试脚本的编写, 也可以选用其他web3库,大同小异
我们上面合约部署完的合约地址是0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904

测试脚本

import time
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('http://qeasyweb3.com'))

if __name__ == "__main__":
    for i in range(0,7):
        print("slot"+str(i)+" = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904",hex(i))))

执行后返回数据如下

slot0 = 0x000000000000000000000000000000000000000000000000000000000000007b
slot1 = 0x000000000000000000001f016bc0e9c6a939f8f6d3413091738665ad1d7d2776
slot2 = 0x4141414242424343430000000000000000000000000000000000000000000001
slot3 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot4 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot5 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot6 = 0x0000000000000000000000000000000000000000000000000000000000000002
  • slot0
    存储的为状态变量count的值,等于123的十六进制
  • slot1
    由于存储紧凑原则,当前slot存储了3部署数据,从右往左依次是
    • 状态变量owner的值,等于用于部署合约的msg.sender=0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776
    • 状态变量isTrue = 01 = true
    • 状态变量u16 = 1f = 31
  • slot2
    存储的为状态变量password
  • slot3
    存储的为状态变量data第1个存储值 = data[0]
  • slot4
    存储的为状态变量data第2个存储值 = data[1]
  • slot5
    存储的为状态变量data第3个存储值 = data[2]
  • slot6
    存储的为变长数组状态变量users的长度,当前已执行两次addUser

查看私有变长数组数据

上面我们从slot6中查询到了变长数组users的长度,下面我们继续查看users中存储的数据。

我们先再次回顾下变长数组的存储原则
对于变长数组,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值

对应的代码逻辑

length = sload(slotA)
slotV = keccak256(slotA) + index// 索引下标
value = sload(slotV)

依据变长数组的存储以及紧凑打包的原则,所以对于前面两次addUser的数据存储槽位置为

  • user1.id == keccak256(slotA) + 0
  • user1.password == keccak256(slotA) + 1
  • user2.id == keccak256(slotA) + 2
  • user2.password == keccak256(slotA) + 3

我们继续使用python3 + web3py进行验证
先计算keccak256(slotA)

print("slot6-keccak = " +w3.toHex(Web3.keccak(hexstr="0x0000000000000000000000000000000000000000000000000000000000000006")))

得到

0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f

所以user1和user2的存储槽位置为

  • user1.id == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f
  • user1.password == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40
  • user2.id == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41
  • user2.password == 0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42

通过测试脚本,获取上面存储槽位置的值

print("user1.id = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f")))
print("user1.password = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40")))
print("user2.id = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41")))
print("user2.password = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42")))

得到数据如下

user1.id = 0x0000000000000000000000000000000000000000000000000000000000000000
user1.password = 0x4141414242424343430000000000000000000000000000000000000000000002
user2.id = 0x0000000000000000000000000000000000000000000000000000000000000001
user2.password = 0x4141414242424343430000000000000000000000000000000000000000000003

经测试与前面两次addUser的数据一致

总结

对于定长数组data,每个元素单独一个存储槽,所以data状态变量依次为slot3-5
对于变长数组users,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值

hardhat config 中optimizer 优化选项

启用optimizer (优化器)后, 优化器会尝试去优化代码,如简化复杂的表达式等,例如常量表达式可能直接优化出计算,以便减少代码大小和执行成本。

runs 很多人有误解,它不是指优化器运行多少次,而是指合约函数未来可能会调用多少次,例如某些锁仓合约只会调用一次,可以将runs值设置为1。 这样编译器会尽可能小的字节码, 减少部署成本(运行时可能会更费 gas)。
如果合约会大量运行,如 token 合约,就可以设为较大的数值,编译器会优化以便在运行时尽可能低 gas 消耗,不过部署时的字节码可能大一些。