去中心化网络应用的世界是一个令人兴奋的地方,近年来已经爆炸性增长,IPFS和以太坊等技术为点对点的网络提供了可能性,创造出生活在传统的客户/服务器模式之外的应用,用户可以直接互动和控制自己的数据。
同时,它仍然不成熟,对于软件开发人员来说,它缺乏传统的基于HTTP的网络应用程序世界的许多能力和生态系统。在这个领域工作的开发者的工具和库要少得多。
在过去的一年里,我一直在努力改善这个问题(作为欧盟地平线的下一代互联网计划资助的项目的一部分),为IPFS和以太坊建立网络拦截库:MockIPFS和Mockthereum。这些库既是一个立即有用的自动化测试库,以支持现代集成测试和CI工作流程,也是为使用任一(或两个)技术的网络应用程序建立更通用的网络代理工具的基础。
如果这听起来很酷,而且你只是想直接进入并亲自尝试这些,你可以从GitHub.com/httptoolkit/mockipfs/和github.com/httptoolkit/mockthereum/开始。
另一方面,如果你想听听这在实践中能做什么,并了解一下它在引擎盖下是如何工作的,请继续阅读。
构建网络应用的新方法
去中心化的网络应用程序通常使用许多不同的技术,在堆栈的不同层次上,如:
IPFS:用于去中心化的静态内容托管和数据存储以太坊:用于去中心化的全局状态、对该状态的计算和金融交易Filecoin/Storj :用于付费的去中心化长期内容存储WebRTC:用于点对点的原始数据传输和视频/音频连接Service workers:一个允许完全离线的网络应用的JavaScript APIHandshake(HNS)/以太坊名称系统(ENS):将域名映射到网络应用上GunDB:用于网络的去中心化数据库,具有点对点同步功能HTTP:用于与现有的 “传统”网络互动,以及与允许访问许多这些协议的节点通信。通过结合这些技术,有可能创建一个从分布式网络提供服务的网络应用程序,而不是一个可能离线或被封锁的单一服务器,它可以存储数据,与他人通信,并普遍提供你期望从传统SaaS网络应用程序获得的所有功能。
现在,这种架构的一个例子看起来像:
将一个基于JS的单页网络应用程序发布到IPFS,使用服务工作者使其完全离线和本地运行使用HNS/ENS将域名映射到发布的内容哈希上允许用户通过WebRTC进行点对点的通信,直接发送消息或在上面使用GunDB来同步结构化数据存储将用户的持久性内容发布到IPFS(可能是加密的),他们可以在自己的IPFS节点中固定,或通过Filecoin/Storj付费镜像修改全局状态或通过以太坊支持付费交易。鉴于这样的设置,拥有兼容浏览器的用户(默认为Brave,或安装了IPFS伴侣和Metamask扩展的Chrome/Firefox等)可以加载网络应用,在他们的机器上使用它,并从其他人那里发送和接收数据,所有这些都没有一个中央服务器参与,所有数据都存储在本地,或在他们自己控制的服务中。
即使原来的出版商不存在了,他们所有的基础设施都关闭了,如果围绕这个模型设计得好,用户将能够永远使用这个应用程序。
这至少是理论上的。在实践中,有相当多的粗糙边缘,所以这是很复杂和具有挑战性的,但这是一个有趣的空间,有许多新技术不断出现和发展。即使在今天,上面的清单也远远没有完成,这些技术放在一起,暗示了网络上去中心化技术的一个有趣的未来。
不过,HTTP如何与此相联系是值得注意的。虽然这些协议中的每一个都是独立于HTTP的,但对于网络应用中的浏览器连接,它们中的许多都使用HTTP作为最后一英里的传输。例如,对于IPFS,你通常会在你的机器上运行一个IPFS节点,直接与IPFS网络进行通信,然后配置你的浏览器使用该节点进行所有的IPFS,然后所有的IPFS互动将通过从你的网络应用中向该节点发出HTTP请求而发生。同样,对于以太坊来说,在绝大多数情况下,网络上的以太坊互动涉及到对托管的以太坊API的HTTP请求(这与中心化服务不一样,因为任何工作节点都可以同样工作,但必须使用一些托管节点)。
进入MockIPFS和Mockthereum
如果你建立一个这样的网络应用,你很快就会发现,测试它是一个严重的挑战。几乎没有可用的工具或库,所以你被迫要么完全手动模拟出API、库或原始HTTP请求(非难事,而且很难做到准确),要么运行一个真正的IPFS/以太坊节点进行测试(慢、重、有限,而且有持久的状态,有用,但不是你想要的自动化测试用例)。
MockIPFS和Mockthereum采取了不同的方法:在HTTP层面上的无状态和完全可配置的模拟,对客户端库和托管节点之间使用的HTTP交互协议有内置的解释和模拟。
这意味着你可以:
在一行代码中模拟两种协议的最常见的互动结果。直接监控、记录或断言客户端和网络之间的所有以太坊/IPFS互动。模拟连接问题和超时等场景。在几毫秒内创建、重置和销毁模拟节点。在同一台机器上同时运行多个完全隔离的模拟节点,以最小的开销,轻松地并行运行测试。用MockIPFS测试一个使用IPFS的dweb应用
一个去中心化的网络应用有很多方式想与IPFS交互,但最常见的是你想从一个CID中读取一些IPFS数据,所以让我们用这个作为例子。
要在网络上做到这一点,你通常要写这样的代码:
import * as IPFS from "ipfs-http-client"; import itAll from 'it-all'; import { concat as uint8ArrayConcat, toString as uint8ToString } from 'uint8arrays'; const IPFS_CONTENT_PATH = '/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu'; async Function runMyApp(ipfsNodeConfig) { const ipfsClient = IPFS.create(ipfsNodeConfig); // ... // Somewhere in your code, read some content from IPFS: const content = await itAll(ipfs.cat(IPFS_CONTENT_PATH)); const contentText = uint8ToString(uint8ArrayConcat(content)); // ... } runMyApp({ /* Your IPFS node config */ });
这使用了ipfs-http-client,这是一个广泛使用的官方库,用于在网络上使用IPFS,向本地IPFS节点发出HTTP请求,以获取IPFS的内容ID(在这个例子中为Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu)。
使用MockIPFS来测试这段代码,并模拟出返回的结果,看起来是这样的:
// Import MockIPFS and create a fake node: import * as MockIPFS from 'mockipfs'; const mockNode = MockIPFS.getLocal(); describe("Your tests", () => { // Start & stop your mock node to reset state between tests beforeEach(() => mockNode.start()); afterEach(() => mockNode.stop()); it("can mock & query IPFS interactions", async () => { // Define a rule to mock out this content: const ipfsPath = "/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu"; const mockedContent = await mockNode.forCat(ipfsPath).thenReturn("Mock content"); // Run the code that you want to test, configuring the app to use your mock node: await runMyApp(mockNode.ipfsOptions); // <-- IPFS cat() here will read 'Mock content' // Afterwards, assert that we saw the requests we expected: const catRequests = await mockNode.getQueriedContent(); expect(catRequests).to.deep.equal([ { path: ipfsPath } ]); }); });
在这种情况下,MockIPFS处理请求,解析API调用以匹配所使用的特定CID,然后返回正确编程和格式的内容,就像一个真正的IPFS节点,完全集成测试你的应用程序的整个客户端代码,但没有真正的IPFS节点的开销、复杂性或不可预测性。
像这样模拟ipfs.cat是最简单的情况,但MockIPFS可以走得更远:
通过mockNode.forPinAdd(cid).... 等调用,测试内容引脚和取消引脚,例如对无效/重复的引脚抛出错误。用mockNode.forNameResolve(name).thenTimeout()为IPNS查询注入超时。用mockNode.forAdd().thenAcceptPublishAs(hash)模拟内容发布结果。要想开始,请看README以获得更多的细节和完整的API文档,或者看一下测试套件以获得涵盖IPFS API每个主要领域的完整工作实例。
使用Mockthereum测试以太坊的dweb应用
在以太坊上构建网络应用时,一个常见的互动是调用合约--即查询区块链上的数据,而不实际创建交易。
使用流行的以太坊网络客户端Web3.js,这样做的代码可能看起来像:
import Web3 from 'web3'; // Parameters for some real Web3 contract: const CONTRACT_ADDRESS = "0x..."; const JSON_CONTRACT_ABI = { /* ... */ }; async function runMyApp(ethNodeAddress) { const web3 = new Web3(ethNodeAddress); // ... // Somewhere in your code, call a method on the Ethereum contract: const contract = new web3.eth.Contract(JSON_CONTRACT_ABI, CONTRACT_ADDRESS); const contractResult = await contract.methods.getText("test").call(); // ... } runMyApp(/* Your Ethereum node API address */);
和上面的IPFS一样,我们可以很容易地定义一个模拟节点,它可以拦截这个请求,返回任何值或者模拟任何你想要的其他行为:
// Import Mockthere and create a fake node: import * as Mockthereum from 'mockthereum'; const mockNode = Mockthereum.getLocal(); describe("Your tests", () => { // Start & stop your mock node to reset state between tests beforeEach(() => mockNode.start()); afterEach(() => mockNode.stop()); it("can mock & query Ethereum interactions", async () => { // Define a rule to mock out the specific contract method that's called: const mockedFunction = await mockNode.forCall(CONTRACT_ADDRESS) // Match any contract address // Optionally, match specific functions and parameters: .forFunction('function getText(string key) returns (string)') .withparams(["test"]) // Mock contract results: .thenReturn('Mock result'); // Run the code that you want to test, configuring the app to use your mock node: await runMyApp(mockNode.url); // <-- Contract call here will read 'Mock result' // Afterwards, assert that we saw the contrat calls we expected: const mockedCalls = await mockedFunction.getRequests(); expect(mockedCalls.length).to.equal(1); expect(mockedCalls[0]).to.deep.include({ // Examine full interaction data, included decoded parameters etc: to: CONTRACT_ADDRESS, params: ["test"] }); }); });
要想开始了解许多其他可以模拟的以太坊行为,请看README,或者看一下测试套件中涵盖广泛的典型以太坊交互的完整工作实例。
测试之外
在上面的快速例子中,我们已经看到了MockIPFS和Mockthereum如何处理特定的普通交互的简单演示,通过配置客户端的模拟节点地址而不是真实节点,这样模拟节点就可以独立于更广泛的网络处理所有流量。
当这样使用时,所有不匹配的请求将收到默认响应,例如,所有IPFS添加请求将显示为成功(同时没有真正发布任何东西),所有以太坊钱包余额将为零。
不过这两个库都可以超越这一点。每个库都可以配置为将不匹配的请求转发到其他地方,这样部分或全部流量就会通过模拟节点传递到真正的IPFS/以太坊节点。这使得它有可能为调试而记录流量,或者只模拟交互的一个子集,而所有其他的请求都表现得很正常。
要配置这一点,在创建模拟节点时,在getLocal调用中传递一个unmatchedRequests选项,像这样:
const ipfsMockNode = MockIPFS.getLocal({ unmatchedRequests: { proxyTo: "http://localhost:5001" } }); const ethMockNode = Mockthereum.getLocal({ unmatchedRequests: { proxyTo: "http://localhost:30303" } });
通过这种配置,你可以在浏览器中把这些节点作为你的正常节点地址(通过在IPFS companion/Metamask/等中配置地址),用于高级代理用例。默认情况下,它们的行为就像它们代理的真实节点一样,但你可以额外添加接收到的交互的日志,以便在你浏览网页时监控客户端以太坊/IPFS的交互,或者你可以通过添加规则来匹配这些请求,模拟出甚至禁用某些类型的交互。
为自己入门
在保持本文简短的同时,很难将这些工具的所有功能都挤进去。但如果这已经激起了你的兴趣,可以去GitHub上看看这些库本身,看看深入的入门指南和解释,以及涵盖其全部功能的详细API文档:MockIPFS, Mockthereum.
还没有评论,来说两句吧...