在区块链世界中,轻客户端是一个应用程序,它运行自己的p2p节点,因此与整个网络相连,没有任何中心化的中介机构。至少从去中心化的思维角度来看,这是一个理想的属性。然而,从纯粹的技术角度来看,这提出了几个问题。
除了p2p网络的经典性能和可靠性问题,区块链系统固有的一个大问题是,这些区块链系统的数据结构通常非常大(GBs - TBs),所以在移动设备上或有时甚至在桌面上下载它根本不可行。由于这个问题,轻客户端的概念被开发出来。这样的轻客户端,在以太坊的情况下,只下载区块头文件,验证的内容要少很多。
有了这样的轻客户端,就有可能成为整个p2p网络的一部分,并通过部署合约、发送交易、查询余额等直接与区块链互动。
这个概念在过去一直为比特币工作,是成功的区块链平台在某些方面似乎需要的东西之一。然而,在以太坊领域,在这篇博文发表时,这只是一个实验性的功能。
当我肤浅地研究这个问题时,我没有发现许多以太坊项目使用轻型客户端。最突出的是status,他们写了自己的go-ethereum的包装器。
我还发现了walleth,这是一个新项目,也是一个基于Kotlin的轻客户端,使用go-ethereum的交叉编译的安卓包。
这个交叉编译包也是我在这篇博文中要使用的,因为它似乎是目前处理这个问题的建议和最标准的方式。
在这个例子中,我将使用 react-native,因为我熟悉 react,想看看包装本地模块是否真的像他们说的那样简单。
就像以太坊世界的惯例一样,几乎没有更新的文档,移动包装器的采用似乎非常有限--这意味着很少有例子。另外,由于整个区块链的事情,测试会很尴尬,所以可能会有点颠簸。
让我们开始吧!
设置
对于设置 react-native 应用程序,我通常使用create-react-native-app并立即使用eject 。这给了我们一个可以工作的、可调试的应用程序,可以使用npm run android 启动。在这个例子中,我使用了Nexus 5 API 23模拟器。
代码示例
第一步是设置 react-native 应用程序,使其能够与本地 Java 代码通信。我不会在这里讨论很多细节,因为这个过程在这里有很好的记录。
最初,我们在android/app/src/main/java/com/reactnativeethereumwallet 中创建以下TestNative.java 文件。
package com.reactnativeethereumwallet;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
public class TestNative extends ReactContextBaseJavaModule {
public TestNative(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "TestNative";
}
@ReactMethod
public void test(String message, Callback cb) {
cb.invoke("hello from java: " + message);
}
}
它只是在我们的TestNative 模块中注册了一个方法test ,这个方法接收一个消息,用一个字符串和给定的消息调用给定的回调,以验证我们能够从 react-native 与 Java 通信。
然后,我们需要把这个模块放在一个包里--在我们的例子中是TestPackage ,就在TestNative 的位置。
package com.reactnativeethereumwallet;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TestPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new TestNative(reactContext));
return modules;
}
}
这只是一些用于包装我们的模块的模板。然后在MainApplication.java 文件中的getPackages 方法中注册这个包。
在创建和注册我们的本地模块后,我们可以从 react-native 中调用它。首先,我们创建一个文件叫ETH.android.js
import React from 'react';
import { StyleSheet, Text, Alert, Button, View, NativeModules } from 'react-native';
export default class Bla extends React.Component {
render() {
return (
<View style={styles.container}>
<Button title="button" onPress={() => {
NativeModules.TestNative.test("Hello!", (str) => {
Alert.alert(str);
})
}}></Button>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
这个组件只是渲染了一个按钮,按下后会调用NativeModules.TestNative.test ,即上面定义的函数,其中有一个字符串和一个回调。回调只是提醒返回的字符串,所以我们看到在react-native和android模块之间传递数值是可行的。
这个ETH 组件需要包含在App.js 中才能被渲染。
到目前为止,一切都很好。
第二步是让go-ethereum包装器在我们的应用程序内运行一个轻型客户端。对于这一部分,我发现使用Android Studio或其他一些Android IDE更容易,能够调试本地代码。
这里有一点免责声明--我不是一个安卓开发者,事实上我从来没有创建过一个原生的安卓应用程序,所以对所有安卓特定的代码都要抱有很大的信心。)
首先,我们需要添加geth-wrapper的依赖关系,也就是build.gradle中的go-ethereum Android包装器。
compile 'org.ethereum:geth:1.6.7'
现在,在MainActivity.java ,Geth 节点被启动。我试着在本地模块中这样做,但应用程序一直崩溃(可能是因为它阻塞了UI线程,但我不确定)。无论如何,节点是在主活动中创建和启动的,然后,在这种情况下,通过一个名为NodeHolder 的全局状态对象共享。
package com.reactnativeethereumwallet;
import android.os.Bundle;
import android.util.Log;
import com.facebook.react.ReactActivity;
import org.ethereum.geth.Account;
import org.ethereum.geth.Geth;
import org.ethereum.geth.KeyStore;
import org.ethereum.geth.Node;
import org.ethereum.geth.NodeConfig;
public class MainActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "reactnativeethereumwallet";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
NodeConfig nc = new NodeConfig();
Node node = Geth.newNode(getFilesDir() + "/.eth1", nc);
node.start();
NodeHolder nh = NodeHolder.getInstance();
nh.setNode(node);
KeyStore ks = new KeyStore(getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP);
Account newAcc = ks.newAccount("reallyhardpassword");
android.util.Log.d("keyfile", newAcc.getAddress().getHex());
nh.setAcc(newAcc);
} catch (Exception e) {
Log.d("error: ", e.getMessage());
e.printStackTrace();
}
}
}
好吧,这看起来并不坏。首先,我们创建一个NodeConfig ,它是light-client的配置对象。我们可以配置我们要使用的网络、创世块和其他许多东西。在我们的例子中,我们只是采取默认的(主网络)。
然后,该节点被创建为一个文件路径。在这个路径中,节点将保存其所有的文件(区块链-状态等),/.eth1 这个名字是完全任意的,你可以使用任何你想要的。
然后,NodeHolder 单元被创建,节点被设置。之后,我们也会创建一个新的账户,它将被保存在/keystore 目录中。有一些关于账户的更多的文档可以参考。
这个账户也会通过NodeHolder 。
如果你在这个时候启动应用程序,节点应该启动,你应该在Android Monitor控制台看到类似下面的东西。
Starting peer-to-peer node instance=GethDroid/v1.6.7-stable/android-386/go1.8.3
Allocated cache and file handles database=/data/user/0/com.reactnativeethereumwallet/files/.eth1/GethDroid/lightchaindata cache=16 handles=16
Initialised chain configuration ...
Disk storage enabled for ethash caches dir=/data/user/0/com.reactnativeethereumwallet/files/.eth1/GethDroid/ethash count=3
Disk storage enabled for ethash DAGs dir=.ethash count=2
Added trusted CHT for mainnet
Loaded most recent local header number=4086142 hash=a8c492…cffaf2 td=577139722519725357888
Starting P2P networking
Light client mode is an experimental feature
RLPx listener up ...
而且,如果有一点运气和耐心,节点应该在一段时间后开始同步。
现在,为了将我们的react-native应用连接到Geth 节点,我们只需在我们的TestNative 模块中使用NodeHolder 中共享的引用。
try {
NodeHolder nh = NodeHolder.getInstance();
Node node = nh.getNode();
Context ctx = new Context();
if (node != null) {
NodeInfo info = node.getNodeInfo();
EthereumClient ethereumClient = node.getEthereumClient();
Account newAcc = nh.getAcc();
BigInt balanceAt = ethereumClient.getBalanceAt(ctx, new Address("0x22B84d5FFeA8b801C0422AFe752377A64Aa738c2"), -1);
cb.invoke(balanceAt.toString() + " ether found. Account address:" + newAcc.getAddress().getHex());
return;
}
cb.invoke("node was null");
} catch (Exception e) {
android.util.Log.d("error: ", e.getMessage());
e.printStackTrace();
}
首先,我们从全局状态对象中获取节点和账户。然后,我们为节点创建一个EthereumClient - 这个客户端然后被用来与节点互动,其方式类似于web3。
随着以太坊客户端的实例化,我们简单地查询一些地址的余额,并将其与我们创建的账户地址一起发回给react-native。 如果查询余额成功,这意味着我们正在从react-native与我们运行的light-client进行通信。这意味着我们也可以执行其他动作,如与合同互动或发送交易。
而且,它是有效的!
我所说的It works! 是,如果我运行应用程序,Geth 节点找到同行并开始同步(它并不总是管理),一旦同步完成(可能需要相当长的时间),那么,按下按钮后,我就会看到硬编码地址的正确余额,可以在etherscan 等网站上检查。
这就是了。你可以在这里找到这个简单例子的完整代码。不过,代码中还有一些日志语句和注释过的行以及硬编码的地址,所以不要指望一个完全清理过的、可以使用的模板。 ;)
如果你启动这个应用程序,它需要一些时间(有时是几分钟......),直到它连接到其他节点并开始同步。同步本身也需要相当长的时间,所以这不是你可以期望的开箱即用的东西。
问题
我认为交叉编译的go-ethereum light-client包装器目前最大的问题是采用。如果有更多的人/项目在使用它,就会有更多的文档和更多当前的例子和项目来获得灵感和帮助。
然而,就目前的情况来看,API是完全没有文档的(Go-Mobile的限制,这将在某个时候被修复),软件本身似乎有点不稳定。很少有最新的例子、教程或工作的开源代码库可以学习。
正如预期的那样,测试和调试是相当困难的。这在原生的iOS或Android项目中可能效果更好,但如果没有源代码和记录的API,你会遇到一些障碍。
关于稳定性,在创建这篇文章的时候,有一个问题使得与测试网络(ropsten)同步变得困难/不可能(https://github.com/ethereum/go-ethereum/issues/14851),当我最终让它与主网络工作时,轻型客户端需要~45分钟来同步到最新的区块,但这可能是可以调整的 - 我没有投入太多的时间来研究这种优化。
总结
虽然这是一次有趣的经历,但我不得不说,在让任何东西工作的过程中都有不少的挫折感。我认为封装器本身是个好主意,交叉编译再次展示了Go的优势,但它还需要更多的工作,而且在我看来最重要的是需要一些使用它的项目。
就像我所有关于区块链,特别是以太坊的文章一样,我不得不说它还没有出现。炒作和投机泡沫是存在的,但在我看来,对于创建稳定的、真实世界的系统来说,这项技术还没有达到它需要的程度。
然而,我对react-native部分的工作情况相当满意。封装API是整个练习中最简单的部分,这表明,当底层的light-client准备好进入黄金时间时,从react-native应用中使用它应该是很容易的。