为你的以太坊应用程序设计架构

当你即将开始以太坊开发,并查阅了一些很好的教程帖子后,创建你的首个以太坊应用程序就成为了你所面临的挑战。

这会带来一系列围绕在你的应用程序架构和蓝图设计周围的新挑战:传统的客户端-服务器应用程序中现在又有一个新部件,区块链

在本篇文章中,我将提及以太坊应用程序中最经典的一些场景。它们的出现源于这三个部件(客户端、服务器与区块链)间的不同互动。我将论述无服务器应用程序、浏览器插件、私有节点、离线签名和其他一些在设计你的解决方案蓝图时扮演重要角色的问题。

无服务器应用程序中的客户端-区块链

以太坊应用程序的典型风格是无服务器的(serverless),你应用程序的整个流程全部是在客户端和区块链之间进行的。

这里的第一步是向用户确切地分发客户端代码。最简便的方法是建立一个静态页面,其中要包含一个可用web 3(校对注:web3是一套以太坊客户端的API,有几种不同语言的实现,即客户端库,被用来通过JSON RPC接口与以太坊节点进行交互)的网页应用程序。任何地方都可托管这类页面:AWS S3、 Google Cloud、 Github pages、其它云服务供应商或你自己的服务器。另外,如果你能指望你的客户端支持bzz或ipfs协议,那么你就甚至可以通过 Swarm 或IPFS来分发代码以达到完全的去中心化。

查询区块链

下一步就是使应用程序能够读取区块链中的信息,而这正如你所知,需要连接到一个活跃的以太坊节点上。作为一个处理节点的 Web3 实际连接的组件,Web 3 provider 会设置好它。

现在,你的一些用户可能已经连接上一个节点了,比如说是通过官方 Mist 客户端或通过浏览器插件,如非常流行的 Metamask,它们对于区块链而言就像轻量级客户端。它们的常见问题答疑中甚至提供一个代码片段,告诉你如何检测它在客户端上是否可用,以及如何将它用作web3 provider。

// Adapted from https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#partly_sunny-web3---ethereum-browser-environment-check
window.addEventListener('load', function() {

  // Checking if Web3 has been injected by the browser (Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Use Mist/MetaMask's provider
    window.web3 = new Web3(web3.currentProvider);
  } else {
    // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)
    window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
  }

  // Now you can start your app & access web3 freely:
  startApp()
})

那么,那些没有 Mist 或 Metamask 的用户怎么办呢?如果你只是需要他们来查询区块链而并不发送任何交易,那么你可以连接一个公共以太坊节点。这得是一个向公众开放的 geth 或 parity 节点,但没有暴露账户管理的个人 API,也没有何解锁的账户。这样的节点只能用作查询固定的合约函数的网关。如果你不想自己作为宿主提供这样的节点,在 Infura(校对注:一个提供区块链基础服务的网站)上可以找到无偿提供的公共节点服务。

这样,有正在运行的以太坊基础设施的用户可以连接到的他们信任的节点,而不那么精通技术的用户则可使用公共节点。这意味着为了便于使用,后者将选择相信由第三方控制的节点所提供的信息。

发送交易

查询区块链容易,但是如果你想要你的用户发送交易并在智能合约上真正采取改变状态的行动,那该怎么办呢?对于你那些安装了Mist或Metamask的用户,问题立马就能解决,只需为他们提供一种管理帐户、在应用程序需要的时候请求用户同意交易的方法。Mist会提供一个通向用户本地节点及其帐号的途径以签署交易,而Metamask则在客户端签署交易,并将其中继到Metamask公共节点上。

-源于用Truffle和MetaMask开发以太坊去中心化应用程序 -

对于你的其他用户,如果你不要求他们安装Metamask或使用Mist来用你的应用程序,那你就需要指导他们从他们任一钱包里手动发送交易来与你的应用程序进行交互。

大多数应用程序是通过让用户发送一定量的以太币到一个地址来实现这种互动的,该地址可选地包括一个二维码或一个复制到剪切板的按钮。

-Shapeshift 以太坊交流对话框-

当带外交易(Transaction Sent Out-of-band)发送(校对注:指把交易数据发送到已连接的本地节点或公共节点上)的时候,你的客户端应用程序应当及时监控合约事件以更新用户界面。由于监控事件只是查询区块链的一种简单的方法,所以通过一个公共节点就能轻松实现。

现在,为了执行合约函数,你可能需要请求用户发送以太币和附加数据来执行特定函数。你可以用合约函数中的 Request 方法轻松获得运行该方法所需的数据,并将数据和目标地址呈现给用户。

SimpleToken.at(tokenAddress).transfer.request("0xbcfb5e3482a3edee72e101be9387626d2a7e994a", 1e18).params[0].data
// returns data for invoking transfer with args => '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000de0b6b3a7640000'

就个人而言,我不是这一模式的狂热粉丝,因为它需要用户进行相当复杂的操作。不是所有的用户都知道附加数据是如何在以太坊中发挥作用的,更不用说如何在交易中发送数据。

如果你确实要以这种方式实现,请确保你合约的 fallback函数拒绝所有支付款项或有一个合理的默认行为,这样如果用户在手动发送交易时忘记了将附加数据包含在内,他不会失去所发送的资金。

注意,在很多情况下,一个巧妙设计的fallback 函数可以一直为发送交易的用户执行预期行为。这样的话,你的智能合约只会对用户发送款项来作出反应,并根据当前的状态(合约状态)运行不同的函数(合约函数),还不会要求用户设置任何附加数据。

另一个技巧就是添加代理合约,一旦收到以太币,每一个代理合约就要执行你主合约中的一个特定合约函数。举一个简单的例子,比如说你正在执行一个二进制投票应用程序:你会在主合约中加入一个vote-yes函数(投赞成票)和一个vote-no函数(投否决票)。与其要求用户将赞成或否决的标记包含在交易内,你可以部署两个附加合约,无需其他逻辑,只需代表信息发送者在收到任何交易时调用主合约中的vote-yes函数或vote-no函数(相应在主合约中投赞成票或否决票)。不用说,这个方法只适用于非常简单的应用程序,但经常能帮助那些浏览器里没有以太坊识别软件的用户使得难度降低。

创建你自己的钱包程序

让你的用户与你的智能合约进行复杂互动的另一选择实际上是将钱包管理加进你的应用程序中。对于用户来说,第一步是应用程序可以为他们创建一个新的帐户,而该账户可用于直接通过你的代码发送任何交易。

-Coral Fundraiser 向新生成的账户要一个密码,可以下载并加密地保存到用户的电脑上,以便用户之后可以在在其他会话中乃至用另一个钱包来访问它。

用户之后将需要转入一些以太币以便可以使用这一账户,要么从另一个账户转入,要么直接从交易所购得;在这里可以考虑将 Shapeshift 集成进来以便于使用。一旦账户上有支付交易费用所需余额,客户端代码就可以代表用户通过一次简单点击来执行任何所需操作。

在这种场景下,你的应用程序将使用所生成账户的私钥来签署任何客户端交易,然后将其转发到一个公共节点去执行。

这一模式涉及繁重的编程任务,因为你需要添加对一个以太坊账户的生成、加密和导入功能(你可能想看看以太坊-钱包库是如何实现这些功能的)。另外,所有的交易需手动精心制作和签署,然后作为原始交易发送到一个节点。这还需要用户的更多参与,其任务是建立一个新账户,且有义务保护账户文件的安全。不过,一旦条件齐备,你的用户就可以毫无障碍地直接从你的应用程序中进行以太坊交易,无需安装任何软件。

总而言之,你采用哪种方法将主要取决于你接触的这类用户以及你期待用户与你的应用程序进行的这类互动,无论是持续使用还是偶尔使用,甚至是一次性访问。

服务器到区块链

现在,让我们在上述结构中添加一个服务器,并在这一节里先不管客户端。同样地,接下来的内容可能不仅适用于应用程序服务器,还适用于独立应用程序、脚本或批处理程序。

建立一个本地节点

第一个解决办法很普通:建立一个本地以太坊节点,并在你的程序中使用其 JSON RPC 接口来进行你需要的区块链操作。

你可能也想要留一个未锁定帐号来运行你应用程序中的交易( Geth 和 Parity 的解锁标志可能在这里会派上用场)。必须确保节点的 JSON RPC 接口除你的应用程序之外其它地方都无法访问,否则任何人都有可能进入你的节点并盗取你的资金。有一个额外的预防措施,未锁定账户里的钱保持在最小值即可,并在需要时从另一个地方将其取出。

离线签名和公共节点

另一个解决办法,和上一节提到的解决办法类似,就是让你的应用程序离线签署交易,并将其转发到一个公共节点。本文探讨如何在你的 Truffle 配置里建立一个web3provider engine来透明化地离线签署交易,并将其发送到一个Infura节点上。

如果你按这条线路走下去,记住你这是在盲目相信你所连接的公共节点:虽然这个节点无法更改你发送过去的交易,但它可以选择不转播到网络,或向你的查询提供假回应。这一点可通过同时连接多个节点,并将同样的交易或查询发送给所有的节点来缓解;但是这会让你的代码库复杂很多。

全盘了解

从头到尾说了一遍设置客户端-区块链和服务器-区块链查询和交易的不同方法后,是时候来讨论如何将一切安排协调起来了。

协调客户端和服务器

客户端和服务器与区块链同时交互暗示着可能两者你都需要协调。比如,你可能想让服务器对客户端链上执行的操作作出反应,或在客户端中将合约状态的变动可视化。

客户端和服务器中观察合约变化的规范方式是监听合约事件。仔细设计你自己的事件,以确保所有相关行为都有一关联事件,并使用索引参数,这样观察者可以只筛选与之相关的事件。一般来说,客户端只会监听直接影响它们的事件,而服务器可能会监视所有与应用程序相关的合约。

如果交易是通过你的应用程序代码直接发出的,那你也可以直接监视具体交易以检查它们是否成功。

客户端也可以把它们发布的交易号发送到服务器来作为链上执行行为的证据,而不是让服务器来监听事件。但是,要记住,恶意行动者可能会监视链上交易,并可能将一个来自于不同客户端的交易号发送给服务器,假装这是属于他的。确保只将客户端到服务器的信息当作通知,而不是最终事实的来源。

无论是靠监视交易或事件,都要确保只有经过合理数量的确认后才能对其做出响应。即使你有一个交易在挖矿,它仍会链重组的影响,并最终在不同的环境中运行,也可能会变得无效。在对链上事件做出响应前,大概等十二个区块(测试网中需要更多),但是可以考虑让你的终端用户知道交易是成功了,但未经过足够数量的确认,这样他们不会被蒙在鼓里。

服务器职责

现在,你需要回答的一个关键问题就是到底为什么你需要一个服务器。在传统的客户端-服务器应用程序中,服务器作为一个永久储存器,执行业务逻辑,并协调客户端;现在所有任务都可以在链上完成。

但是,服务器仍有很多用途可以支持你的应用程序。首先,链上代码无法直接与链外服务一起工作。这意味着如果你想要与第三方服务结合起来,注入像美元/以太币利率这样的外部数据,或执行如发送邮件这样的基本操作,你就需要一个服务器来做到这些。

服务器也可充当你智能合约的高速缓冲存储器或索引引擎。虽然事实的最终来源是区块链,但是你的客户端可以依赖服务器的搜索功能,并证实链上返回的数据。

如今以太坊的大数据存储极其昂贵,这是由使用合约存储时产生的gas成本造成的。同样地,你的应用程序可能也会靠服务器来储存大数据块,而只有一个散列是保存在链上进行验证的。

这也同样发生在复杂的计算中,因为复杂的计算所消耗的gas也许会超过以太坊区块的gas上限,所以需要在链外的基础架构中运行。

值得一提的是越来越多关于以太坊虚拟机(EVM)的项目正在出现,这些项目为这些服务提供了无缝集成。例如用于存储的 Filecoin或Storj ,用于计算的 Truebit,或用于预言机(校对注:为区块链应用获取由第三方Web API提供的数据,比如天气、股票等公共数据)的 Oraclize 。

最终,服务器可能变得越来越瘦,直到它们消失在无数侧链服务和集成中。可能,几年后,这个帖子将过时,我们的区块链应用程序将会以一种绝对分散的方式运行。

如果你对讨论智能合约安全感兴趣,请加入我们的slack channel,在Medium上关注我们或申请与我们共事!智能合约安全系统开发和审计工作也可以找我们。

原文链接: https://blog.zeppelin.solutions/designing-the-architecture-for-your-ethereum-application-9cec086f8317
作者: Santiago Palladino
翻译&校对: 李丽 & Elisa、风静縠纹平

本文由作者授权 EthFans 翻译及再出版。

你可能还会喜欢:

INFURA如何解决以太坊的其他大规模挑战
使用 Infura 和 web3.js 呼叫合约
代币支付的以太坊智能服务

评论