以太坊预言机合约:设置和方向
已发表: 2022-03-11以太坊智能合约不仅仅是“新热点”。 我相信它们(或相关的东西)准备好在即将到来的互联网新时代改变人类相互做生意的方式。 时间会证明情况是否如此。
这是一篇关于使用 Solidity 开发以太坊智能合约的三部分文章中的第一篇,最具体地探讨了使用所谓的“预言机”合约——这些合约基本上是将数据注入区块链以供其他智能合约使用的合约。
- 第 1 部分:介绍使用 Truffle 进行开发,以及进一步实验的项目设置
- 第 2 部分:深入研究代码以进行更深入的检查
- 第 3 部分:关于智能合约预言机的概念性讨论
本系列的第 1 部分的目标不是深入了解预言机合约的概念、它们背后的哲学,甚至不深入了解它们的本质; 我们以太坊预言机教程这一部分的目标很简单:
- 让您开始使用 Truffle 构建智能合约。
- 构建一个智能合约项目,它将在第 2 部分和第 3 部分中为我们服务。
- 介绍一些与以太坊智能合约和智能合约编码相关的概念。
- 使用 Truffle 和智能合约介绍编译/运行/调试周期。
定义:甲骨文。 智能合约从区块链以外的世界访问数据的一种手段。 一种智能合约本身,预言机从外界获取数据并将其放入区块链以供其他智能合约使用。
本文的第一部分将包括设置所有先决条件。 然后,我们将建立一个单一的以太坊合约并使用 Truffle 对其进行测试。 最后,我们将预言机与客户端分开并共同测试它们。
软件要求
- 任何主要的操作系统都可以工作,尽管某些安装和设置在不同的系统上当然会有所不同。 我已经在 Ubuntu Linux (16.04) 上完成了所有这些工作。 我在 Windows 上设置环境也没有问题。 我没有尝试过 Mac,但我知道在 Mac 上这样做也很常见。
- 不需要运行完整的 eth 节点; 我们将使用带有自己的测试网的 Truffle。 如果您对自己的工作有所了解,则可以使用您选择的任何其他测试网; 就本教程而言,Truffle 的本地开发测试网是最简单且最易于访问的。
知识要求
- 区块链工作原理的基本知识
- 了解什么是基于区块链的智能合约
- 一些关于智能合约开发的基本世界经验会有所帮助,但如果你聪明且雄心勃勃,则不是必需的。 (我知道你是!)
本系列文章可以作为对智能合约的第一次介绍,但它很快就会进入更高级的概念。 如果这是您的第一个 eth 智能合约教程,请准备好快速爬升。 如果您感到自信,那就太好了; 如果没有,请随意获得一两个更简单的“hello world”类型的教程。 对于初学者,请查看一篇或以前的以太坊文章和 Cryptozombies。
警告:智能合约空间如此新,变化很快。 撰写本文时新的 Solidity 语法功能在您阅读本文时可能已被弃用或过时。 Geth 版本可能来来去去。 Solidity 总是在添加新的语言特性并弃用旧的特性。 许多新功能目前正在开发中。 因此,如有必要,请做好准备,使本文中的信息适应未来的新形势; 如果您认真学习智能合约开发,那么我对您有信心。
示例应用程序的描述
用例:用户投注拳击比赛。
- 用户可以提取可下注的拳击比赛列表。
- 用户可以选择一场比赛并对获胜者下注。
- 用户可以下注高于指定最小值的任何金额。
- 如果用户的选择输了,则用户输掉全部赌注。
- 如果用户的选择获胜,则用户根据他/她的赌注大小和对比赛失败者的总赌注获得一部分底池,之后房子(合约所有者)将获得一小部分奖金.
什么是以太坊预言机?
智能合约仍然是一种新事物。 它们还没有成为主流,它们如何工作的许多方面还没有敲定和标准化。 我将简要解释一下“神谕”这一理念背后的推动力——请耐心等待; 我们将在后面的部分更深入地讨论它。
设计区块链合约不像编写客户端-服务器应用程序。 一个重要的区别是与合约交互的数据必须已经在区块链上。 没有呼出区块链。 不仅语言不支持,区块链范式也不支持。 合约可以以基于以太坊的货币形式进行投注,将其存储在合约中,并在宣布比赛获胜时根据公式将其释放到正确的钱包地址。 但是合同怎么知道赢家呢? 它不能查询 REST API 或类似的东西。 它只能使用已经在区块链中的数据! 智能合约的许多用例都遇到了类似的问题——它们受到严重限制,除非它们能够与区块链之外的世界进行交互。
如果合约只能与区块链上的数据交互,一个明显的解决方案是将必要的数据注入到区块链中。 这就是神谕。 预言机是另一个合约,它将数据注入区块链,允许其他合约使用它。 虽然这可能会引发有关信任和不信任的问题,但现在只需接受这就是预言机的含义。 在本系列的第 3 部分中,我们将讨论这些细微差别。 在我们的示例用例中,预言机将是将数据注入区块链的合约,涉及 (a) 哪些匹配可用,以及 (b) 谁赢得了这些匹配,一旦决定。
设置以太坊开发环境
对于基本设置,我们将安装:
- Geth(暂时可选)
- 松露
- Ganache CLI(可选)
- 开发环境(可选)
本文没有足够的篇幅作为环境设置的完整指南,而只是一个粗略的指南。 不过没关系,因为您的特定操作系统已经有很多更完整的设置指南,而互联网并不需要新的。 因此,我将带您快速了解路径,并为您提供一些资源,以便根据需要获取更多详细信息。 准备好按照您的系统要求和 Google 的指示安装要求和先决条件。
安装 Geth(可选)

Geth 是 Go-ethereum,以太坊核心软件; 虽然这个练习完全没有必要,但任何想成为以太坊开发者的人都应该拥有它并熟悉它。 如果您要将智能合约部署到实时以太坊网络,这将是必要的。
- http://www.talkcrypto.org/blog/2018/01/23/what-is-geth/
- https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Ubuntu
- https://github.com/ethereum/go-ethereum/wiki/Installation-instructions-for-Windows
安装松露

Truffle 是我们将用于开发的主要内容,并且绝对是本指南的要求。 查找并按照您的操作系统的特定说明安装 Truffle。 以下是一些希望对您有所帮助的链接。
- https://truffleframework.com/docs/truffle/getting-started/installation
- https://github.com/trufflesuite/truffle
- https://truffleframework.com/tutorials/how-to-install-truffle-and-testrpc-on-windows-for-blockchain-development
安装 Ganache CLI(可选)

我建议安装 Ganache CLI 以用作另一个测试工具,尽管我们实际上不会在我们的教程中使用它。 这是可选的。
https://github.com/trufflesuite/ganache-cli
以太坊开发环境
使用任何简单的文本编辑器(如 Notepad++、gedit、vi 或您选择的任何文本编辑器或 IDE)来完成整个教程都是不可能的。 我个人正在使用具有以下扩展的 Visual Studio Code:
- 坚固性
- Solidity 扩展
- 材质图标主题
注意:扩展不是必需的——它们只是为了更好的编码环境。
设置代码
项目设置
Truffle 是一个非常方便的工具,用于编译智能合约,将它们迁移到区块链,它还提供开发和调试实用程序。 为了与 Truffle 集成,需要进行一些项目设置。 现在我们将在 Truffle 和目录结构中为我们的项目设置 shell。 只需坐下来,暂时按照机器人的步骤进行操作,然后尽情享受吧。
创建一个目录来存放所有代码; 称之为oracle-example 。
在根目录中,创建两个子目录,因为最终项目将包含两个子项目。 创建目录:
- /oracle 示例/客户端
- /oracle 示例/oracle
进入客户端文件夹,因为这是我们要开发的第一个项目。 在/oracle-example/client文件夹中打开一个终端(命令行)窗口。
运行命令truffle init
。
请注意,创建的许多文件中有 truffle -config.js和truffle.js 。 我们两个都不需要,所以删除truffle-config.js (只是为了避免混乱和混乱)。
我们需要编辑truffle.js ,以便将 Truffle 指向正确的测试方向。 将truffle.js的内容替换为以下内容:
module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" // Match any network id } } };
https://github.com/jrkosinski/oracle-example/tree/part1-step1/client/truffle.js
请注意,Truffle init 创建了一个名为migrations ( oracle-example/client/migrations ) 的目录。 该文件夹内应该是一个名为1_initial_migration.js的文件。
在迁移目录中添加另一个文件并将其命名为2_deploy_contracts.js ,内容如下:
var BoxingBets = artifacts.require("BoxingBets"); module.exports = function(deployer) { deployer.deploy(BoxingBets); };
https://github.com/jrkosinski/oracle-example/tree/part1-step1
添加代码
现在简单的设置已经完成,我们准备开始编码。 请记住,本文的这一部分仍然是介绍和设置,因此我们将快速浏览代码。 我们将在第 2 部分对代码进行更深入的解释,并在第 3 部分对架构和概念进行更深入的讨论。也就是说,我们将快速触及代码中明显的一些核心概念。 仔细跟随以跟上。
GitHub 上提供了该过程中此步骤的完整代码:https://github.com/jrkosinski/oracle-example/tree/part1-step1
Solidity 中的合约
Solidity 中的“契约”大致类似于其他面向对象语言中的类。 该语言本身已与 Golang 和 JavaScript 等进行了比较。 Solidity 中的其他一些语言结构——我们稍后会有例子——是修饰符、库和接口。 合约支持继承(包括多重继承)。 Solidity 合约文件具有 .sol 扩展名。
甲骨文接口
将此文件添加到您的项目中: /oracle-example/client/contracts/OracleInterface.sol
https://github.com/jrkosinski/oracle-example/tree/part1-step1/client/contracts/OracleInterface.sol

通常,oracle 接口就是这样——一个接口。 对于第一次迭代,它只是 Solidity 项目中包含的一个简单类,现在只是作为占位符。 在我们成功编译并在 Truffle 上运行合约之后,我们将在下一步中将其移出。 稍后我们将其转换为实际接口后,函数实现将为空。
客户合同
将此文件添加到您的项目中: /oracle-example/client/contracts/BoxingBets.sol
https://github.com/jrkosinski/oracle-example/tree/part1-step1/client/contracts/BoxingBets.sol
这是使用拳击比赛数据的合约,允许用户查询可用的比赛并对其下注。 在以后的迭代中,它将计算并支付奖金。
编译运行
现在是时候看看我们是否第一次做对了!
编译和迁移合约
在/oracle-example/client/文件夹中打开一个终端
使用以下命令编译代码:
truffle compile


替代方案:使用我的 recompile.sh shell 脚本 (https://github.com/jrkosinski/oracle-example/tree/part1-step1/client/recompile.sh)。
请注意,您会看到很多警告,因为我们的代码还不是最终形式!
打开 Truffle 开发控制台:
truffle develop
现在,在 Truffle 开发者控制台中,迁移到测试网络:
truffle(develop)> migrate
运行合约
在开发控制台提示符下,输入以下代码行:
truffle(develop)> BoxingBets.deployed().then(inst => { instance = inst })
现在,“instance”是引用 BoxingBets 合约的变量,可用于调用其公共方法。
使用以下命令对其进行测试:
truffle(develop)> instance.test(3, 4)
请注意,我们在BoxingBets.sol中包含了一个公共“测试”函数。 它将您传递给它的任何两个数字相加,只是为了证明合约正在执行代码,并且我们可以从 Truffle 开发控制台调用它。 如果我们得到一个看起来很理智的回应(见下文),那么我们的工作就完成了(至少现在是这样)。
分离以太坊神谕
如果到目前为止一切都成功了,那么我们就完成了。 接下来我们要做的是将预言机合约与 BoxingBets 合约分开。 在实际使用中,预言机的合约将与区块链上的客户端合约分开存在,因此我们需要能够:
- 通过区块链地址实例化它。
- 动态更改客户端合约用来引用预言机的预言机地址。
所以简而言之,我们现在要做的就是将预言机和客户端分成两个独立的区块链合约实体,并让它们相互通信。 客户端将通过地址实例化预言机并调用它。
客户合同
首先,我们将更改客户端合约(client),使其引用到 oracle 的动态接口,而不是具体的类。 然后我们将确保它从外部合约实例化预言机。
进入/oracle-example/client/contracts/OracleInterface.sol 。 正如我们之前提到的,这目前还不是一个界面,但我们即将把它变成一个界面。 将其中的内容替换为以下内容:

https://github.com/jrkosinski/oracle-example/tree/part1-step2/client/contracts/OracleInterface.sol
pragma solidity ^0.4.17; contract OracleInterface { enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg cancelled) Decided //index of participant who is the winner } function getPendingMatches() public view returns (bytes32[]); function getAllMatches() public view returns (bytes32[]); function matchExists(bytes32 _matchId) public view returns (bool); function getMatch(bytes32 _matchId) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner); function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint participantCount, uint date, MatchOutcome outcome, int8 winner); function testConnection() public pure returns (bool); function addTestData() public; }
在BoxingBets.sol中,我们将替换这一行:
OracleInterface internal boxingOracle = new OracleInterface();
用这两行:
address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
现在我们想要的是一种动态设置预言机地址的方法,以及一个我们可以调用以找出当前预言机地址的函数。 将这两个函数添加到BoxingBets.sol :
/// @notice sets the address of the boxing oracle contract to use /// @dev setting a wrong address may result in false return value, or error /// @param _oracleAddress the address of the boxing oracle /// @return true if connection to the new oracle address was successful function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) { boxingOracleAddr = _oracleAddress; boxingOracle = OracleInterface(boxingOracleAddr); return boxingOracle.testConnection(); } /// @notice gets the address of the boxing oracle being used /// @return the address of the currently set oracle function getOracleAddress() external view returns (address) { return boxingOracleAddr; }
最后,为了测试客户端和预言机之间的连接,我们可以将 BoxingBets 中的测试函数替换为一个测试预言机连接的函数:
/// @notice for testing; tests that the boxing oracle is callable /// @return true if connection successful function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
可拥有
请注意, setOracleAddress
的定义后面有一个onlyOwner
修饰符。 这限制了该函数被合约所有者以外的任何人调用,即使该函数是公共的。 这不是语言功能。 这是由 Ownable 合约提供给我们的,它是从 OpenZeppelin 的通用 Solidity 合约库中提取出来的。 我们将在第 2 部分中详细介绍,但为了方便使用onlyOwner
修饰符,我们需要进行一些更改:
将Ownable.sol从 https://github.com/jrkosinski/oracle-example/tree/part1-step2/client/contracts/Ownable.sol 复制到/oracle-example/client/contracts/中。
在BoxingBets.sol的顶部添加对它的引用,如下所示:
import "./Ownable.sol";
(您可以在导入OracleInterface.sol的行下方添加它。)
修改 BoxingBets 的合约声明,使其继承自 Ownable,由此:
contract BoxingBets {
对此:
contract BoxingBets is Ownable {
我们应该都准备好了。 完整代码在这里,以防您迷路:https://github.com/jrkosinski/oracle-example/tree/part1-step2/client/contracts
甲骨文合约
设置
现在 BoxingBets 合约试图通过地址引用一个完全独立的合约(即预言机),我们的下一个工作是创建该预言机合约。 所以现在我们要创建一个包含预言机合约的完整独立项目。 这与我们已经为客户合同项目所做的设置基本相同; 即设置Truffle进行编译和开发。
您应该已经有一个名为/oracle-example/oracle/的文件夹,我们在上一步中创建了该文件夹(如果没有,请立即创建该空目录)。 在该目录中打开一个终端。
- 运行命令
truffle init
。 - 删除/oracle-example/oracle/truffle-config.js 。
- 像这样编辑/oracle-example/oracle/truffle.js :
module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" // Match any network id } } };
请参阅此处的示例:https://github.com/jrkosinski/oracle-example/tree/part1-step2/oracle/truffle.js
在/oracle-example/oracle/migrations/中,创建一个名为2_deploy_contracts.js的文件,其内容如下:
var BoxingOracle = artifacts.require("BoxingOracle"); module.exports = function(deployer) { deployer.deploy(BoxingOracle); };
请参阅此处的示例:https://github.com/jrkosinski/oracle-example/tree/part1-step2/oracle/migrations/2_deploy_contracts.js
甲骨文代码
对于此步骤,只需将以下三个文件从 https://github.com/jrkosinski/oracle-example/tree/part1-step2/oracle/contracts/ 复制到您的/oracle-example/oracle/contracts/
文件夹中:
- BoxingOracle.sol:主要的预言机合约。
- Ownable.sol:对于仅所有者功能,正如我们已经在客户端合同中使用的那样。
- DateLib.sol:一个日期库。 我们将在本系列的第 2 部分中更深入地研究它。
测试 Oracle
现在,在项目的当前迭代中,我们确实需要彻底测试我们的智能合约预言机,因为这将是我们构建项目其余部分的基础。 所以,既然我们已经设置了 oracle 项目并复制了代码,我们将希望:
- 编译预言机。
- 确保预言机运行。
- 在 Truffle 控制台中运行一些函数以确保预言机按预期工作。
编译和迁移 Oracle
仍然在打开/oracle-example/oracle/
的终端中,运行以下命令。 同样,这些步骤与我们已经完成的编译和迁移客户端合约的步骤相同。
truffle compile
替代方案:使用我的 recompile.sh shell 脚本 (https://github.com/jrkosinski/oracle-example/tree/part1-step2/oracle/recompile.sh)。
打开 Truffle 开发控制台:
truffle develop
迁移到测试网络:
truffle(develop)> migrate
运行和测试 Oracle
仍然在 Truffle 开发控制台中,输入以下内容以捕获指向预言机合约的可用指针:
truffle(develop)> BoxingOracle.deployed().then(inst => { instance = inst })
现在我们可以(并且应该)在我们的预言机合约上运行一套测试来测试它。 尝试依次运行以下命令并检查结果。
truffle(develop)> instance.testConnection() ... truffle(develop)> instance.getAllMatches() ... truffle(develop)> instance.addTestData() ... truffle(develop)> instance.getAllMatches() ...
此时鼓励您查看 oracle 代码,查看可用的公共方法,阅读代码中的注释,并提出一些您自己的测试来运行(并在控制台中运行它们,如如上所示)。
测试和调试
现在我们准备好进行最后的测试:测试客户端合约是否可以调用已经在区块链上的预言机合约,并拉入并使用其数据。 如果所有这些都有效,那么我们就有了一个客户端-预言机对,我们可以将其用于进一步的实验。 我们运行端到端测试的步骤:
- 编译并运行预言机合约
- 编译并运行客户端合约
- 获取预言机合约地址
- 在客户端合约中设置预言机地址
- 将测试数据添加到预言机合约
- 测试我们是否可以在客户端合约中检索该数据
打开两个终端窗口:
- /oracle-example/client/中的一个
- 另一个在/oracle-example/oracle/
我建议您保持/oracle-example/client/一个在左侧打开, /oracle-example/oracle/一个在右侧打开,并密切关注以避免混淆。
编译并运行 Oracle 合约
在/oracle-example/oracle/终端中执行以下命令:
bash recompile.sh truffle develop truffle(develop)> migrate truffle(develop)> BoxingOracle.deployed().then(inst => { instance = inst })
编译并运行客户端合约
在/oracle-example/client/终端中执行以下命令:
bash recompile.sh truffle develop truffle(develop)> migrate truffle(develop)> BoxingBets.deployed().then(inst => { instance = inst })
获取 Oracle 合约的地址
在/oracle-example/oracle/终端中对 Truffle 执行以下命令:
truffle(develop)> instance.getAddress()
复制作为此调用输出的地址; 并在下一步中使用它。
在客户端合约中设置 Oracle 地址
在/oracle-example/client/终端中执行以下命令来松露:
truffle(develop)> instance.setOracleAddress('<insert address here, single quotes included>')
并测试它:
truffle(develop)> instance.testOracleConnection()
如果输出为true
,那么我们就可以开始了。
测试我们是否可以在客户端合同中检索该数据
在/oracle-example/client/终端中执行以下命令来松露:
truffle(develop)> instance.getBettableMatches()
它应该返回一个空数组,因为尚未将测试数据添加到 oracle 端。
执行以下命令在/oracle-example/oracle/终端中添加测试数据:
truffle(develop)> instance.addTestData()
在/oracle-example/client/终端中执行以下命令 truffle,看看我们是否可以从客户端获取新添加的测试数据:
truffle(develop)> instance.getBettableMatches()
现在,如果您从getBettableMatches()
返回的数组中获取单个地址,并将它们插入getMatch()
。
此时鼓励您查看客户端代码,查看可用的公共方法,阅读代码中的注释,并提出一些您自己的测试来运行(并在控制台中运行它们,如更多)。
第一部分的结论
我们从这个练习中得到的结果是有限的,但我们的目标也是有限的,以保持现实的步伐。 我们的客户还没有能力下注、处理资金、分配奖金等。我们所拥有的——除了获得的知识和经验——是:
- 一个功能强大的智能合约预言机
- 可以连接到预言机并与之交互的客户端
- 进一步发展和学习的框架
这对于一篇短文来说还不错。
在本系列的第二部分中,我们将更深入地研究代码并查看智能合约开发独有的一些功能以及 Solidity 特有的一些语言功能。 许多在这部分中被忽略的东西将在下一节中解释。
在本系列的第三部分中,我们将讨论一些关于智能合约的理念和设计,特别是关于它们与预言机的使用。
进一步的可选步骤
单人实验是一种很好的学习方式。 如果您正在考虑如何扩展本教程以获得更多知识,这里有一些简单的建议(第 2 部分和第 3 部分将不涉及以下内容):
- 将合约部署到 Ganache(以前的 testrpc)并运行相同的测试来验证功能。
- 将合约部署到 ropsten 或 rinkeby 测试网并运行相同的测试来验证功能。
- 为 oracle 或客户端(或两者)构建 web3js 前端。
祝你好运,如有任何问题,请随时与我联系。 我不能保证一定会迅速回复,但我会尽力而为。