交易收据包含交易的产出(状态和日志)。收据数据存储在状态数据库,根哈希值存储在block的header中。

区块中智能合约的信息存储有2种方式,账户存储和日志存储。

账户存储(Account Storage)定义了智能合约状态以及可访问的合约。在下图所示的state Trie里

日志存储(Log Storage)是用来存储中间状态,这些状态其实并不是给合约使用的,一般是给其他第三方dAPP来访问(比如前后端程序,以及一些分析网站)。日志存储比起账户存储便宜的多。如下图所示的receipt trie

以一个简单的erc721(NFT)合约为例,看看合约是如何存储的:

// 部分代码contract ERC721 is IERC721 {// 存储在 receipt trie 里event Transfer(address indexed from, address indexed to, uint indexed id);event Approval(address indexed owner, address indexed spender, uint indexed id);event ApprovalForAll(address indexed owner,address indexed operator,bool approved);// Mapping from token ID to owner address// 存储在 state trie 里mapping(uint => address) internal _ownerOf;// Mapping owner address to token count// 存储在 state trie 里mapping(address => uint) internal _balanceOf;。。。。function _mint(address to, uint id) internal {require(to != address(0), "mint to zero address");require(_ownerOf[id] == address(0), "already minted");_balanceOf[to]++;_ownerOf[id] = to;emit Transfer(address(0), to, id);}}

Token 所有权(owner)->账户存储:智能合约必须显式的展示谁拥有token。合约能证明谁是拥有者,并提供transfer函数,token id和token owner必须存储在账户存储中

Token 所有权变更历史->日志存储:合约里只需要知道Token当前归谁所拥有。投资分析或其他需求,要想追踪token的所有权变更历史,需要到日志里查找(谁minted或者交易)。

UI 通知->日志存储:当一个NFT被mint的时候,dAPP希望能收到通知,了解详情。因为是异步交易,智能合约没法直接返回结果给前端(或者后端)。所以,需要mint结束的时候,会写到log里,这样dApps可以监听日志,以便获得通知来展示他们。

链下触发器(off chain triggers)->日志存储:如果你需要将你的NFT转给(transfer)其他链(比如你在其他链部署了游戏),你可以新建一个转账(transfer)给网关,交易的时候会记录日志。网关将会获取数据,在其他链mint

智能合约如果想记录日志,代码里可以使用emit,这样就可以将收据记录到日志记录中

contract MyNFTCollection{ // 隐射token ID到拥有者地址 // 存储在Account Storage mapping(uint256 => address) private _owners; // Declaration of Event event Transfer(address indexed _from, address indexed _to, uint256 tokenId);//function _transfer(address from, address to, uint256 tokenId) internal virtual{// written to the log recordsemit Transfer(from, to, tokenId);}}

每个交易只有一个交易收据。在日志里,收据包括 状态,gas 使用量, logs bloom

Status

值是0或1,标记交易是否成功。因为是异步交易,调用者需要等待交易结果,可以通过这个值来判断。

如果gas费不够(也有可能是因为其他原因),整个交易可能会回退。调用者只能读取status,回退的信息无法直接获取。要想获取完整信息需要检查client 节点 traces,它包含了信息的详情和执行。

Gas Used

整个交易的gas使用量

Logs

日志collection。一条日志包括主题(topics)和数据(data)。主题List包括事件签名,indexed 内容。一条日志可以有4个主题。主题总容量受限,最好不要存储大量数据。通常来说,想作为搜索关键字的,可以存储为topic。其他数据存储到data里

以一段简单的代码为例

emit Transfer(from, to, tokenID)

因为 event name, from, to 都是indexed, 下面的查询效率就比较高了

  • 所有Token,今天的交易记录
  • 过去一个小时,这个用户(钱包地址)的销售token记录
  • 上周,这个用户(钱包地址)购买记录

因为tokenID并没有indexed,存储在data域里,类似“这个tokenID今天是否有交易”这种查询,会比较慢

Logs Bloom

查询指定用户(钱包)在某个块(block)里,所有销售的token

我们可以分析日志来完成上面的查询,因为“from”地址是indexed的。但是要完成查询,需要遍历整个块里的交易。

假设块里有500个交易,我们就需要遍历所有交易的logs(包括topics和data)。那我们是否可以创建一个对象,以便我们可以快速查询交易日志里是否有我们需要信息呢?Bloom Filter 就是干这事的。

从一个简单的例子开始,我们来了解Bloom Filter。比如我们要在一个list里查找是否包含某个用户。最简单的方法就是遍历。显而易见,如果数据量大的话,遍历的成本还是比较高的,速度也慢。一个比较简单的方法就是,把所有用户的名字做hash,然后映射到一个地方,如下图

如图所示,最终的映射结构是每个结果的并集。来看看如何查询

bloom filter 能得到2种结果:“可能存在”和“一定不存在”

让我们回到之前的区块例子,假设块中的500个交易,有2个交易符合条件。我们就可以查询logs bloom(topics创建的)是否存在用户账户(地址),如果命中,再到交易中查询详情。这样会大幅减少查询时间。

EVM(eth虚拟机)会汇聚所有 交易级别的 logs bloom ,创建一个块级别的logs bloom 。

查询指定用户(钱包)在过去1天里,所有销售的token

这个查询就需要跨多个块,我们可以在块的bloom filter 里先查找,如果有的话,再到块里找。

Trie(树)结构

现在我们知道了什么是收据,接着来了解一下数据结构

这种组织结构对轻节点友好。因为轻节点只存储了块的header。类似“过去N天,从X钱包转出的交易”,轻节点必须去全节点查询。轻节点blocker header里的merkle tree的root hash,能验证全节点里的tree是否是安全的。

参考:

https://medium.com/coinmonks/ethereum-data-transaction-receipt-trie-and-logs-simplified-30e3ae8dc3cf