前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Truffle Provider 构造及其解释

Truffle Provider 构造及其解释

作者头像
lambeta
发布2019-02-26 14:37:09
1.4K0
发布2019-02-26 14:37:09
举报
文章被收录于专栏:编舟记编舟记

Truffle default web3 HttpProvider

定义

Truffle[1] 是以太坊上智能合约的开发环境,类似Java语境下的Maven或者Gradle工具,但是内涵更加丰富,除了编译链接,它还兼顾了智能合约的部署,部署需要适配多样的目标环境,例如本地Ganache模拟的开发网络、以太坊测试网络(Ropsten Rinkeby or Kovan Net)、以太坊主网(Main Net)。Truffle 为此提供了provider配置选项,默认使用Web3JS中定义的Web3.providers.HttpProvider,它会使用hostoption选项构造出目标地址http://<host>:<port>,如下所示。

代码语言:javascript
复制
module.exports = {
  networks: {
    development: {
      host: "localhost",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};

一般来说,我们使用Ganache-cli(以太坊客户端的简单实现)模拟出一个以太坊节点,然后监听在8545端口,形成这套本地开发环境就足够支持调试合约。不过我们得了解,Truffle和Ganache在这里隐藏了很多细节,仔细思考一下以太坊的编程模型,创建一份智能合约本质上就是发送一条交易,即通过RPC调用sendTransaction[2]或sendRawTransaction[3]方法,这两者的区别在于后者发送的数据是签过名的,而前者没有。因为Truffle的配置文件并没有声明钱包(即公私钥,公钥用来产生地址和验证交易签名,私钥用来给交易数据签名),所以Web3.providers.HttpProvider只能利用sendTransaction创建合约。那么问题来了,创建合约的这条交易数据是什么时候被签名的呢?

签名的时机

当调用sendTransaction函数创建合约时,合约数据是没有被签名的,那么很容易想到,唯一能签名的地方就是以太坊的客户端了。在开发环境下,Ganache启动时会自动帮我们生成10个账号,也即10对公私钥。当我们使用Truffle部署(创建)合约时,默认会使用第一个账号web3.eth.accounts[0]对应的私钥签名合约数据。为了验证假设的正确性,我们可以设计两个试验。

Ganache-cli 在执行部署合约时,会把Truffle调用的JSON RPC方法名打印出来,所以按照以太坊JSON RPC规范的定义[4],一定会存在获取所有账号的调用eth_accounts

JSON RPC

为了简单验证,我们用truffle init创建一个项目,并启动ganache-cli命令行程序。当执行truffle deploy命令时,Ganache的命令行中会打印出如下的RPC命令。

代码语言:javascript
复制
net_version
eth_accounts
eth_getBlockByNumber
...

其中,net_version返回了当前网络的ID,对于以太坊而言,1是主网,3是测试网络Ropsten,4是测试网络Rinkeby,42是测试网络Kovan。

eth_accounts是在migrate(truffle deploy的别名)过程中被调用到的,具体逻辑我们可以查看truffle-migrate/migration.js文件,文件中有一个异步公开的async run(option, callback)函数,它调用了子函数self._load(options, context, deployer, resolver, callback),这个load函数的用途是加载账号等资源并执行部署任务。代码如下:

代码语言:javascript
复制
const Deployer = require("truffle-deployer");
const Require = require("truffle-require");

// self._load(options, context, deployer, resolver, callback)
const accounts = await context.web3.eth.getAccounts(); // 获取账号
...
Require.file(requireOptions, async (err, fn) => {
...
  try {
    const migrateFn = fn(deployer, options.network, accounts);
    await self._deploy(options, deployer, resolver, migrateFn, callback);
  } catch (err){
    callback(err);
  }
});

这段代码中有两处需要特别留意。第一处就是通过RPCeth_accounts获取账号,这个验证了我们之前的假设。第二处是Require.file(..., async (err, fn) => {...})中的fn回调函数,这个回调函数其实就是Truffle项目中migrations目录下的迁移脚本里module.exports导出的函数,例如:1_initial_migration.js

代码语言:javascript
复制
var Migrations = artifacts.require("./Migrations.sol");

module.exports = function(deployer) {
  deployer.deploy(Migrations);
};

详细的调用逻辑需要到truffle-require/require.js中寻找,大意是Require.file(..., done)提供的done回调参数,最后函数体中回调了done(null, m.exports)

当然,以上的分析都是细节。可以学到的重要的知识点是,Truffle的迁移脚本中导出的函数其实接收了三个参数,所以可写成如下方式:

代码语言:javascript
复制
module.exports = function(deployer, network, accounts) {
  deployer.deploy(Migrations);
};

这样设计有个很有用的地方,部署合约时,可以切换成不同的账号。

到这里,虽然我们验证了Truffle在部署时,确实会获取账号eth_accounts,但是还没有验证Truffle是否会取用第一个账号,而且我们最终的目的是验证签名发生在以太坊的客户端上,此处的客户端便是Ganache。接下来,我们设计一个把第一个账号锁起来的试验,按照定义,所谓锁账号[5],就是把账号对应的私钥从内存中移除,使得该账号无法发送交易。如果试验结果是Truffle无法部署合约,那么就验证了假设,即签名发生在以太坊客户端上,和Truffle无关。这点很重要,因为它是我们在项目中使用Truffle HD Wallet Provider的理由之一。

锁账号

按照试验设计思路,Ganache启动完毕,我们通过命令truffle console直接连接到Ganache客户端上。假设此处我们第一个账号地址是0x2462faca627e2b8511a7c362e4e0bf524b7fa368,执行命令如下:

代码语言:javascript
复制
web3.eth.personal.lockAccount('0x2462faca627e2b8511a7c362e4e0bf524b7fa368')

true

同时,Ganache的控制台也会输出personal_lockAccountRPC命令。

然后,我们开始执行部署命令truffle deploy,不出所料,部署失败,抛出的错误是"Migrations" -- Returned error: signer account is locked。而在另一边,Ganache控制台停在了eth_sendTransactionRPC命令那一步。

小结

从上面两个试验的结果得出结论:在默认Provider是Web3.providers.HttpProvider情况下,Truffle部署合约并不会签名交易数据,而是交由以太坊客户端处理,以太坊客户端会找到未锁的账号,拿出私钥对交易数据进行签名,然后发送到当前网络。

不过,这种部署方式显然存在问题。第一,长期不锁账号,想要部署就必须保持客户端中有未锁的账号,这样很不安全。而且通过暴露Personal RPC接口以便解锁账号就会涉及密码的传输问题,也很不安全,尤其是暴露在互联网上;第二,我们部署合约的客户端可能并不是自己的,很多第三方提供了现成的API,比如:infrua,人家比我们搭建的客户端要稳定。那么此时你不可能要求infrua[6]解锁我们自己的账号,也不大放心把私钥放到它上面。

有了上面提到的两个问题,自然而然我们需要一个更好的部署方案了,这个方案就是Truffle HDWallet Provider.

Truffle HDWallet Provider

定义

truffle-hdwallet-provider[7] 是基于HD Wallet(可以从我之前介绍BIP32、BIP39和BIP44了解)的Web3 Provider。Wallet就意味着公私钥,所以不难想象它就是预先用来对合约数据进行签名的,然后调用sendRawTransaction把创建合约的交易发送到网络。那么,Truffle是如何做到的呢?我们先列出几个关键概念,然后用代码解释。

关键概念

  1. Web3 provider engine
  2. HookedSubprovider
Web3 provider engine

要弄清楚Web3 provider engine,首先得搞明白什么是Web3。Web3[8]是一组和以太坊客户端交互的JSON RPC API的定义。而Provider就是执行RPC命令的程序,例如专门设计用HTTP请求发起JSON RPC调用的HttpProvider就是其中之一。

Web3 provider engine是MetaMask[9]这个组织下一款开源工具,用来组合不同的Web3 provider,这些provider可能各自实现了Web3定义的部分功能,所以也被称为SubProvider。比如,你可以只使用Web3的filter功能,如下:

代码语言:javascript
复制
const ProviderEngine = require('web3-provider-engine')
const FilterSubprovider = require('web3-provider-engine/subproviders/filters.js')

var engine = new ProviderEngine()
var web3 = new Web3(engine)
engine.addProvider(new FilterSubprovider())

上面的代码也说明了Web3 provider engine是所有SubProvider的入口。

HookedSubprovider

HookedSubprovider和FilterSubprovider在概念上类似,不过它通过使用eth_sendRawTransaction模拟了eth_sendTransaction调用,造成一种假象,我们好像调用的是sendTransaction函数,这样做的目的应该是为了兼容以前的代码。

代码解释

先去HDWalletProvider里一窥究竟,关于BIP39定义的Mnemonic不是本篇的重点,对于理解HDWalletProvider的运作影响也不大,所以忽略不讲。直接进入往Provider engine中添加SubProvider的代码逻辑里。

代码语言:javascript
复制
this.engine.addProvider(
    new HookedSubprovider({
      getAccounts: function(cb) {
        cb(null, tmp_accounts);
      },
      getPrivateKey: function(address, cb) {
        if (!tmp_wallets[address]) {
          return cb("Account not found");
        } else {
          cb(null, tmp_wallets[address].getPrivateKey().toString("hex"));
        }
      },
      signTransaction: function(txParams, cb) {
        let pkey;
        const from = txParams.from.toLowerCase();
        if (tmp_wallets[from]) {
          pkey = tmp_wallets[from].getPrivateKey();
        } else {
          cb("Account not found");
        }
        const tx = new Transaction(txParams);
        tx.sign(pkey);
        const rawTx = "0x" + tx.serialize().toString("hex");
        cb(null, rawTx);
      },
      signMessage(message, cb) {...}
    })
  );

在实现HookedSubprovider时,我们看到了getPrivateKeysignTransaction函数,这两个函数存在的意义就是为了协调eth_sendRawTransaction发送签名后交易数据。其中,signTransaction函数中使用库ethereumjs-tx对交易参数做了签名处理。看上去疑问已经被澄清了,但是,在那之前我们还有几个问题需要弄清楚,第一点就是自定义的SubProvider是如何被调用到的?

Provider、SubProvider接口和SubProvider的调用时机

Provider接口定义很简单,只有一个sendAsync函数,顾名思义,以异步的方式发起JSON RPC调用。

代码语言:javascript
复制
export interface Provider {
    sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): void;
}

SubProvider接口则定义了一个handleRequest(...)需要子类敷写。

代码语言:javascript
复制
SubProvider.prototype.handleRequest = function(payload, next, end) {
  throw new Error('Subproviders should override `handleRequest`.')
}

在我们执行truffle deploy的时候,整个调用链条大体像下面这样:

代码语言:javascript
复制
deployer.deploy(YourContract) -> new TruffleContract(args) -> new Web3.eth.Contract(args) -> this.eth.sendTransaction(options,  cb) -> method.requestManager.sendAsync(payload, cb)

链条的终点回到了eth.sendTransaction,并且最终交给Provider的sendAsync函数执行,此处的requestManager就是Provider接口的实例。

在Web3 provider engine(它是调用所有SubProvider的入口)的文件中,可以看到sendAsync的实现如下:

代码语言:javascript
复制
Web3ProviderEngine.prototype.sendAsync = function(payload, cb){
  const self = this
  self._ready.await(function(){

    if (Array.isArray(payload)) {
      // handle batch
      map(payload, self._handleAsync.bind(self), cb)
    } else {
      // handle single
      self._handleAsync(payload, cb)
    }
  })
}

_handleAsync这个函数又会按链式结构依次调用到所有SubProvider的handleRequest(payload, next, end)函数,截取的代码片段如下:

代码语言:javascript
复制
var provider = self._providers[currentProvider]
provider.handleRequest(payload, next, end)

这样,我们就弄明白了SubProvider的调用时机。那么接下来的两个问题是交易什么时候被签名的?sendTransaction什么时候被替换成sendRawTransaction的?

顺其自然地,我们进到自定义的HookedSubprovider中,研究它的handleRequest函数,其中有段switch...case在利用方法名做函数调用的分配:

代码语言:javascript
复制
case 'eth_sendTransaction':
    txParams = payload.params[0]
    waterfall([
      (cb) => self.validateTransaction(txParams, cb),
      (cb) => self.processTransaction(txParams, cb),
    ], end)
    return

我们再经由processTransaction进入函数finalizeAndSubmitTx(...)中,这个函数就是我们的答案,它的职责是签名交易并把交易发送到全网,这其中必然会涉及函数替换。

代码语言:javascript
复制
HookedWalletSubprovider.prototype.finalizeAndSubmitTx = function(txParams, cb) {
  const self = this
  // can only allow one tx to pass through this flow at a time
  // so we can atomically consume a nonce
  self.nonceLock.take(function(){
    waterfall([
      self.fillInTxExtras.bind(self, txParams),
      self.signTransaction.bind(self),
      self.publishTransaction.bind(self),
    ], function(err, txHash){
      self.nonceLock.leave()
      if (err) return cb(err)
      cb(null, txHash)
    })
  })
}

不难看出,self.signTransaction.bind(self)就是之前自定义在Truffle HDWallet Provider中的signTransaction函数,交易签名的问题解决了。我们再查看一下publicTransaction函数。

代码语言:javascript
复制
HookedWalletSubprovider.prototype.publishTransaction = function(rawTx, cb) {
  const self = this
  self.emitPayload({
    method: 'eth_sendRawTransaction',
    params: [rawTx],
  }, function(err, res){
    if (err) return cb(err)
    cb(null, res.result)
  })
}

eth_sendRawTransaction出现了,这正是sendTransaction被替换成sendRawTransaction的证据。诚如之前分析的一样,这个HookedSubprovider利用eth_sendRawTransaction模拟了sendTransaction的操作。至此,总算搞明白Truffle HDWallet Provider的运作机制.

小结

Truffle HDWallet Provider 给开发者提供一种能力,那就是在省去了自己搭建以太坊客户端的同时,也兼顾了私钥的安全性。

最后,我们看看Truffle完整的配置文件(truffle-config.js)大概是什么样子的,如下:

代码语言:javascript
复制
var HDWalletProvider = require("truffle-hdwallet-provider");

var mnemonic = "mountains supernatural bird ...";
module.exports = {
    networks: {
      mainnet: {
          provider: function() {
              return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/<infura-key>')
          },
          network_id: '1',
          gas: 4500000,
          gasPrice: 10000000000
       }
    }
}

总结

Truffle 这个开发环境在节约开发者时间的同时,也引入了很多理解上的障碍层。而提出假设,然后动手实验或者阅读源码是检验假设的最好方法。希望我们在高效开发DApp时,也不要忘记思考手中武器的构造,尝试解释它们。


  1. Truffle Framework
  2. Ethereum web3ethsendtransaction
  3. Etheurem web3ethsendrawtransaction
  4. Ethereum JSON-RPC
  5. Ethereum personal_lockAccount
  6. Ethereum service provider - Infura
  7. Truffle HDWallet Provider
  8. Web3
  9. MetaMask
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.01.26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Truffle default web3 HttpProvider
    • 定义
      • 签名的时机
        • JSON RPC
        • 锁账号
      • 小结
      • Truffle HDWallet Provider
        • 定义
          • 关键概念
            • Web3 provider engine
            • HookedSubprovider
          • 代码解释
            • Provider、SubProvider接口和SubProvider的调用时机
          • 小结
          • 总结
          相关产品与服务
          访问管理
          访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档