Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7612: 通过覆盖树实现的 Verkle 状态转换

描述了如何使用覆盖树来利用 Verkle 树结构,同时保持历史状态不变。

Authors Guillaume Ballet (@gballet), Ansgar Dietrichs (@adietrichs), Ignacio Hagopian (@jsign), Gottfried Herold (@GottfriedHerold), Jamie Lokier (@jlokier), Tanishq Jasoria (@tanishqjasoria), Parithosh Jayanthi (@parithosh), Gabriel Rocheleau (@gabrocheleau), Karim Taam (@matkt)
Created 2024-01-25
Discussion Link https://ethereum-magicians.org/t/ethereum-state-trie-format-change-using-an-overlay/4165
Requires EIP-4762, EIP-6800, EIP-7545

摘要

本 EIP 提出了一种将状态树格式从十六进制 Merkle Patricia 树 (MPT) 切换到 Verkle 树 (VKT) 的方法:MPT 树被冻结,并且对状态的新写入存储在“覆盖”在十六进制 MPT 上的 VKT 中。 历史 MPT 状态保持不变,其最终迁移将在稍后处理。

动机

以太坊状态正在增长,而 VKT 提供了一个很好的缓解策略来阻止这种增长并实现弱无状态性。 由于在访问大型存储的合约时翻译这些合约很困难,因此迁移当前 MPT 状态的提案很复杂,并且需要客户端团队进行漫长的重构代码过程以处理此转换。

状态越大,任何转换过程所需的时间就越长。 这会影响转换发生时以及如果转换是共识的一部分时完整同步链。 核心开发团队广泛使用 Fullsync 来测试新代码的性能。 超过一个月的转换会影响通常以此速率发布的客户端团队的发布时间表。 无法遵循转换的节点需要等待更长时间才能重新加入。 转换也会使重组变慢,因此减少其持续时间是可取的。

目前的提案建议通过激活一个新的“覆盖”VKT 来阻止 MPT 状态的增长,所有新的状态更新都将写入该 VKT。 “基本”MPT 被冻结在适当的位置,直到所有执行客户端都准备好执行完全转换。 数据首先从覆盖树中读取,如果未找到,则从 MPT 中读取。

每当冻结 MPT 的区块最终确定时,可以删除内部节点数据,以便释放磁盘空间。

规范

本文档中的关键词“必须”、“不得”、“必需”、“应”、“不应”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

常量

参数 描述
FORK_TIME TBD 激活覆盖树的时间。

辅助函数

# 确定 `block` 是否为分叉激活区块
def is_fork_block(block):
    return block.parent.timestamp < FORK_TIME && block.timestamp >= FORK_TIME
    
# 将帐户写入 verkle 树
def verkle_set_account(tree: VerkleTree, key: Bytes32, account: Optional[Account]):
    if account is not None:
        basicdata = bytes(0) # 版本
        basicdata += bytes(4) # 保留
        basicdata += len(account.code).to_bytes(3, 'big')
        basicdata += account.nonce.to_bytes(8, 'big')
        basicdata += account.balance.to_bytes(16, 'big')
        tree.set(key, basicdata)
        ckkey = key
        ckkey[31] = CODEHASH_LEAF_KEY
        tree.set(ckkey, account.code_hash)

# 从 verkle 树读取帐户
def verkle_get_account(tree: VerkleTree, key: Bytes32) -> Optional[Account]:
    basicdata_leaf = tree.get(key)
    if basicdata_leaf is not None:
        cs = int.from_bytes(basicdata_leaf[5:8], 'big')
        nonce = int.from_bytes(basicdata_leaf[8:16], 'big')
        balance = int.from_bytes(basicdata_leaf[16:32], 'big')
        ckkey = key
        ckkey[31] = CODEHASH_LEAF_KEY
        ck = tree.get(ckkey)
        cskey = key
        cskey[31] = CODE_SIZE_LEAF_KEY
        cs = tree.get(cskey)
        account = Account(0, balance, nonce, ck, cs)

    return account

执行规范的变更

在执行规范中,按如下方式修改 State 类:

@dataclass
class State:
    """
    包含在事务之间保留的所有信息。
    """

    _main_trie: Trie[Address, Optional[Account]] = field(
        default_factory=lambda: Trie(secured=True, default=None)
    )
    _storage_tries: Dict[Address, Trie[Bytes, U256]] = field(
        default_factory=dict
    )
    _snapshots: List[
        Tuple[
            Trie[Address, Optional[Account]], Dict[Address, Trie[Bytes, U256]]
        ]
    ] = field(default_factory=list)
    _created_accounts: Set[Address] = field(default_factory=set)

    # 此 EIP 中添加
    _overlay_tree: VerkleTree[Address, Bytes32]

状态访问函数按如下方式修改:

def get_account_optional(state: State, address: Address) -> Optional[Account]:
    account = verkle_get_account(state._overlay_tree, get_tree_key_for_version(addr))
    if account is not None:
        return account
    
    return trie_get(state._main_trie, address)

def set_account(state: State, address: Address, account: Optional[Account]) -> None:
    verkle_set_account(state._overlay_tree, get_tree_key_for_nonce(addr), account)

def get_storage(state: State, address: Address, key: Bytes) -> U256:
    value = state._overlay_tree.get(get_tree_key_for_storage_slot(addr, slot))
    if value is not None:
        return value
        
    trie = state._storage_tries.get(address)
    if trie is None:
        return U256(0)

    value = trie_get(trie, key)

    assert isinstance(value, U256)
    return value

def set_storage(
    state: State, address: Address, key: Bytes, value: U256
) -> None:
    state._overlay_tree.set(get_tree_key_for_storage_slot(addr, slot), value)

添加以下函数,该函数用于将合约存储在树中:

def state_set_codechunk(state: State, addr: Address, chunk_num: int, chunk: Bytes):
    state._overlay_tree.set(get_tree_key_for_code_chunk(addr, chunk_num), chunk)

区块头的变更

FORK_TIME,区块头状态根从 MPT 根更改为 VKT 根。

理由

这种方法不会转换状态,这留给后续的 EIP。 这是一种临时措施,以防我们决定将转换本身推迟到以后的时间。 它具有简单性的优点,这意味着 Verge 分叉可以与其他更简单的 EIP 同时发生。 它也不需要在共识层进行任何更改。

向后兼容性

未发现向后兼容性问题。

测试用例

参考实现

  • transition-post-genesis 分支在 github.com/gballet/go-ethereum 中实现了这一点,当在命令行上设置 --override.overlay-stride=0 时。

安全考虑

需要讨论。

版权

通过 CC0 放弃版权及相关权利。

Citation

Please cite this document as:

Guillaume Ballet (@gballet), Ansgar Dietrichs (@adietrichs), Ignacio Hagopian (@jsign), Gottfried Herold (@GottfriedHerold), Jamie Lokier (@jlokier), Tanishq Jasoria (@tanishqjasoria), Parithosh Jayanthi (@parithosh), Gabriel Rocheleau (@gabrocheleau), Karim Taam (@matkt), "EIP-7612: 通过覆盖树实现的 Verkle 状态转换 [DRAFT]," Ethereum Improvement Proposals, no. 7612, January 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7612.