使用 Solidity 和 Node.js 构建简单的区块链预言机(一)

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 15 天,点击查看活动详情

区块链上的预言机是允许区块链世界与来自WEB其余部分的数据交互的框架,将其称为 WEB 2.0 世界。随着智能合约应用的不断扩展,处理独特用例所需的各种数据也将不断扩大。

事实上,WEB 2.0WEB 3.0 是两个不同的网络,目前最实用的数据都存在于 WEB 2.0 上。通过创建一组协议来使智能合约能够访问这些数据,新一代的WEB、系统设计和区块链将会出现。

当前的协议倾向于使用预言机的概念来构建混合系统,这些系统依赖于智能合约和链下 API 来桥接 WEB 2.0 数据以及其他区块链。最著名的预言机是 Chainlink,它提供定价数据、与其他区块链的连接、对大多数 API 的访问以及各种其他数据馈送。

其他主要的例子包括代币桥,它允许在链下服务的帮助下在链之间移动代币和数据。随着时间的推移,可能会出现更多独特的预言机。

目前,像 Singularity NET 这样的组织正在努力构建预言机,以创建市场并轻松访问提供人工智能推理等服务的 API。

在本文中,目标是通过浏览当前用例的一般架构并使用 soliditynode.js 构建一个简单的链上天气预言机来对预言机概念进一步的了解。

事件驱动的预言机设计

image.png

在处理使用链下服务代表智能合约执行某些操作的问题时,要记住的最重要的事情是智能合约和服务之间没有正式的消息传递过程。有了这个前提条件,就知道智能合约不能 push ,服务必须是 listenwatchpull

该服务只有两个链上项目可以监视,即状态变量和事件。察状态变量很麻烦,因为它需要与合约进行多次交互。另一方面,事件不需要直接交互。

像这样发出智能合约事件:

emit newEvent(block.timestamp)
复制代码

事件可以看作是开发人员定义的智能合约操作的日志。就像其他类型的日志一样,其他服务可以订阅此提要以监视特定类型的事件,从它们的参数中收集数据,并用它们做任何他们想做的事情。任何有权访问区块链的人都可以看到这些日志,并且可以通过 web3.js 等库进行访问。

鉴于这种独特的通信系统,智能合约可以廉价地“通知”外部世界中的服务事件,或需要完成的预言机案例工作。了解事件以及链上到链下的消息传递是预言机设计中最重要的部分。

一旦服务发现新事件并触发其操作,它就可以获取事件中有价值的数据和唯一的作业 ID,并像任何其他程序一样执行链下操作。

任务完成后,服务可以使用 web3 库与合约进行交易。典型的交易可以“上传”带有作业 ID 的请求/事件的结果,因此智能合约可以继续处理它计划对这些链下数据执行的任何操作。把这一切放在一起,它看起来像这样:

  1. Oracle 智能合约发出一个包含作业信息的事件。

  2. 链下预言机服务监听事件并在事件触发时拉取信息。

  3. 链下预言机与任何服务或数据交互以接收结果。

  4. 链下预言机与预言机智能合约进行交易以更新工作数据。

  5. 智能合约生态系统根据需要使用数据。

当然,这是对像 Chainlink 这样的预言机设计的过度简化,其中包括许多节点和共识协议,以确保预言机数据是分散的。虽然有趣且重要,但简单的理解将是构建该概念的最佳背景。

构建一个简单的预言机

为了更好的理解预言机的概念,本文选择了天气相关的服务,输入的数据容易传输,开放 API 也很丰富。鉴于对事件驱动预言机的理解,系统设计如下所示:

image.png

从系统设计来看,有一个 Solidity 合约和一个 node.js 程序,这个设计的要求很简单:

  • Solidity 智能合约
  • node.js
  • Web3.js
  • 天气 API(使用OpenWeather)

创建项目目录 node-oracle,进入项目目录,初始化项目:

npm init
复制代码

安装依赖:

npm install hardhat chai @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers ethers ethereum-waffle --save-dev 
复制代码

现在编写智能合约,在项目根目录下创建智能合约文件夹并进入目录 contracts,运行以下命令来创建一个 truffle 项目,这样就可以开始写智能合约,其中写入以下代码:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract WeatherOracle {
    // 遍历 jobId => 检查智能合约交互的完成状态
    // 默认所有的为 false
    mapping(uint => bool) public jobStatus;

    // 如果jobStatus值为0,则表示结果实际上为0
    mapping(uint => uint) public jobResults;

    // 当前可用的 jobId 
    uint jobId;

    // 事件触发的预言机 API
    event NewJob(uint lat, uint lon, uint jobId);

    constructor(uint initialId){
        jobId = initialId;
    } 

    function getJobId() public view returns (uint) {
       return jobId;
    }

    function getJobResults(uint _jobId) public view returns (uint) {
       return jobResults[_jobId];
    }

    function getWeather(uint lat, uint lon) public {
        emit NewJob(lat, lon, jobId);
        jobId++;
    }

    function updateWeather(uint temp, uint _jobId)public {
        jobResults[_jobId] = temp;
        jobStatus[_jobId] = true;
    }
}
复制代码

已经逐行提供了包含更多细节的在线注释,智能合约是使用几个 map 来保存 jobIds(true = complete, false = incomplete) 工作的状态和结果。

该合约还提供了一个触发工作的函数,函数 getWeather 将位置数据作为参数,创建一新的 jobId,并发出一个带有相关位置和工作信息的事件。

这个预言机的一个重要特性是 updateWeather 函数上的操作符修饰符。为了只允许服务与这个函数交互,这个修饰符是必需的,否则,任何人都可以更新 job 数据。

测试合约

在部署之前,先来测试合约以确保可以逻辑都是正确的。在项目根目录中创建文件夹 test ,此文件夹可以包含客户端测试和以太坊测试。

test 文件夹中添加 test.js 文件,该文件将在一个文件中包含合约测试,代码如下:

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("WeatherOracle", function () {
    let oracleContract;
    before(async () => {
        const oracleFactory = await ethers.getContractFactory("WeatherOracle");
        oracleContract = await oracleFactory.deploy(1);
        await oracleContract.deployed();
    });
    it("Should have currentJobId", async () => {
        let currentJobId = await oracleContract.getJobId();
        expect(currentJobId).to.equal(1);
    });
});
复制代码

然后在项目根目录下执行脚本:

npx hardhat test
复制代码

选择创建 Create an empty hardhat.config.js,替换为以下代码:

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
require("@nomiclabs/hardhat-waffle");

const config = {
    alchemy: "9aa3d95b3bc440fa88ea12eaa4456161", // 测试网络token
    privateKey:
        "e5275ae4ad0f4fd33e539e4d8af9fceaeb1c0f905f423cc6aa48bb6e", // 钱包私钥
};

module.exports = {
    solidity: "0.8.4",
    networks: {
        ropsten: {
            url: `https://ropsten.infura.io/v3/${config.alchemy}`,
            accounts: [config.privateKey],
            chainId: 3,
        },
    },
};
复制代码

再次执行测试命令:

npx hardhat test
复制代码

部署(到 Ropsten 测试网络)

在项目根目录下创建文件夹 scripts,然后在文件夹中创建文件 deploy.js

const main = async () => {
    const oracleFactory = await ethers.getContractFactory("WeatherOracle");
    const oracleContract = await oracleFactory.deploy(1);

    const [deployer] = await ethers.getSigners();

    console.log("Deploying contracts with the account: ", deployer.address);

    console.log("Account balance: ", (await deployer.getBalance()).toString());
    await oracleContract.deployed();
    console.log("Contract deployed to: ", oracleContract.address);
};

const runMain = async () => {
    try {
        await main();
        process.exit(0);
    } catch (error) {
        console.log(error);
        process.exit(1);
    }
};
runMain();
复制代码

要部署合约,请在项目根目录下运行命令:

npx hardhat run scripts/deploy.js --network ropsten
复制代码

执行完成之后可以看到结果:

Deploying contracts with the account:  0xDC13b48Cf2a42160f820A5Ad79B39E695C0c84
Account balance:  4807844068484
Contract deployed to:  0x0006544b9c915Ab3cb0e21000E4a4ABE746
复制代码

待续

猜你喜欢

转载自juejin.im/post/7109454508459032607