使用React Native的Ethereum Light-客户端

292 阅读9分钟

在区块链世界中,轻客户端是一个应用程序,它运行自己的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.javaGeth 节点被启动。我试着在本地模块中这样做,但应用程序一直崩溃(可能是因为它阻塞了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应用中使用它应该是很容易的。

资源