Node.js core collaborator emeriti; LearningBooks && codingMonkey; NFT Collector;

以太坊黄皮书公式解析(上)
前言与版本笔者最近在结合以太坊黄皮书读以太坊源码,结合自己的理解解析下黄皮书内的公式,与大家共同学习进步,若大家在阅读以太坊黄皮书时,对公式产生理解上的困惑,可以参阅本文一起看。文章基于当前(2022/1/10)的黄皮书,版本号为 BERLIN VERSION fabef25 ,若有不准确之处,欢迎指出。由于该黄皮书内除附录外有 183 个公式,为了让文章篇幅不过长,该解析会由三部分组成一个系列,每个系列解析约 60 个公式,本文为上篇。公式解析(1)σ 为以太坊世界状态。Υ 为以太坊状态转换函数。T 为一个交易。上述公式阐述的是,以太坊是一个基于交易而改变状态的状态机。即每进来一个交易,都会改变一次以太坊的旧世界状态,从而进入一个新世界状态。(3)B 为一个区块。T0, T1, ... 为一组交易。在实际运作时,基于效率考量,以太坊是批量处理交易的,一个批次的交易,会被打包在一个区块中,这就是 (3) 公式的含义。(2)Π 表示区块层面上的状态转换函数。上述公式即是 (1) 的批量处理版本,以太坊的世界状态通过区块(即一组交易)批量更新。(4)Ω 为区块确定性状态转换函数,会奖...

交易所模式总结
交易所是市场的 DNA 。交易所的内部运行模式,决定了它下面所有的交易动态。 目前交易所提供流动性(liquidity)的方式,主要有四种:中央限价订单簿(CLOB, 即 central limit order books)联合曲线自动化做市(bonding curve automated market maker)询价单(RFQ,即 requests for quote)拍卖(auction)每种模式各有自己的优缺点,它们需要在以下这些特性优先级中做出权衡:价格发现(price discovery,匹配的买卖双方发现对方的难易程度)流动性(liquidity,资产买入和卖出的方便程度)滑点(slippage,订单请求水平和订单实际执行价格的差额)对价格波动的反应(responsiveness to volatility,对资产价格波动的响应速度)做市商的资本效率(capital efficiency for market makers,做市商的资本可否被最优化配置)可操作性(manipulability,影响资产价格的能力)可组合性(composability,组合资产的能力)...
[译]对于 zk-SNARKs 运行的简介
在过去十年中,普适的简洁零知识证明或许是密码学领域最有影响力的发展方向之一,通常简称为 zk-SNARKs(zero knowledge succinct arguments of knowledge)。zk-SNARKs 可以让你为执行的计算生成一些证明(proof),虽然这些证明的生成可能会耗费非常多的算力,但是却可以被很快地验证。并且这些证明还拥有“零知识”的特性,即证明中会隐藏计算中的输入信息。 举个例子,你可以给出一个关于你确实知道某个密码数字的证明:你会把这个数字加到字符 cow 后面,然后执行 100 万次 SHA256 哈希,最终结果会以 0x57d00485aa 开头。在 zk-SNARKs 的应用场景里,验证者可以以远比执行 100 万次哈希快的时间验证你是否确实知道你所说的密码数字,并且该数字不会暴露给验证者。 在区块链中,zk-SNARKs 有两个非常有用的应用场景:可扩展性(scalability):如果一个区块需要花很多时间才能被验证,那么某人可以预先为其生成证明,然后其他人只需要快速地验证生成的证明即可隐私性(Privacy):你可以在隐藏具体收到资...

Subscribe to DavidCai.eth
<100 subscribers
Share Dialog

昨日冷兔预售,除了成为国产 NFT 之光冲上 OpenSea 时段榜一之外,不知大家是否察觉,整个预售过程,Gas 费并没有明显暴涨:

可以看到,整个下午的 Gas Price 在图中并没有明显尖刺(和当天晚上,以及后一天凌晨对比明显)。在项目如此之热的情况下,冷兔是如何做到的呢?让我们从它的合约代码来一探究竟:
// XRC.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "./ERC721A.sol";
contract XRC is ERC721A, Ownable, ReentrancyGuard {
using ECDSA for bytes32;
// ...
function presaleMint(
uint256 amount,
string calldata salt,
bytes calldata token
) external payable {
require(status == Status.PreSale, "XRC: Presale is not active.");
require(
tx.origin == msg.sender,
"XRC: contract is not allowed to mint."
);
require(_verify(_hash(salt, msg.sender), token), "XRC: Invalid token.");
require(
numberMinted(msg.sender) + amount <= maxMint,
"XRC: Max mint amount per wallet exceeded."
);
require(
totalSupply() + amount + reserveAmount - tokensReserved <=
collectionSize,
"XRC: Max supply exceeded."
);
_safeMint(msg.sender, amount);
refundIfOver(PRICE * amount);
emit Minted(msg.sender, amount);
}
// ...
}
可以看到,整个合约多重继承了 Ownable,ReentrancyGuard 和 ERC721A 。前两个合约都是来自大家常用的OpenZeppelin,分别用于控制部分关键函数的调用权限和防止重入攻击,而第三个继承的合约: ERC721A ,即是 presaleMint 函数的关键部分 _safeMint(msg.sender, amount); 的实现之处。
其实,ERC721A 也是对 @openzeppelin/IERC721 的一个实现,相比于 OpenZeppelin 自带的实现,优化了 mint 时的 Gas 开销。在 5 天前的 Azuki 项目 mint 时,首次使用。我们在 Azuki 的合约中也能看到它:
// Azuki.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./ERC721A.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract Azuki is Ownable, ERC721A, ReentrancyGuard {
// ...
function publicSaleMint(uint256 quantity, uint256 callerPublicSaleKey)
external
payable
callerIsUser
{
// ...
_safeMint(msg.sender, quantity);
refundIfOver(publicPrice * quantity);
}
}
那么 ERC721A 相比大家常用的 @openzeppelin/ERC721Enumerable ,具体在哪里做了优化呢?让我们对比它们的源码来一探究竟。
我们知道,以太坊中的 storage 存储是昂贵的,并且,在以太坊中,调用不修改合约状态的只读函数(view / pure)是免费的。而在 @openzeppelin/ERC721Enumerable 实现中,为了方便读取 NFT 的所有者信息,做了许多冗余的元数据存储,作为代价,在 mint 函数内,则需要额外的开销来存储这些信息。而 ERC721A 实现则相反,将所占的必须存储压缩到了最小,这样虽然增加了读取操作的复杂度,但是,读取是免费的。我们具体先看 @openzeppelin/ERC721Enumerable 中所用的存储:
abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {
mapping(address => mapping(uint256 => uint256)) private _ownedTokens;
mapping(uint256 => uint256) private _ownedTokensIndex;
uint256[] private _allTokens;
mapping(uint256 => uint256) private _allTokensIndex;
// ...
}
_ownedTokens 是钱包地址到另一个 map 的映射,另一个 map 表示用户拥有的第 N 个该 NFT 的 ID 。即 _ownedTokens['addr1'][0] = 201 表示,addr1 这个钱包地址,拥有的第一个该 NFT 的 ID 是 201 。
_ownedTokensIndex 保存了该 NFT ID 到用户拥有索引的映射。即 _ownedTokensIndex[201] = 0 表示 ID 为 201 的该 NFT 是所属用户的拥有列表中的第一个。
_allTokens 表示了所以被 mint 出来的该 NFT 的 ID 列表。
_allTokensIndex 表示了具体某个 ID 的 NFT 在 _allTokens 列表中的位置。
我们可以看到上面四个存储的数据中,有两个(*Index)数据都是另两个数据的索引,若读取开销为免费的话,则它们(*Index)是冗余的,可以通过遍历来实现同样的效果。
而在 ERC721A 的实现中,去除了那两个冗余索引:
contract ERC721A is
Context,
ERC165,
IERC721,
IERC721Metadata,
IERC721Enumerable
{
struct TokenOwnership {
address addr;
uint64 startTimestamp;
}
struct AddressData {
uint128 balance;
uint128 numberMinted;
}
mapping(uint256 => TokenOwnership) private _ownerships;
mapping(address => AddressData) private _addressData;
// ...
}
可以看到,仅做了 ID => 钱包地址,钱包地址 => 所有数量,这两个映射的存储。
在 @openzeppelin/ERC721Enumerable 实现中 mint 只支持单个,一次 mint 多个需具体 NFT 合约自行通过多次调用来实现:
// ERC721Enumerable 使用的是 @openzeppelin/ERC721 中的 _safeMint
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
// ...
function _safeMint(
address to,
uint256 tokenId,
bytes memory _data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, _data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
// ...
}
这就意味着,如果一次 mint N 个,合约中的元数据会被进行 N 次改写,例如,上文中的 _allTokens 会被在尾部进行 N 次 push。而 ERC721A ,则支持批量 mint ,并且通过其特制的数据结构(后文会细述),只需要对元数据进行一次修改:
contract ERC721A is
Context,
ERC165,
IERC721,
IERC721Metadata,
IERC721Enumerable
{
function _safeMint(
address to,
uint256 quantity, // 支持批量 mint
bytes memory _data
) internal {
// ...
}
}
ERC721A 使用的数据结构假设每个用户所 mint 的 ID 是连续的。所以每次批量 mint ,都只会记录一下用户的第一个 mint 出来的该 NFT ID ,以及当前使用的 NFT 计数即可。举个例子:有 A, B,C 三个地址分别进了 mint 后,A 拥有 101,102,103 号 NFT,B 用户 104,105号 NFT,C 只拥有 106 号 NFT,那么储存的数据便是:
#101:A
#102:空
#103:空
#104:B
#105:空
#106:C
当前已使用 NFT 计数为 106 。
位置 102,103,105,并不会存储任何数据,由于之前定义的前提,用户的 mint 是连续的,我们也可以知道它们的所有者。具体实现为:
contract ERC721A is
Context,
ERC165,
IERC721,
IERC721Metadata,
IERC721Enumerable
{
function _safeMint(
address to,
uint256 quantity, // 支持批量 mint
bytes memory _data
) internal {
uint256 startTokenId = currentIndex;
// ...
// 1)这里仅记录了第一个 ID
_ownerships[startTokenId] = TokenOwnership(to, uint64(block.timestamp));
uint256 updatedIndex = startTokenId;
for (uint256 i = 0; i < quantity; i++) {
emit Transfer(address(0), to, updatedIndex);
// ...
updatedIndex++;
}
// 2)更新了计数
currentIndex = updatedIndex;
}
}
这样一来,ERC721A 做到了就把对 storage 的写入从 O(N) 优化到了 O(1) 。单次 mint 的数量越多,优化效果则越明显。
根据 Azuki 官方给出的试验效果,同样印证了我们刚才得出的“把对 storage 的写入从 O(N) 优化到了 O(1)”的结论:
