1总览

本章主要是对Reach和Reach程序结构进行了非正式的概述。本文的目标是给予你更多技术细节以帮助你更好地理解Reach的功能,但它并非像教程参考资料那样。当你已经准备好开始实施一个项目时,你可以从其中一个开始,甚至是从研讨会开始。

如果你有使用已有工具进行区块链开发的经验,我们推荐您阅读本文并与其他开发平台做一下比较

关于这些材料的现场研讨会录音可在YouTube上查阅

1.1``去中心化应用

DApp)是由多个通过后端共识网络)相互交互的代理构成,就像以太坊和Algorand那样。这些代理能够通过授权信息代表委托人进行行动。这些委托人可以是人类或是其他自动化代理,甚至可以是有内部结构的委员会或组织。共识网络允许这些代理发送或接收网络特定的代币,如ETH和ALGO。这些网络同样允许创建合约以确保所有代理都能按照一定的规则计算和发送有价值的代币。这些合约细节是共识网络特有的,但它们能够隐含被所有代理和委托人信任,因为对合约的操作能够被独立验证是否符合先前商定的规则。

一个Reach程序包含了所有DApp)所需要的特性:

  • 参与人)后台)是能够代表委托人的代理。
  • 前端)通常是是参与者)与委托人之间接口的技术表现形式。
  • 一个合约)能够实施程序的规则和操作顺序。

在Reach中,程序开发者仅需要指定参与人)的行为—独立和联合起来各需要做什么。Reach编译器能够通过连接器)按规则为共识网络)自动推导出一个合约)。

1.2``一个最小化的Reach程序

我们来看一个有Alice和Bob两个参与人参加交互的Reach程序。在这一DApp)中,Alice有部分有价值的信息希望以共识网络代币为交易形式最终将信息销售给Bob。

通过访问overview/index.rsh能够看到所有的样例程序

在编辑其中访问IDE/Text Editor Support以获取针对Reach开发语言的支持

程序的主要部分如下所示:

  1. 1 'reach 0.1';
  2. 2 'use strict';
  3. 3
  4. 4 export const main =
  5. 5 Reach.App(
  6. 6 {},
  7. 7 [Participant('Alice', { request: UInt,
  8. 8 info: Bytes(128) }),
  9. 9 Participant('Bob', { want: Fun([UInt], Null),
  10. 10 got: Fun([Bytes(128)], Null) })],
  11. 11 (A, B) => {
  12. .. // ...body...
  13. 31 } );
  • 第1行表明这是一个Reach程序。
  • 第2行表明该程序可以使用严格模式)进行编译,该模式会对未用变量进行检查。
  • 第4行定义了程序主要的导出)形式。其中的main仅仅是Reach默认使用的样例。
  • 第5行表明这是一个应用。
  • 第7和第8行指定了Alice的参与人)和前端)。在该情况下,Alice的前端)必须提供一个名为request的数字和一个名为info的字符串。
  • 第9和第10行指定为Bob准备的接口,该接口包含了一个名为want的函数,该函数的输入为一个数字并返回null 空值,以及一个名为got的函数用于接收信息。
  • 最终在第11行,将参与人)绑定到程序标识符AB处。

从12行到30行包含了应用的内容,可以将其分为四个部分。

..    // ...
12    A.only(() => {
13      const request = declassify(interact.request); });
14    A.publish(request);
15    commit();
..    // ...
  • 第12和13行表明Alice需要发起一个本地步骤)以便对其需求的代币数进行解密)。在Reach中,所有从前端)传递的秘密)数据会在进行解密)操作后得到公开)。
  • 第14行Alice通过公开值及能够表明合约)进行什么操作的程序逻辑以加入)应用程序。、
  • 第15行让合约)提交该值并继续进行余下的程序部分。

在这一时刻,Bob的后端)已知晓 request 对应值并将其发送至Bob的前端)以获取其许可。这将触发下一段程序逻辑。

..    // ...
17    B.only(() => {
18      interact.want(request); });
19    B.pay(request);
20    commit();
..    // ...
  • 第17行和18行使Bob执行这一交付过程。其中 interact.want 不会显式的返回一个boolean值,原因是如果Bob不愿意继续操作那么前端不会返回任何内容。一个更加好的版本可能是该程序能返回 false 且向Alice进行传达。
  • 第19行和第20行使Bob加入)到应用程序中并提交符合要求数量的支付费用并使合约)提交。

现在又轮到Alice操作了,

..    // ...
22    A.only(() => {
23      const info = declassify(interact.info); });
24    A.publish(info);
25    transfer(request).to(A);
26    commit();
..    // ...
  • 第22和第23行指定了Alice需要解密)的信息。
  • 第24行Alice公开了这一信息。
  • 第25行使合约)交易了Alice请求数量的费用给其本人。
  • 第26行将交易提交至共识网络)。

唯一剩下的就是Bob的后端)需要将信息传递给他的前端)。

..    // ...
28    B.only(() => {
29      interact.got(info); });
30    exit();
..    // ...
  • 第28和29行进行了这项工作。
  • 第30行退出了这一程序。

Reach程序开发者无需考虑诸如合约存储协议流程图状态验证公示网络细节等信息;取而代之的,他们仅仅只需要关注其应用的业务逻辑即可。

1.3``编译

在Reach开发者像文件overview/index.rsh一般写好了应用,则他们可以运行

$ reach compile overview/index.rsh

编译后的目录会包含一个名为index.main.mjs的新文件,该文件内包含使用JavaScript语言为每一位参与者实现的后端),以及合约)的以太坊字节码。

如果你好奇的话,可以去proview/build/index.main.mjs看看这个文件。Ethereum字节码是不可读的,但如果你了解Solidity,你可能会想看看 overview/build/index.main.sol,看看它编译的原始Solidity源码。Reach 在使用 -intermediate-files 运行时,可以保留这样的文件。

对于这样一个31行的应用,Reach编译器会生成包含2个函数的915行JavaScript代码,其中一个是为Alice生成,另一个是为Bob生成。此外,他还生成了147行Solidity代码来实现合约。如果程序员没有使用Reach,他们则必须在这三个模块中分别编写构成这1062行代码,并且还需要在开发过程的每一步都使程序保持同步。

此外,Reach不仅能在以太坊工作:其对区块链底层不敏感的特性可以轻松地配置以使用不同的连接器)以针对其他公示网络)进行开发,就像Algorand那样。此外Reach也不仅仅只与JavaScript绑定,它能够被配置以适配诸如Go这类的后端)语言。

1.4``验证

Reach 不仅仅是编译你的程序,它还会对程序进行验证,确保错误不会发生。例如,它总是保证程序结束时合约)中的余额为零。这一点很重要,因为如果不这样处理,那么代币就会被合约)锁定,无法访问。

对于这一样例程序,能够明显发现交易请求在第18行发出,而针对交易请求的交易在第24行实施,随后在程序终止时,余额变为0。不过,我们可以做一个小小的调整,来证明出现错误了。

我们可以修改第三步骤以留下一点点资金:

..    // ...
21    A.only(() => {
22      const info = declassify(interact.info); });
23    A.publish(info);
24    transfer(request-1).to(A); // <--- Oops!
25    commit();
..    // ...

随后运行编译器

$ reach compile overview/index-error.rsh

它将打印出详细的错误信息,显示存在违规行为。

.. // …
2 Verifying for generic connector
3 Verifying when ALL participants are honest
4 Verification failed:
5 when ALL participants are honest
6 of theorem: assert
7 msg: “balance assertion”
8 at ./index-error.rsh:29:11:application
9
10 // Violation witness
11 const interact_Alice_request = 1;
12 // ^ from interaction at ./index-error.rsh:4:12:application
13
14 // Theorem formalization
15 assert(0 == (interact_Alice_request - (interact_Alice_request - 1)));
16
.. // …

验证失败中包含大量的信息,如一个具体的反例:显示了可能由前端)提供的值,这些值会导致相关属性的不成立。在这一情况下,如果Alice在第4行的程序开始时将一个interact.request传给1,那么在程序结束时,合约的余额将不能被证明为0

Reach的程序开发者不需要担心这些问题,因为编译器已自动对其代码检查并确保这些问题不会发生。事实上,关于自动验证的细节还有很多,的确,它是Reach最强大的功能之一,但我们暂时不会展开其内容。

1.5``接口

事实上Reach编译器生成的后端并不是一个应用。特别的,每一个参与者)需要一个前端)来与之进行交互。在实际的部署中,交互代码通常与GUI界面结合实现,例如一个网页前端或智能手机应用。我们可以参考一个能在私有开发链上进行测试的命令行版本。

通过访问overview/index.mjs你能够看到整个接口程序的样例。

该程序仅仅有23行,且其命令非常简单:

 1    import { loadStdlib } from '@reach-sh/stdlib';
 2    import * as backend from './build/index.main.mjs';
 3    
 4    (async () => {
 5      const stdlib = await loadStdlib();
 6    
 7      const accAlice = await stdlib.newTestAccount(stdlib.parseCurrency(5));
 8      const accBob = await stdlib.newTestAccount(stdlib.parseCurrency(10));
 9    
10      const ctcAlice = accAlice.deploy(backend);
11      const ctcBob = accBob.attach(backend, ctcAlice.getInfo());
12    
13      await Promise.all([
..        // ...
22      ]);
23    })();
  • 第1和第2行导入了Reach标准库装载器及已编译好的应用后端。
  • d第5行动态加载了针对网络设计的Reach标准库,基于REACH_CONNECTOR_MODE这一环境变量。如果没有指定则Reach标准会按照以太坊作为共识网络进行处理。Reach 的所有网络专用标准库都遵循一个通用的接口,这使得您可以编写对共识网络不敏感的程序。
  • 第7行和第8行为Alice和Bob初始化了最新的测试账户。
  • 第10行使Alice在共识网络中部署了智能合约。
  • 第11行使Bob与合约建立了联系。其中 ctcAlice 的值没有包含任何秘密信息,能够轻松地与Bob在共识网络外进行分享。
  • 第13到22行启动了后端并等待其完成操作,我们将在随后介绍这一部分。

这段代码对于所有的测试程序来说都会非常相似,并且展示了如何直接为Reach应用程序的测试提供框架。

从Alice开始,我们可以看一看每个初始化参与者的过程及接口。

..    // ...
14    backend.Alice(ctcAlice, {
15      request: stdlib.parseCurrency(5),
16      info: 'If you wear these, you can see beyond evil illusions.'
17    }),
..    // ...
  • 第14行提取了Alice的后端。
  • 第14行同时将后端传递至合适的标准库和合约句柄。这需要他们能够与共识网络进行交互。
  • 第15行提供了 request 值。
  • 第16行提供了info 对应的值。

接下来我们看看Bob是什么情况。

..    // ...
18    backend.Bob(ctcBob, {
19      want: (amt) => console.log(`Alice asked Bob for ${stdlib.formatCurrency(amt)}`),
20      got: (secret) => console.log(`Alice's secret is: ${secret}`),
21    }),
..    // ...
  • 第18行的操作就像初始化Alice那般,对Bob进行初始化。
  • 第19行提供了 want 函数,用于产生日志信息,且总会被允许执行。
  • 第20行提供了 got 函数,用于在控制台显示秘密信息。

除了那些直接影响业务决策的细节,如交易货币数量,Reach已将所选共识网络)的所有细节从程序员处完全抽象出来。Reach允许程序员在每个阶段重点关注其应用程序的业务逻辑,例如从核心应用程序到接口元素。

1.6``执行

现在到了执行这一测试程序并保证所有模块能够正确工作的时间。在这一情况下,我们简单地设置部署了我们的应用程序:其中有一个Reach文件作为应用,一个JavaScript文件作为交互接口。这是一种常见的做法,所以Reach自带了一个简单的包装脚本来构建和执行这种应用,我们只要运行:

$ reach run

随后Reach会帮助我们

  • 编译overview/index.rsh
  • 创建一个临时的Node.js包;
  • 基于Reach包的标准镜像构建Docker镜像;以及,
  • 运行连接到Reach的标准私有以太坊开发测试网镜像的应用程序。

在典型的开发者笔记本电脑上,整个过程只需要几秒钟,并且可以完全集成到现有的开发IDE中,比如VSCode,因此Reach开发者可以通过一个命令来编译、验证、构建、启动和测试他们的Reach应用。

Reach将构建和维护共识网)测试环境和构建脚本的所有细节完全从程序员身上抽象出来,因此他们可以完全专注于应用的业务逻辑。事实上,Reach可以适用于多个网络,所以如果我们改运行:

$ REACH_CONNECTOR_MODE=ALGO reach run

那么Reach就会转而启动一个私有的Algorand devnet镜像,并使用Algorand连接器)。开发者不需要对他们的程序做任何改变,因为Reach在部署过程中完全不知道共识网络)的情况。

1.7``网页应用

您可以在YouTube上观看一段7分钟的视频,该视频演示了本节代码的操作,并对其工作原理进行了简要解释。

前面的执行使用Node.js在命令行进行测试运行。然而,大多数Reach开发人员通过Web应用程序部署他们的DApps,正如我们在下面描述的那样。

Web部署使用完全相同的index.rsh文件,但连接到一个基于React的index.js文件。(它还使用一些简单的React 视图css来配合。) 让我们来看看React index.js的一些片段,并与之前使用Node.js编写的index.mjs进行对比:

...    // ...
  7    import * as backend from './build/index.main.mjs';
  8    import * as reach from '@reach-sh/stdlib/ETH';
...    // ...

在文件顶端,我们按照命名 backend 导入了使用Reach生成的后端,并按照命名 reach 导入了保准库。

...    // ...
 27    async componentDidMount() { // from mode: ConnectAccount
 28      const acc = await reach.getDefaultAccount();
...    // ...

我们挂入App组件生命周期事件componentDidMount来获取用户的账户信息。getDefaultAccount会自动与浏览器扩展进行交互,例如MetaMask,以获取用户当前选择的账户。Reach能够通过扩展的API提示用户,部署合约并向共识网络发送交易,而无需通过Reach编程手动进入。这就像在Node.js部署中,Reach程序员确实需要解码底层共识网络的交互API的细节一样。

...    // ...
 71    async deploy() { // from mode: Deploy
 72      const ctc = this.props.acc.deploy(backend);
 73      this.setState({mode: 'EnterInfo', ctc});
 74      const ctcInfoStr = JSON.stringify(await ctc.getInfo(), null, 2);
 75      this.setState({ctcInfoStr});
 76    }
...    // ...

我们的React组件有一个名为 deploy 的方法,它实际上是在网络上部署合约,使用与测试部署中相同的调用:在第72行,我们调用 acc.deploy 函数,在第74行,我们调用 ctc.getInfo函数;这与我们在Node.js程序中所做的完全一样。

...    // ...
 79    async runBackend() { // from mode: RunBackend
 80      const {ctc, requestStandard, info} = this.state;
 81      this.setState({mode: 'BackendRunning'});
 82      const request = reach.parseCurrency(requestStandard);
 83      await backend.Alice(ctc, {request, info});
 84      this.setState({mode: 'BackendRan'});
 85    }
...    // ...

同样,我们实现了一个runBackend方法,利用从React UI收集到的信息,以Alice的身份执行Reach程序。

...    // ...
112    async runBackend(ctcInfoStr) { // from mode: RunBackend
113      const ctcInfo = JSON.parse(ctcInfoStr);
114      const ctc = this.props.acc.attach(backend, ctcInfo);
115      this.setState({mode: 'ApproveRequest'});
116      const interact = {
117        want: (request) => this.setState({mode: 'DisplayInfo', requestStandard: reach.formatCurrency(request, 4)}),
118        got: (info) => this.setState({info}),
119      };
120      await backend.Bob(ctc, interact);
121    }
...    // ...

我们在Bob组件中实现了类似的方法,像Bob一样运行后端。

我们像在Node.js中一样,指定Alice和Bob各自的参与者交互界面)。在React程序中,我们能够利用Bob的interact函数作为回调,可以更新React状态,以便向React用户界面显示,或者从React用户界面上采集信息。

你可以将@reachsh/stdlib JavaScript库安装到你的React项目中,或者为了方便起见,不需要设置React项目,你可以简单的使用命令:

$ reach react

该命令在Docker容器中运行你的DApp与React开发服务器,该容器中预装了Reach和React JavaScript依赖项,因此它的启动速度比自己构建它们快得多。

1.8``下一步

在这个概述中,我们已经简要介绍了Reach应用程序的结构和基本概念。我们已经展示了如何构建一个简单的程序,编译它,连接一个接口,在命令行进行测试,并使用React Web应用程序进行部署。由于这只是对Reach能做的事情的简单概述,我们省略了很多内容。但即便如此,我们也应该清楚为什么Reach是最简单、最安全的去中心化应用开发的编程语言。

此外,这个程序有很多缺陷,不应该在实践中使用。例如,在Alice未能提供信息的情况下,它没有为Bob提供任何保护,也没有试图确保信息是他想要的。Reach可以让你抽象掉你的去中心化程序的低级细节,专注于这类更加关乎大局的问题。在指南的其他部分,我们讨论了这样的设计问题。比如说,

不过,除非你现在就准备好深挖,否则你接下来的步骤是。

谢谢你成为Reach的一员