基于Quorum构建Dapp: 企业级私有链

Ethereum 最优秀的设计点在于公开的网络, 使得每笔交易记录历史以及每笔交易的参与者信息, 都可以被任何人在任意时间查看.

在 Ethereum 开始之初, Truffle就获取到了其交易账本全部内容, 公有链只围绕智能合约展开. 随着时间的推移, 技术在进步, 基于权限的区块链 Quorum 开始出现在视野中.

Quorum 是 Ethereum 的一个分支版本, 在 Ethereum 顶层设计之上增加了一些新特性. 例如, Quorum 允许参与者之间创建私有链, 同时在顶层设计中加入交易隐私.

交易隐私适用于多个应用场景, 尤其在企业或者银行系统领域. 例如, 大银行想要借助区块链的优势, 但是不想客户的交易信息被随意获取. Quorum 提供一个可选的机会.

让我们举个小例子: Bob, Tom 和 Alice 共同创建一个区块链, Alice 想转账 20 TC 给 Bob, 但是不想让 Tom(或者除了Bob的其他人) 知道, 因为想要保护自己的隐私. 使用 Quorum, Alice 可以很方便的创建一个交易, 并且使交易信息只发送给 Alice 和 Bob.

本示例意味着 Truffle 官方支持 Quorum. 本示例结束后, 你将学会如何一起使用 Truffle 和 Quorum 来创建支持隐私内容的 Dapp.

 依赖内容

本示例期望你掌握 Truffle, 以太坊, Quorum 和 Solidity 的部分知识内容, 这部分内容的详细介绍, 参考如下文章:

大部分情况下, 本示例基于命令行, 所以你需要对命令行有大致了解. 此外操作系统上还需要安装如下软件:

 快速开始

本示例中将展示如何使用 Truffle 开发基于 Quorum 的 Dapp 程序 7nodes(7节点示例). 基本步骤如下:

  • 建立 Quorum 客户端
  • 使用 Truffle 于 Quorum 连接
  • 部署智能合约到 Quorum 上
  • 使用 Quorum 隐私功能进行私密交易
  • 与私有的合约交互

使用 Truffle 开发基于 Quorum 的项目与开发以太坊公有链项目基本相同. Truffle 极好的支持 Quorum 功能, 建立基于以太坊公有链应用的策略, 方法也都完全适用于 Quorum 应用.

 设置 Quorum 客户端

Quorum 客户端可以完全取替以太坊客户端. 使用 Quorum 客户端, 可以设置私有链, 仅供你和你允许的人参与.

本示例中, 我们将使用已经设置在虚拟机设置好的 Quorum 的7节点集群. 当然, 你也 可以 选择手动 下载 并且编译安装. 但是本例子中, 使用预编译集群更方便.

  1. 运行集群, 开启一个命令行并且进入工作目录(你想在哪里安装环境):

    mkdir workspace
    cd workspace
    
  2. 下载 Quorum 例子:

    git clone https://github.com/jpmorganchase/quorum-examples
    
  3. 我们使用 Vagrant 来实例化集群虚拟机. 这一步准备虚拟机, 所以比较耗时.

    cd quorum-examples
    vagrant up
    
  4. vagrant up 执行成功后, 我们需要一个可以访问我们刚刚创建的矿机的方法. 一个虚拟机和另一台电脑是一个概念, 所以我们需要有方法可以访问它们. vagrant 提供了一个方法:

    vagrant ssh
    

    执行这个命令后, 我们的命令行变成了下面的样子:

    ubuntu@ubuntu-xenial:~$
    

    这意味着我们已经进入了虚拟机内部.

  5. 在进入虚拟机后, 进入我们从测试例子目录:

    ubuntu@ubuntu-xenial:~$ cd quorum-examples/7nodes/
    
  6. 我们的 Quorum 客户端基本已经完成, 只需要执行另外两个命令即可: 第一个命令创建7个 Quorum 节点模拟真实的 Quorum 环境. 第二个命令开启这7个节点. 第一个命令只需要执行一次, 而第二个命令在每次重启虚拟机后都要执行.

    ubuntu@ubuntu-xenial:~/quorum-examples/7nodes$ ./raft-init.sh
    ubuntu@ubuntu-xenial:~/quorum-examples/7nodes$ ./raft-start.sh
    

成功了! 我们现在成功的创建了一个拥有7个节点的 Quorum 环境, 我们可以将其视作私有网络中的不同参与者.

 使用 Truffle 连接 Quorum

设置 Truffle 之前, 我们需要先初始化一个 Truffle 初始化工程, 不带任何智能合约和代码的空工程.

  1. 我们开启新的命令行控制台, 原来的控制台一会儿还有用.

  2. 新控制台中, 进入工作目录, 创建工程目录:

    cd workspace
    mkdir myproject
    
  3. 接下来, 进入工程目录, 开始初始化 Truffle 工程:

    cd myproject
    truffle init
    

    查看 myproject 目录内容, 你将发现很多子目录已经创建成功了. 查看 Truffle 文档 来进一步了解 Truffle 工程结构.

  4. 继续之前, 我们需要配置 Truffle 网络指向 Quorum 客户端. 本例中, 我们编辑 truffle.js 文件 networks.development 来作为我们的节点, 指向7个节点中的第一个节点.

    // 文件名: `truffle.js` (edited for 7nodes example)
    module.exports = {
      networks: {
        development: {
          host: "127.0.0.1",
          port: 22000, // 原来为 8545 端口, 现在改为 22000
          network_id: "*", // Match any network id
          gasPrice: 0,
          gas: 4500000
        }
      }
    };
    

    我们更改了默认的端口8545位22000, 因为 VirtualBox 和 Vagrant 已经将虚拟机内部端口, 映射到了主机的 22000 端口, 所有可以使用 127.0.0.1:22000 来连接.

    注意: 七个不同的实例, 端口依次加1, 从节点1 22000 到节点7 22006.

至此, 我们已经完成对 Truffle 的设置, 可以进入代码的操作了.

 部署智能合约到 Quorum

我们不花费过多时间在编写和部署智能合约上面, 因为我们已经有 丰富的文档 讲解了, 我们只专注于如何部署智能合约到 Quorum 上.

  1. 首先, 创建智能合约 SimpleStorage.sol. 放在 contracts/ 目录下:

    // 文件: `./contracts/SimpleStorage.sol`
    
    pragma solidity ^0.4.17;
    
    contract SimpleStorage {
      uint public storedData;
    
      function SimpleStorage(uint initVal) public {
        storedData = initVal;
      }
    
      function set(uint x) public {
        storedData = x;
      }
    
      function get() constant public returns (uint retVal) {
        return storedData;
      }
    }
    
  2. 在工程目录下执行 truffle compile 编译合约.

    truffle compile
    
  3. 创建部署脚本, 2_deploy_simplestorage.js 存放在 migrations/ 目录下. 部署脚本(migration)与公有链的Truffle部署脚本基本类似, 但是有一个不同之处:

    // 文件: `./migrations/2_deploy_simplestorage.js`
    
    var SimpleStorage = artifacts.require("SimpleStorage");
    
    module.exports = function(deployer) {
      // 将 42 传递给合约的第一个构造参数
      deployer.deploy(SimpleStorage, 42, {privateFor: ["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]})
    };
    

    If you guessed the difference was privateFor, you got it! privateFor is an extra transaction parameter added to Quorum that specifies that the transaction you're making – in this case a contract deployment – is private for a specific account, identified by the given public key. For this transaction, the public key we chose represents node 7. Since we previously configured Truffle to connect to node 1, this transaction will deploy a contract from node 1, making the transaction private between node 1 and node 7.

    如果你发现多了一个 privateFor, 那么你猜对了! privateFor 是为 Quorum 提供的一个额外的交易参数, 本例中, 指定的是部署对象的账号, 账号ID为第七个节点. 由于truffle配置的是第一个节点, 所以合约将由节点一部署, 用于节点一和节点七之间做私有交易.

  4. 下面开始部署合约, 执行 truffle migrate 命令:

    truffle migrate
    

    输出与下面类似: 只有交易ID略有不同.

    Using network 'development'.
    
    Running migration: 1_initial_migration.js
      Deploying Migrations...
      Migrations: 0x721650d027d87cd247a3a776c4b6170bf1e5b936
    Saving successful migration to network...
    Saving artifacts...
    Running migration: 2_deploy_simplestorage.js
      Deploying SimpleStorage...
      SimpleStorage: 0x10ae69385c79ef3eb815ac008a7013d6878f1d38
    Saving successful migration to network...
    Saving artifacts...
    

合约部署完毕, 准备开始出发吧!

 使用 Quorum 私有特性做私有交易

We originally configured Truffle to point our development environment to the first of the seven nodes provided by the example. You can think of the first node as "us", as if we were developing a dapp for a private network that's used by multiple other parties. Since the 7nodes example provides us seven nodes to work with, we can tell Truffle about the other nodes so we can "be" someone else, and ensure the contract we deployed was private.

前面我们已经配置好了 Truffle, 使其指向7个测试节点中的第一个, 你可以假定第一个节点就是我们自身, 就像我们正在研发私有链上的 dapp 一样拥有多个其他节点存在. 由于该例子提供给我们7个节点用来测试, 我们便可以在 Truffle 中定义其他节点, 让我们冒充其他用户运行程序, 来确认我们运行的程序是私有的.

  1. truffle.js 中加入一些新的配置, 为了让网络节点名有描述含义, 我们命名节点4为 nodefour, 节点7为 nodeseven:

    // 文件: `truffle.js`
    module.exports = {
      networks: {
        development: {
          host: "127.0.0.1",
          port: 22000, // was 8545
          network_id: "*", // 支持任何网络ID
          gasPrice: 0,
          gas: 4500000
        },
        nodefour:  {
          host: "127.0.0.1",
          port: 22003,
          network_id: "*", // Match any network id
          gasPrice: 0,
          gas: 4500000
        },
        nodeseven:  {
          host: "127.0.0.1",
          port: 22006,
          network_id: "*", // Match any network id
          gasPrice: 0,
          gas: 4500000
        }
      }
    };
    

    像以往一样, VirtualBox和Vagrant将会提供本地端口, 所以新节点与原有的 development 节点不同的地方就是端口一项.

  2. 至此配置完成, 我们可以使用 Truffle 控制台与部署好的合约交互. 首先连接节点1, 使用 development 配置. 最简单的方法就是启动 Truffle 控制台, 可以直接调用部署好的合约.

    truffle console
    

    控制台变化:

    truffle(development)>
    
  3. 获取部署好的合约 SimpleStorage, 读取从 development 节点写入的数据:

    truffle(development)> SimpleStorage.deployed().then(function(instance) { return instance.get(); })
    

    获得下面的返回:

    { [String: '42'] s: 1, e: 1, c: [ 42 ] }
    

    Note that Truffle's contract abstractions use Promises to interact with Ethereum. This can be a little cumbersome on the console as it requires a few extra key strokes to get things done, but within your application it makes control flow a lot smoother. Additionally, take a look at the output we received: We got 42 back, but as an object. This is because Ethereum can represent larger numbers than those natively represented by JavaScript, and so we need an abstraction in order to interact with them.

    Truffle 智能合约基类使用 Promise 与 Ethereum 交互时使用的是 Promise 的语法形式. 这会在控制台内看起来很复杂, 因为引入了不少额外的内容来完成任务, 但是使用ide会使代码看起来更平滑. 此外, 看一下返回的输出, 数据值为 42, 但是是一个对象类型. 这是因为 Ethereum 比原生的JS支持更大的数据范围, 所以我们需要抽象后进行交互.

  4. 接下来我们以节点4的身份试着访问 SimpleStorage 智能合约. 具体方法为退出当前交互(使用 Ctrl + C / Command + C), 然后使用节点4的配置重新进入:

    truffle console --network nodefour
    

    控制台变化:

    truffle(nodefour)>
    
  5. 执行相同的命令从 SimpleStorage 智能合约中获取数据:

    truffle(nodefour)> SimpleStorage.deployed().then(function(instance) { return instance.get(); })
    

    得到的是下面的内容

    { [String: '0'] s: 1, e: 0, c: [ 0 ] }
    

    你会发现获取到的值为0, 这是因为节点4对应的账号不是合约中的私有成员.

  6. 最后, 我们测试通过节点7是否可以获取智能合约内容:

    Lastly we can try with node 7, which was privy to this contract.

    truffle console --network nodeseven
    

    得到的是下面的内容

    truffle(nodeseven)>
    
  7. 执行相同的程序, 从 SimpleStorage 中获取整型数据

    truffle(nodeseven)> SimpleStorage.deployed().then(function(instance) { return instance.get(); })
    

    得到的是下面的内容

    { [String: '42'] s: 1, e: 1, c: [ 42 ] }
    

    正如你看到的, 我们仍然得到了42, 这展示了我们可以仅对期望的人执行部署智能合约.

 与私有合约交互

此前介绍了如何通过 migration 部署私有智能合约, 当使用 Quorum 构建 dapp 的时候, 更广泛的应用场景是如何使所有的交易均保持隐私.

Truffle uses its truffle-contract contract abstraction wherever contracts are used in JavaScript. When you interacted with SimpleStorage in the console above, for instance, you were using a truffle-contract contract abstraction. These abstractions are also used within your migrations, your JavaScript-based unit tests, as well as executing external scripts with Truffle.

Truffle 使用 truffle合约 与抽象层交互, 无论合约是否是以 js 方式编写. 例如当在控制台中, 像前面一样使用 SimpleStorage 合约, 实际上是使用了 truffle-contract 合约抽象. 使用 Truffle 可以在部署合约(migrate), js单元测试, 执行外部部署代码中使用抽象层.

Truffle's contract abstraction allow you to make a transaction against any function available on the contract. It does so by evaluating the functions of the contract and making them available to JavaScript. To see these transactions in action, we're going to use an advanced feature of Truffle that lets us execute external scripts within our Truffle environment.

Truffle 的合约抽象层允许倚靠任何可用的合约方法来完成交易, 实现原理是预先将合约的方法转换成js方法. 想要验证交易效果, 我们将使用 Truffle 的一个高级属性, 在 Truffle 环境下执行外部js脚本.

  1. 在工程根目录下创建 sampletx.js 文件(与 truffle.js 文件同目录):

    var SimpleStorage = artifacts.require("SimpleStorage");
    
    module.exports = function(done) {
      console.log("Getting deployed version of SimpleStorage...")
      SimpleStorage.deployed().then(function(instance) {
        console.log("Setting value to 65...");
        return instance.set(65, {privateFor: ["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]});
      }).then(function(result) {
        console.log("Transaction:", result.tx);
        console.log("Finished!");
        done();
      }).catch(function(e) {
        console.log(e);
        done();
      });
    };
    

    This code does two things: First, it asks Truffle to get our contract abstraction for the SimpleStorage contract. Then, it finds the deployed contract and sets the value managed by SimpleStorage to 65, using the contract's set() function. As with the migration we wrote previously, the privateFor parameter can be appended within an object at the end of the transaction to tell Quorum that this transaction is private between the sender and the account represented by the given public key.

    代码完成两个任务, 首先让 Truffle 获取 SimpleStorage 合约的抽象, 然后找到部署好的合约, 使用 set() 设置value值为65. 我们前面编写的合约的 privateFor 值可以添加新成员, 来标记交易是基于发送者和 privateFor 指定的公有账号之间的私有链.

  2. 使用 truffle exec 执行代码:

    truffle exec sampletx.js
    

    输出内容类似于 (交易 ID 会不同):

    Using network 'development'.
    
    Getting deployed version of SimpleStorage...
    Setting value to 65...
    Transaction: 0x0a7a661e657f5a706b0c39b4f197038ef0c3e77abc9970a623327c6f48ca9aff
    Finished!
    
  3. 我们可以使用 Truffle 控制台, 像以往一样检查交易结果, 第一个节点:

    truffle console
    
    truffle(development)> SimpleStorage.deployed().then(function(instance) { return instance.get(); })
    

    结果为:

    { [String: '65'] s: 1, e: 1, c: [ 65 ] }
    

    得到 65! 第四个节点 (非交易私有成员):

    truffle console --network nodefour
    
    truffle(nodefour)> SimpleStorage.deployed().then(function(instance) { return instance.get(); })
    

    结果为:

    { [String: '0'] s: 1, e: 0, c: [ 0 ] }
    

    正如所想, 结果是0, 接下来查看节点7:

    truffle console --network nodeseven
    
    truffle(nodeseven)> SimpleStorage.deployed().then(function(instance) { return instance.get(); })
    

    结果为:

    { [String: '65'] s: 1, e: 1, c: [ 65 ] }
    

我们如期得到了65!! 这就是我们使用 Truffle 的智能合约抽象层来与 Quorum 共建私有交易的方法.

 这就是Truffle的全部功能么?

当然不是, 我们今天讲述的是为 Quorum 创建dapp的与公网创建的不同. 大部分内容都是相同的, 只是私有网络增加了 privateFor 参数来保证部署和交易的私有性. 其余内容完全相同!

实际上既然已经有基础了, 你可以继续浏览 我们其他的文档资源, 关于使用 Truffle 构建 dapp工程, 包括例子, 编写高级部署脚本, 单元测试 (使用Solidity), 等等.

使用 Truffle 开发, 不仅能够使用最好的开发工具和技术(例如Quorum), 还能够加入庞大的 Ethereum 开发者社区. 别犹豫了, 赶紧 来聊聊, 或者 从社区获取帮助, 这里总有一群愿意解决问题的朋友!

欢呼吧, 开启愉快的开发之旅!