在以太坊上使用SNARK生成安全随机数

  • Paradigm
  • 发布于 2023-01-13 22:19
  • 阅读 66

本文提出了一种利用SNARK和VDF技术在以太坊上实现完全安全随机性的设计和参考实现,探讨了当前随机数生成服务的缺陷,并提出了以RANDAO和SNARK块哈希预言机为基础的随机信标方案。通过消除偏差和延迟问题,本文旨在为以太坊的去中心化应用提供一个考虑周全、实用的随机数解决方案。

大纲

引言

在这篇文章中,我们提出了利用 SNARKs 和 VDFs 达成 以太坊上的完全安全随机性 的设计和参考实现。

以太坊上的去中心化应用程序,如 NFT 铸造、彩票、游戏等,需要安全的随机性,以确保它们的合约是公平的。尖端的随机性服务虽然满足了一些安全随机性的要求,但仍然存在缺陷,例如成本、偏差和活性。

什么使随机性安全?

为了使随机源安全,它需要具备以下四个特性:

  • 不可预测:随机值无法预测。
  • 不可偏倚:参与随机生成的角色无法有意义地影响结果。
  • 可验证:任何感兴趣的角色都可以验证随机数生成的完整性。
  • 活跃:随机请求可以在未来的任何时间点满足,而不需要任何特定角色的参与,假定基础层的活性。

此外,理想的随机源应该对消费者是 便宜或免费的,以便于使用。

请查看以下表格中的 加粗 贡献:

之前做了什么?

使用区块的哈希

块哈希是检索伪随机值的一种简单、零延迟机制,适合低风险环境,但并不安全。

历史块哈希是可预测的,因为它们已被公开暴露给整个网络。如果攻击者能够预测将要使用的随机值,他们可能能够确定并操纵特定情况的结果,例如随机彩票的结果。因此,安全随机性信标承诺一个未知的未来值,如未来块哈希,以确保不可预测性。

在后面的博客文章中,所有的随机性信标构建都需要承诺未来未知的值,即使没有明确说明。

下面这个未优化的例子演示了承诺未来块的概念:

上述方案是不可预测和可验证的,但它是可偏倚的,因为给定插槽的块提议者可以通过操纵块体直接操纵块的哈希值。

由于 BLOCKHASH 操作码的限制,它还面临活性问题。BLOCKHASH 操作码只能访问最后 256 个块的哈希值,这要求应用程序必须在有限的时间范围内使用承诺的块哈希。

Chainlink VRF

Chainlink 的预言机使用可验证随机函数(由 [Micali 等人](https://people.csail.mit.edu/silvio/Selected%20Scientific%20Papers/Pseudo%20Randomness/Verifiable_Random_Functions.pdf)首次提出)在链上提供不可预测和可验证的随机值

然而,偏倚仍然存在,因为 Chainlink VRF 操作员如果生成的随机性不利,可以通过省略来审查请求。通过引入 斩决(由 Chainlink 部署)可以增加偏移的成本,但在使用 VRF 的系统中仍可能存在随机值,这可能使偏置成为一种盈利的策略。VRF 操作员组的参与理想上应为无权限,但这也可能通过质押/斩决来解决。

而且,将 Chainlink VRF 集成理想上应是免费的,可以在多个请求中摊销,而当前设计由于节点操作员的激励不对称而产生额外的天然Gas费用和原生费用。

信标链 RANDAO

RANDAO 是一个由以太坊的块提议者通过一种经济激励的 承诺-揭示 游戏生成的原生伪随机值。更详细地说,RANDAO 是一个累加器,收集来自块提议者的随机性。在每个区块中,提议者包括一个称为 randao_reveal 的确定性值,表示他们对整体 RANDAO 值的各自贡献。新 RANDAO 值通过将前一个 RANDAO 值与提议块的 randao_reveal 混合来确定,如下代码片段所示。

_感谢 Ben Edgington,代码片段取自 这里。_

process_randao 函数首先验证 randao_reveal 然后进行混合。randao_reveal 被设计为确定性的,因此块提议者无法贡献任意值来影响最终的 RANDAO 值。对于某个插槽的 randao_reveal 需要是该插槽的块提议者对时代编号的签名,这承诺他们对随机性贡献的唯一、允许项。如果块提议者创建一个不同的 randao_reveal 的块,则该块被视为无效,并且不会进行混合。

尽管块提议者不能选择任意的 randao_reveal 值来影响 RANDAO,但如果生成的 RANDAO 不利,他们可以选择不提议一个块。在这种情况下,下一个块将由下一个块提议者创建,这将因为新的块提议者的不同 randao_reveal 的贡献而导致不同的 RANDAO 值。这种省略块的能力使块提议者有机会“重新投掷”随机结果,这可能是不公平的。这是一种通过省略形成的偏差,通常被称为对 RANDAO 的 影响位

通过将 RANDAO 与块哈希示例中的“承诺未来值”技术结合,我们可以创建一种随机性的来源,既不可预测又可验证,同时消除了对 Chainlink 等第三方网络的依赖。此外,这种方法产生的随机值相比于简单使用块哈希较不易受到偏倚。然而,需要注意的是,仅使用未来 RANDAO 仍然容易受到偏见,并且存在活性问题。

偏差性

如前所述,如果生成的 RANDAO 值不利,块提议者可以选择省略一个块。

活性

当前 RANDAO 值可以通过 DIFFICULTY 操作码在执行层访问,该操作码仅返回当前交易所在块的 RANDAO。

直接利用区块的 RANDAO 的这种简单方法消除活性问题,因为 RANDAO 值将始终可用。然而,它仍然存在类似使用历史块哈希时遇到的可预测性问题。为了解决这个可预测性问题,我们可以承诺一个未来块的 RANDAO,并在块提议后引用它,一旦该 RANDAO 可用。

然而,使用事先承诺的未来 RANDAO 块引入活性问题,因为 DIFFICULTY 操作码仅返回当前的 RANDAO 值。这 需要 在之前承诺的确切块中包含一笔交易,这可能很难保证,并且如果被遗漏就无法恢复。

我们能做得更好吗

放宽相同区块 DIFFICULTY 操作码限制

如前所述,通过 DIFFICULTY 操作码访问 RANDAO 并不实用,因为消费随机性的交易必须包含在其之前承诺的同一块中。在本节中,我们将探索克服此限制的方法。

获取 RANDAO 的一种替代方法是通过区块头中的 mixHash 字段。区块头是包含有关区块的元数据的数据结构,例如其高度(number),前一个区块的哈希(parentHash),RANDAO 值(mixHash)等。

我们可以创建一个结构,使得我们能够通过引用历史区块头数据来验证和参考历史 RANDAO 值,而不依赖于 DIFFICULTY 操作码。

要验证历史区块头数据,可以利用相关区块的块哈希。块哈希作为对相关区块头数据的承诺,是块的唯一标识符。通过块哈希,可以验证区块及其内容的完整性,因为对区块头数据的任何更改都会导致不同的块哈希。

要从以太坊的区块头中导出块哈希,你可以:

  1. 获取某个特定块的 区块头数据
  2. RLP(递归长度前缀)编码 区块头数据为二进制字符串。
  3. 使用 Keccak256 对 RLP 编码的区块头进行哈希计算,并计算出块哈希。

以下是一个用以太坊区块头及其区块哈希证明历史 RANDAO 的示例函数:

该函数首先使用 Keccak256 对输入的 RLP 编码区块头进行哈希并验证计算出的块哈希是否引用有效的历史区块。如果没有这个检查,就有可能将过去的假区块头传递进来,并虚假证明历史区块头数据。验证区块头后,函数再对编码的区块头进行 RLP 解码,并将 RANDAO 值存入存储中,以供未来参考。

在智能合约中,有现成的开源 Solidity 库可用于 RLP 解码。但是,验证链上任意历史区块哈希作为真实历史区块哈希是一项更复杂的任务。

验证历史块哈希的最简单方法是 BLOCKHASH 操作码,这允许合约引用最近的 256 个块的哈希。这使我们能够证明过去 256 个块内的任何区块头数据,从而放宽了 DIFFICULTY 操作码所呈现的相同区块回望限制。尽管 BLOCKHASH 操作码允许我们引用比 DIFFICULTY 操作码更多的 RANDAO 值,但它并未完全解决在审查或拥堵情况下的活性问题。在这种情况下,使用 BLOCKHASH 操作码的交易可能会与所需的哈希块相去太远。

要创建一个完全活跃的随机性信标,我们需要一个没有硬性 256 块回望限制的结构。在本博客文章的剩余部分中,我们将 “验证历史区块哈希的构建” 称为“区块哈希预言机”。

放宽 256 个区块哈希回望限制

通过 BLOCKHASH 操作码实现的区块哈希预言机合约

克服 BLOCKHASH 操作码限定回望范围的一种方法是创建一个专门用于存储历史块哈希的合约,同时仍使用 BLOCKHASH 操作码。这允许消费者在不显著提高复杂性的情况下引用更广泛的历史区块头数据。

即使只有单独的一方满足块哈希请求,一旦它们被存储在合约中,任何消费者都可以在未来的任何时间点引用该值。然而,使用 BLOCKHASH 操作码仍会对可以引用的块数量施加限制,这使得请求的块哈希可能会错过时机而无法在合约中存储。因此,这种方法并未提供完全的活性,尽管它允许访问更多的块哈希。

通过 SNARKs 实现的区块哈希预言机合约

由于在区块头中包含对父区块哈希的引用,理论上可以引用历史上任意遥远的块。然而,随着你向历史深处推进,通过父哈希的逐层证明来证明历史块哈希成本以线性方式增长,并且会迅速变得代价高昂。解决此问题的一种方法是使用 SNARKs(具有简洁性特性)创建一个固定大小的历史块哈希的证明。利用这些历史证明,可以创建一个完全活跃且稳定的区块哈希预言机。

一种扩展 BLOCKHASH 操作码预言机的方法是创建一个 SNARK 来证明Merkle根代表一个有序块哈希的承诺,这些哈希在各自块的头部通过 parentHash 互相引用,列表中最后一个块哈希是最新的。

证明代表有效、有序块哈希的Merkle根的 SNARK 可以由离线证明者生成,并发送给验证合约。合约验证该证明,这确保Merkle根与上述的承诺方案匹配,并单独确认列表最后的块哈希对应于合约之前验证的现有块哈希。如果没有将列表锚定到先前验证过的块哈希,就有可能证明一个不准确反映以太坊真实状态的任意顺序块头列表。如果证明和列表中的最后一个块被确认有效,合约将在存储中锁定块哈希的Merkle根。利用这种结构,覆盖范围内的任何 RANDAO 值都可以满足。

通过此方法,我们可以保守地证明 ~1024 个块哈希(n=10)在一次证明中,随着证明系统的不断成熟,可能会有更多。我们可以通过反复执行这个过程,直至覆盖我们想要访问的范围,从而可以很远地回溯历史。

假设这些组件是开源的,任何参与者都可以合理地通过生成并提交大型区块头间隔的证明来保持系统的活性。

现在,我们有了一个由 SNARK 支持的区块哈希预言机,具备强大的活性保证,但仍然存在 RANDAO 的偏倚性问题。我们将在下一节中讨论解决方案。

使用 VDF 消除偏差

如上面的 process_randao 代码片段所示,计算新的 RANDAO 值基本上只是一个 xor 操作。RANDAO 可能存在偏倚,因为在分配的插槽上,块提议者可以 快速 计算 x 或操作的结果,预测他们对随机生成过程的贡献结果,并选择不提议他们的块(如果结果不利)。可以在 RANDAO 上应用一个可验证延迟函数(VDF),以确保块提议者在他们需要贡献的时间内无法知道他们的贡献结果,这意味着他们跳过他们插槽的能力不会影响随机结果。

可验证延迟函数(VDF),由 Boneh 等人 提出的密码学函数,要求计算其输出需要一定时间(“延迟”),但一旦生成后可以轻松验证(“可验证”)。VDF 的例子包括用 SHA256 迭代哈希输入或使用 MinRoot 进行迭代代数变换以计算输出。VDF 设计为计算密集型,且只能顺序执行,因此很难让拥有并行处理能力的对手显著快于具有适当硬件的诚实参与者。

我们可以结合两个先前的想法,Justin Drake 的 RANDAO+VDF 构建 和类似于 StarkWare 的 VeeDoNova 驱动的 minRoot VDF,为执行层创建不可偏倚的随机性。一个离线参与者使用区块的 RANDAO 值作为输入运行 VDF 计算,并返回执行证明和结果(uint256),用于作为随机值。证明和返回的随机值提交给验证合约,验证 VDF 证明并将随机值永久存储以供任何人参考。至关重要的是,任何人 都可以充当运行 VDF、证明历史块哈希和在链上提交相关证明的角色。

虽然这种结构理论上授予我们安全随机性,但 VDF 在实际应用中并不简单。很难量化 绝对 最小 VDF 可以运行的时间,这是部署生产环境中不可破解的 VDF 的关键。VDF 是一个 活跃的研究领域 并且对以太坊的用户体验和安全性有其他影响。

我们做了什么?

基于 RANDAO 和 SNARK 区块哈希预言机的随机性信标

通过结合 SNARKs 和 VDFs,我们开发了一个 简单、ETH 原生、兼容 VDF 的随机性信标,由 RANDAO可插拔的区块哈希预言机 提供支持。我们设计它以尽可能 mimick 现有的随机性请求流程。

我们编写了一个 随机性信标接口、一个可行的 基于 RANDAO 的信标 和一个 使用 StarkWare 的 VeeDo VDF 的 VDF 参考实现(还未准备好投入生产),可在我们的 Github 找到。

我们还编写了一个 区块哈希预言机接口基于区块哈希操作码的区块哈希预言机证明历史块哈希的电路基于 SNARK 的区块哈希预言机,该合约验证证明并在链上证明历史块哈希。

这些合约是概念验证,但已准备好作为一旦 VDF 准备好投入生产的基础。

这些合约和电路未经优化、未审计且具有实验性质 — 使用风险自负!欢迎提出现有问题和拉取请求。

未来工作

还有很多要做的事情以及多种贡献方式。以下是我们邀请你进行的几项想法。

  • 证明者激励:设计一种高效机制,在尽量减少用户随机性成本的同时,激励中继满足请求。
  • 区块哈希预言机:创建一个高效的、面向生产的区块哈希预言机,实现我们的 IBlockhashOracle 接口。
  • VDF:编写一个用于 Nova SNARK VDF 的 Solidity 验证合约。
  • Noir (为 SNARK 证明系统新兴的领域特定语言)

结论

尽管以太坊上存在多种负担随机性的变体,但我们发现其中许多具备微妙的缺陷,如偏倚性、活性、不必要的成本或额外的信任假设。

为了应对这些问题,我们提出了可能的设计,SNARK 和 VDF 独特地支持我们的设计。我们还开发了可轻易集成生产就绪的可验证延迟函数(VDF)的概念验证替代方案,以提供安全、经济实惠且整合简易的随机性,为以太坊执行层提供服务。

如果你有兴趣探讨任何这些主题,随时与我们联系!你可以在 Twitter 上找到我们 @amangotchu@sina_eth_@gakonst

感谢 Yi Sun 和 Jonathan Wang 向我们引入可行的 SNARK 区块哈希预言机的概念。

鸣谢: Yi SunJonathan WangDan LeeTransmissions11Dan RobinsonMaxim VezenovKevaundray WedderburnJustin Drake

// 使用上一个块的哈希作为随机性。
function coinflip() view external returns (bool) {
    return uint256(blockhash(block.number-1)) % 2 == 0;
}
contract RandomnessProvider {
    /// 存储一个请求承诺到的区块。
    mapping(bytes32 => uint256) public revealBlock;

    /// 用户请求未来块的随机性以及请求 ID。
    function commitRandomness(bytes32 _requestId, uint256 _revealBlock) external {
        require(_revealBlock > block.number, "必须承诺到未来块");
        require(revealBlock[_requestId] == 0, "请求已提交");
        revealBlock[_requestId] = _revealBlock;
    }

    /// 返回块哈希,前提是请求的目标
    /// 块已达到。
    function fetchRandomness(bytes32 _requestId) public view returns (uint256) {
        bytes32 randomnessBlock = revealBlock[_requestId];
        require(block.number > randomnessBlock, "请求尚未准备好");
        require(block.number <= randomnessBlock + 256, "请求已过期");
        return blockhash(randomnessBlock);
    }
}
def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
    epoch = get_current_epoch(state)

    # 验证 RANDAO 揭示
    proposer = state.validators[get_beacon_proposer_index(state)]
    signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
    assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal)

    # 混合 RANDAO 揭示
    mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
    state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
// 返回该函数被调用的块的 RANDAO。
// 例如,如果一笔交易调用这个函数并包含在
// 第 10 块中,则返回第 10 块的 RANDAO。
function getRandomValue() internal returns (uint256) {
    return block.difficulty;
}
function submitRANDAO(bytes memory rlpEncodedHeader) external {
    // 验证块哈希是一个有效的历史块哈希。
    bytes32 blockHash = keccak256(rlpEncodedHeader);
    if (!isValidHistoricalBlockhash(blockHash)) {
        revert BlockhashUnverified(blockHash);
    }

    // 解码 RLP 编码的区块头。
    RLPReader.RLPItem[] memory ls = rlpEncodedHeader.toRlpItem().toList();

    // 从区块头提取混合哈希(RANDAO)。
    uint256 RANDAO = ls[13].toUint();

    // 将 RANDAO 值存储在此区块编号下。
    blockNumToRANDAO[blockNum] = RANDAO;
}
/// @notice 将验证过的块哈希映射到其区块编号。
mapping(bytes32 => uint256) public blockhashToBlockNum;

/// @notice 验证在此交易调用之前块的哈希。
function poke() public {
    blockhashToBlockNum[blockhash(block.number - 1)] = blockNum;
}

/// @notice 使用块哈希操作码验证特定块编号的块哈希。
/// 块哈希操作码目前限于过去 256 个块。
/// @param blockNum 要验证的块编号。
function pokeBlocknum(uint256 blockNum) public {
    bytes32 blockhashVal = blockhash(blockNum);
    if (blockhashVal == bytes32(0)) {
        revert PokeRangeError();
    }

    blockhashToBlockNum[blockhashVal] = blockNum;
}
/// @notice 为提供随机性的合约接口。
interface IRandomnessProvider {
    /// @notice 发出对此特定块的随机性请求。
    event RandomnessRequested(address indexed requester, uint256 indexed randomnessBlock);

    /// @notice 发出在特定块上履行随机性的事件。
    event RandomnessFulfilled(uint256 indexed fulfilledBlock, uint256 randomSeed);

    /// @notice 请求随机性并返回与请求关联的块编号。
    function requestRandomness() external returns (uint256);

    /// @notice 请求来自特定块的随机性。
    function requestRandomnessFromBlock(uint256 blockNum) external;

    /// @notice 返回特定块的 >= 1 个随机值。
    function fetchRandomness(uint256 blockNum, uint256 numberRandomValues) external view returns (uint256[] memory);
}
/// @notice 所有区块哈希预言机必须实现的接口
/// 以便 RandomnessProvider 可以插入。
interface IBlockhashOracle {
    /// @notice 如果块哈希被验证,
    /// 则返回非零的、准确的块编号。
    function blockHashToNumber(bytes32 hash) external view returns (uint256);
}
  • 原文链接: paradigm.xyz/2023/01/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Paradigm
Paradigm
Paradigm 是一家研究驱动型技术投资公司 https://www.paradigm.xyz/