以太坊源码解析(五)dApp开发人员

Posted by 姚飞亮 on 2020-07-11

以太坊源码解析(五)dApp开发人员

EVM追踪

以太坊有两种不同类型的交易:纯价值转移和合同执行。普通的价值转移只是将以太币从一个帐户转移到另一个帐户,因此从本指南的角度来看并不有趣。但是,如果交易的接收者是具有关联的EVM(以太坊虚拟机)字节码的合同账户-除了传输任何以太币外-该代码还将作为交易的一部分执行。

将代码与以太坊账户相关联可以使交易进行任意复杂的数据存储,并通过进一步与外部账户和合同进行内部交易,使交易对先前存储的数据起作用。这创建了一个相互交织的合同生态系统,其中单个交易可以与数十个或数百个帐户进行交互。

合同执行的缺点是很难说出交易实际上做了什么。交易收据确实包含用于检查执行是否成功的状态代码,但是无法查看修改了哪些数据,也没有看到调用了哪些外部合同。为了对事务进行自省,我们需要跟踪其执行。

跟踪先决条件

在最简单的形式中,跟踪交易需要以太坊节点请求以不同程度的数据收集重新执行所需的交易,并使其返回汇总摘要以进行后处理。但是,重新执行事务需要满足一些先决条件。

为了使以太坊节点重新执行事务,它需要使事务访问的所有历史状态都可用:

  • 收款人以及所有内部调用的合同的余额,现时,字节码和存储。
  • 阻止在外部事务和所有内部创建的事务的执行期间引用的元数据。
  • 由与被跟踪对象相同的块中包含的所有先前事务生成的中间状态。

根据您节点的同步和修剪模式,不同的配置会导致不同的功能:

  • 保留所有历史数据存档节点可以在任何时间点跟踪任意事务。跟踪单个事务还需要重新执行同一块中的所有先前事务。
  • 一个完全同步的节点保留所有历史数据初始同步后,只能从跟踪块的初始同步点以下交易。跟踪单个事务还需要重新执行同一块中的所有先前事务。
  • 快速同步节点仅保留周期性状态数据初始同步之后只能跟踪从块的初始同步点之后的交易。跟踪一个单个事务再次执行嗣继承所有先前的交易在相同的块,以及直到前一存储的快照所有前面的块。
  • 光同步节点检索数据按需在理论上跟踪交易针对所有所需历史状态是容易获得网络中的罐中。实际上,数据可用性不是可行的假设。

当运行整个块或链段的批量跟踪时,上述规则有例外。这些将在后面详细介绍。

基本痕迹

go-ethereum可以生成的最简单的事务跟踪类型是原始EVM操作码跟踪。对于事务执行的每条VM指令,都会发出一个结构化日志条目,其中包含所有认为有用的上下文元数据。这包括程序计数器操作码名称操作码成本剩余电量执行深度和任何 发生的错误。结构化日志还可以选择包含 执行堆栈执行内存合同存储的内容

单个操作码的示例日志条目如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"pc": 48,
"op": "DIV",
"gasCost": 5,
"gas": 64532,
"depth": 1,
"error": null,
"stack": [
"00000000000000000000000000000000000000000000000000000000ffffffff",
"0000000100000000000000000000000000000000000000000000000000000000",
"2df07fbaabbe40e3244445af30759352e348ec8bebd4dd75467a9f29ec55d98d"
],
"memory": [
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000060"
],
"storage": {
}
}

原始EVM操作码跟踪的整个输出是一个JSON对象,它具有一些元数据字段:消耗的气体故障状态返回值;以及 采用上述形式的操作码条目列表:

1
2
3
4
5
6
{
"gas": 25523,
"failed": false,
"returnValue": "",
"structLogs": []
}

生成基本跟踪

要生成原始的EVM操作码跟踪,请go-ethereum提供一些RPC API端点,其中最常用的是 debug_traceTransaction

以最简单的形式,traceTransaction接受交易哈希作为其唯一参数,跟踪交易,汇总所有生成的数据,并将其作为大型 JSON对象返回。从Geth控制台调用的示例为:

1
debug.traceTransaction("0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f")

当然,也可以通过HTTP RPC从节点外部调用相同的调用。在这种情况下,请确保HTTP端点通过启用--rpcdebugAPI命名空间通过暴露--rpcapi=debug

1
$ curl -H "Content-Type: application/json" -d '{"id": 1, "method": "debug_traceTransaction", "params": ["0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f"]}' localhost:8545

在Rinkeby网络(节点保留足够的历史记录)上运行上述操作将导致此跟踪转储

调整基本痕迹

默认情况下,原始操作码跟踪程序会发出在处理事务时EVM内发生的所有相关事件,例如EVM堆栈EVM内存更新的存储插槽。但是,某些用例可能不需要报告其中的某些数据字段。为了满足这些用例,可以使用跟踪器的第二个options参数来忽略这些庞大的字段:

1
2
3
4
5
{
"disableStack": true,
"disableMemory": true,
"disableStorage": true
}

在禁用数据字段的情况下,从Geth控制台运行上一个跟踪程序调用:

1
debug.traceTransaction("0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f", {disableStack: true, disableMemory: true, disableStorage: true})

同样也通过HTTP RPC从节点外部运行筛选的跟踪器:

1
$ curl -H "Content-Type: application/json" -d '{"id": 1, "method": "debug_traceTransaction", "params": ["0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f", {"disableStack": true, "disableMemory": true, "disableStorage": true}]}' localhost:8545

在Rinkeby网络上运行上述操作将导致此跟踪转储大大缩短。

基本痕迹的限制

尽管我们上面生成的原始操作码跟踪有其用途,但这种基本跟踪方式在现实世界中是有问题的。在大多数情况下,每个操作码只有一个单独的日志条目,对于大多数用例而言,级别太低,并且将要求开发人员创建其他工具来对跟踪进行后处理。此外,完整的操作码跟踪可以轻松进入数百兆字节,这使它们非常耗费资源,无法从节点进行外部处理。

为避免所有前面提到的问题,go-ethereum支持以太坊节点运行自定义JavaScript跟踪程序这些跟踪程序可以完全访问EVM堆栈,内存和协定存储。这允许开发人员仅收集他们需要的数据,以及做任何处理数据。请参阅下一部分以了解我们的自定义节点内跟踪器

修剪

默认情况下,Geth会在内存中进行状态修剪,丢弃它认为不再需要维护的状态条目。通过--gcmode选项进行配置。通常,人们会遇到状态不可用的错误。

假设您要对数据块进行跟踪B。现在有两种情况:

  1. 您已经完成了一个快速同步的数据透视块P,其中P <= B
  2. 您已经完成了一个快速同步的数据透视块P,其中P > B
  3. 您已经完成了完全同步,并进行了修剪
  4. 您已完成完全同步,没有修剪(--gcmode=archive

在每种情况下,都会发生以下情况:

  1. Geth将通过B在具有完整状态的时间之前的最近点重播块来重新生成所需的状态。默认情况下为128最大块数,但您可以在... "reexec":1000 .. }对跟踪器的实际调用中指定更多内容。
  2. 抱歉,如果不从创世开始重播就无法完成。
  3. 与1相同
  4. 不需要重播任何内容,可以立即加载状态并处理请求。

您还有另一种选择,可能会或可能不会满足您的需求。那就是使用Evmlab

1
docker pull holiman/evmlab && docker run -it holiman/evmlab

在那里您可以使用复制器。复制器将从infura中逐步获取数据,直到它具有在与映像捆绑在一起的evm上本地创建跟踪所需的所有信息为止。它将创建一个自定义的创世纪,其中包含交易涉及的状态(余额,代码,随机数等)。应该提到的是,由于外部交易引起的气体成本,evmlab复制器被严格保证是完全准确的,因为evmlab不能完全计算非零数据等的气体成本,但是通常足以分析合同和事件。

Go API

以太坊区块链及其两个扩展协议Whisper和Swarm最初被概念化为web3的支持支柱,为称为DApps的新一代分布式(实际上是去中心化)应用程序提供共识,消息传递和存储主干。

实现web3梦想的第一个体现是一个命令行客户端,它提供了对等协议中的RPC接口。该客户端很快就得到了类似于Web浏览器的图形用户界面的扩展,从而使开发人员可以基于久经考验的HTML / CSS / JS技术编写DApp。

由于许多DApp的要求比浏览器环境所能满足的要求更为复杂,因此很明显,提供对web3支柱的编程访问将打开通往新型应用程序的大门。因此,web3梦想的第二个体现是将我们的所有技术作为可重用的组件开放给其他项目。

从的1.5版本家族开始go-ethereum,我们从仅提供完整的以太坊客户端开始过渡,并开始交付可嵌入第三方桌面和服务器应用程序的官方Go软件包。

请注意,本指南将假定您熟悉Go开发。它不会尝试涵盖有关Go项目布局,导入路径或任何其他标准方法的常规主题。如果您不熟悉Go,请考虑先阅读其入门指南

快速概述

我们可重复使用的Go库专注于四个主要使用领域:

  • 简化的客户账户管理
  • 通过不同的传输方式进行远程节点接口
  • 通过自动生成的绑定进行合同交互
  • 处理中的以太坊,耳语和Swarm对等节点

您可以在2016年9月(上海)的以太坊Devcon2开发者大会上发表的彼得(@karalabe)演讲“导入Geth:来自以太坊之外的以太坊”中观看有关这些内容的快速概述。幻灯片可在此处获得

去包

go-ethereum库直接从我们的GitHub存储库中作为标准Go软件包的集合进行分发。这些软件包可以通过官方的Go工具包直接使用,而无需任何第三方工具。外部依赖项在本地提供 vendor,以确保自包含以及代码稳定性。如果您go-ethereum在自己的项目中重复使用 ,请遵循以下最佳做法并自行进行销售,以免API意外损坏!

为规范进口路径go-ethereumgithub.com/ethereum/go-ethereum,与位于下的所有包。虽然有相当多的人,你只需要关心一个有限的子集,其中的每一个将在其相关的部分进行适当引入。

您可以通过以下方式下载我们所有的软件包:

1
$ go get -d github.com/ethereum/go-ethereum/...

您可能还需要Go的原始上下文包。尽管已将其移至Go 1.7中的官方Go SDK中,但go-ethereumgolang.org/x/net/context 我们正式放弃对Go 1.5和Go 1.6的支持之前,将取决于原始软件包。

1
$ go get -u golang.org/x/net/context

转到帐户管理

为了为您的本地应用程序提供以太坊集成,您应该首先要做的是帐户管理。

尽管当前所有领先的以太坊实施都提供内置的帐户管理,但是建议不要将帐户保留在多个应用程序和/或多个人之间共享的任何位置。您不用以相同的方式将登录凭据委托给ISP(毕竟您是进入互联网的网关);您也不应该用凭据委托以太坊节点(您是进入以太坊网络的网关)。

处理本机应用程序中用户帐户的正确方法是执行客户端帐户管理,所有这些操作都独立存在于您自己的应用程序中。这样,您可以确保在认为必要时对敏感数据进行细粒度(或粗略)访问,而无需依赖任何第三方应用程序的功能和/或漏洞。

为此,go-ethereum提供了一个简单而全面的帐户包,可为您提供所有工具,以通过加密的密钥库和受密码保护的帐户来进行适当的安全帐户管理。您可以利用go-ethereum 加密实施的所有安全性,同时在自己的应用程序中运行所有内容。

加密的密钥库

尽管在应用程序本地处理帐户确实提供了一定的安全保证,但以太坊帐户的访问密钥绝不应以明文形式存在。这样,我们提供了一个加密的密钥库,可以为您提供适当的安全保证,而无需您对关联的加密原语的全面了解。

使用加密密钥库时要知道的重要一点是,其中使用的加密原语可以在标准轻量模式下运行。前者以增加的计算负担和资源消耗为代价提供了更高级别的安全性:

  • 标准需要256MB内存和现代CPU上1秒的处理才能访问密钥
  • light需要4MB内存和现代CPU上100毫秒的处理才能访问密钥

因此,标准更适合本机应用程序,但是,如果要针对更多资源受限的环境,则应该注意一些折衷。

对于那些对密码和/或实现细节感兴趣的人,密钥库使用secp256k1椭圆加密曲线,该曲线在高效加密标准中定义,由libsecp256k图书馆实施并由包裹 github.com/ethereum/go-ethereum/accounts。帐户以Web3秘密存储格式存储在磁盘上。

Go的密钥库

加密的密钥库由 包中的accounts.Manager 结构实现,该 结构 github.com/ethereum/go-ethereum/accounts还包含上述标准 安全性模式的配置常量。因此,要从Go中进行客户端帐户管理,您只需要将accounts包导入到代码中即可:

1
2
3
import "github.com/ethereum/go-ethereum/accounts"
import "github.com/ethereum/go-ethereum/accounts/keystore"
import "github.com/ethereum/go-ethereum/common"

之后,您可以通过以下方式创建新的加密帐户管理器:

1
2
ks := keystore.NewKeyStore("/path/to/keystore", keystore.StandardScryptN, keystore.StandardScryptP)
am := accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: false}, ks)

密钥库文件夹的路径必须是本地用户可写但其他系统用户不可读的位置(显然出于安全原因),因此我们建议将其放置在用户的主目录中,或者将其更锁定后端应用程序。

的最后两个参数 accounts.NewManager 是crypto参数,用于定义密钥库加密的资源强度。您可以选择accounts.StandardScryptN, accounts.StandardScryptPaccounts.LightScryptN, accounts.LightScryptP 或指定您自己的号码(请确保您了解这个底层加密)。我们建议使用标准版本。

帐户生命周期

为以太坊帐户创建加密的密钥库后,您可以使用此帐户管理器来满足本机应用程序的整个帐户生命周期要求。这包括创建新帐户和删除现有帐户的基本功能;以及更新访问凭证,导出现有帐户并将其导入另一台设备的更高级功能。

尽管密钥库定义了用于存储帐户的加密强度,但是没有可以授予所有帐户访问权限的全局主密码。而是每个帐户都是单独维护的,并以加密格式 分别存储在磁盘上,从而确保更干净,更严格地分离凭据。

但是,这种个性意味着,任何需要访问帐户的操作都需要以密码形式为该特定帐户提供必要的身份验证凭据:

  • 创建新帐户时,调用者必须提供密码短语来加密帐户。以后的任何访问都需要使用此密码,如果缺少该密码,则使用新创建的帐户将永远失去该密码。
  • 删除现有帐户时,呼叫者必须提供密码来验证帐户的所有权。这在密码上不是必需的,而是一种防止意外丢失帐户的保护措施。
  • 更新现有帐户时,呼叫者必须提供当前密码和新密码。完成操作后,将无法再通过旧密码访问该帐户。
  • 导出现有帐户时,在将密钥文件返回给用户之前,调用者必须提供当前密码来解密该帐户,并提供导出密码来对其进行重新加密。这是必需的,以允许在计算机和应用程序之间移动帐户而无需共享原始凭据。
  • 导入新帐户时,调用者必须提供要导入的密钥文件的加密密码短语以及用于存储帐户的新密码短语。这要求允许存储具有与用于移动帐户的凭据不同的凭据的帐户。

请注意,没有丢失密码短语的恢复机制。加密的密钥库的加密属性(如果使用提供的参数)可确保在任何有意义的时间内都不会强制使用帐户凭据。

Go帐户

以太坊账户由 包中的accounts.Account 结构 实现 github.com/ethereum/go-ethereum/accounts。假设我们已经有上一节中accounts.Manager 被调用的实例 am,我们可以通过少量的函数调用(省略错误处理)轻松地执行所有描述的生命周期操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create a new account with the specified encryption passphrase.
newAcc, _ := ks.NewAccount("Creation password")
fmt.Println(newAcc)

// Export the newly created account with a different passphrase. The returned
// data from this method invocation is a JSON encoded, encrypted key-file.
jsonAcc, _ := ks.Export(newAcc, "Creation password", "Export password")

// Update the passphrase on the account created above inside the local keystore.
_ = ks.Update(newAcc, "Creation password", "Update password")

// Delete the account updated above from the local keystore.
_ = ks.Delete(newAcc, "Update password")

// Import back the account we've exported (and then deleted) above with yet
// again a fresh passphrase.
impAcc, _ := ks.Import(jsonAcc, "Export password", "Import password")

尽管的实例 accounts.Account 可用于访问有关特定以太坊账户的各种信息,但它们不包含任何敏感数据(例如密码短语或私钥),而仅充当客户端代码和密钥库的标识符。

签名授权

如上所述,账户对象不持有相关的以太坊账户的敏感私钥,而只是用来标识加密密钥的占位符。所有需要授权(例如,交易签名)的操作都由客户经理授予其私钥访问权限后执行。

有几种方法可以授权客户经理执行签名操作,每种方法都有其优点和缺点。由于不同的方法具有完全不同的安全性保证,因此必须清楚每种方法的工作原理:

  • 单一授权:通过帐户管理器签署交易的最简单方法是在每次需要签名时提供帐户的密码,这将暂时解密私钥,执行签名操作并立即丢弃解密后的密钥。缺点是每次都需要向用户查询密码短语,如果经常这样做会很烦人。或应用程序需要将密码短语保留在内存中,如果操作不正确,可能会导致安全后果;并且根据密钥库的配置强度,不断解密密钥可能会导致不可忽略的资源需求。
  • 多种授权:通过帐户管理器签署交易的一种更复杂的方法是通过其密码短语一次解锁帐户,并允许帐户管理器缓存解密的私钥,从而使所有后续签名请求都无需密码即可完成。缓存的私钥的生存期可以手动(通过显式锁定帐户备份)或自动(通过在解锁期间提供超时)进行管理。对于用户可能需要签署许多交易或应用程序无需用户输入就签署的交易的情况,此机制非常有用。要记住的关键方面是,有权解锁帐户的任何人都可以在解锁特定帐户(例如运行不受信任代码的应用程序)的同时签署交易

请注意,创建事务不在此处讨论范围之内,因此本节的其余部分将假定我们已经具有要签名的事务哈希,并且将仅专注于创建授权它的密码签名。创建实际交易并将授权签名注入其中将在以后介绍。

从Go签名

假设我们已经有上一节中accounts.Manager 被调用的实例 am,我们可以创建一个新帐户来通过已经演示过的NewAccount 方法签署交易 ;并且为了避免目前进行交易创建,我们可以对随机代码进行硬编码 common.Hash来代替。

1
2
3
// Create a new account to sign transactions with
signer, _ := ks.NewAccount("Signer password")
txHash := common.HexToHash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")

有了样板,我们现在可以使用上述授权机制来签署交易:

1
2
3
4
5
6
7
8
9
10
11
// Sign a transaction with a single authorization
signature, _ := ks.SignHashWithPassphrase(signer, "Signer password", txHash.Bytes())

// Sign a transaction with multiple manually cancelled authorizations
_ = ks.Unlock(signer, "Signer password")
signature, _ = ks.SignHash(signer, txHash.Bytes())
_ = ks.Lock(signer.Address)

// Sign a transaction with multiple automatically cancelled authorizations
_ = ks.TimedUnlock(signer, "Signer password", time.Second)
signature, _ = ks.SignHash(signer, txHash.Bytes())

您可能想知道为什么 SignWithPassphrase 将一个accounts.Account 作为签名者,而Sign仅 将一个 作为签名者 common.Address。原因是 accounts.Account 对象还可能包含自定义密钥路径,从而允许 SignWithPassphrase 使用密钥库外部的帐户进行签名;但是,它 Sign依赖于已在密钥库中解锁的帐户,因此它无法指定自定义路径。

合同绑定

[请注意,事件尚未实现,因为它们需要一些正在审查的RPC订阅功能。]

以太坊平台的最初路线图和/或梦想是以多种语言提供可靠,高性能的共识协议客户端实现,这将为JavaScript DApp提供与之通信的RPC接口,并朝着Mist浏览器的方向发展。 ,用户可以通过它与区块链进行交互。

尽管这是主流采用的可靠计划,并且确实涵盖了人们提出的很多用例(大多数情况下是人们与区块链手动交互的地方),但它却避开了服务器端(后端,全自动,开发者)的用例,鉴于其动态特性,JavaScript通常不是首选的语言。

此页面介绍了服务器端本机Dapps的概念:可以从合约ABI和EVM字节码完全自动生成对任何以太坊合约的Go语言绑定,这些合约是编译时类型安全的,高性能的,而且最重要的是。

该页面以更加入门的教程风格编写,使人们更容易开始编写Go native Dapps。随着开发人员需要/遇到这些概念,它们将逐步引入。但是,我们确实假设读者总体上熟悉以太坊,对Solidity有一定的了解,并且可以编写Go。

代币合约

为避免陷入无用的学术实例的谬误,我们将以正式的Token合同作为引入Go本机绑定的基础。如果您不熟悉合同,只需浏览链接页面就足够了,详细信息暂时不相关。简而言之,合同实现了一个自定义令牌,该令牌可以部署在以太坊之上。为了确保本教程在链接的网站发生更改时不会过时,也可以在访问Token合同的Solidity源代码token.sol

去绑定生成器

已经可以通过以太坊客户端公开的RPC接口与Go上的以太坊区块链上的合同进行交互(或事实上的任何其他语言)。但是,编写将好的Go语言构造转换为RPC调用并返回的样板代码非常耗时,而且非常脆弱:实现错误只能在运行时才能检测到,并且几乎不可能发展合同,因为即使Solidity发生微小变化也可以移植到Go会很痛苦。

为了避免所有这些麻烦,go-ethereum实现引入了源代码生成器,该源代码生成器可以将以太坊ABI定义转换为易于使用的类型安全的Go包。假设您已经建立,godep安装了有效的Go开发环境并正确检出了go-ethereum存储库,则可以使用以下命令构建生成器:

1
2
$ cd $GOPATH/src/github.com/ethereum/go-ethereum
$ godep go install ./cmd/abigen

生成绑定

生成与以太坊合约的Go绑定所需的唯一基本条件是合约的ABI定义JSON文件。对于我们的Token合同教程,您可以自己编译Solidity代码(例如,通过@chriseth的在线 Solidity 编译器)获得,也可以下载我们的预编译token.abi

要生成绑定,只需调用:

1
$ abigen --abi token.abi --pkg main --type Token --out token.go

标记在哪里:

  • --abi:绑定到合同ABI的强制路径
  • --pgk:必须的Go软件包名称,用于将Go代码放入
  • --type:分配给绑定结构的可选Go类型名称
  • --out:生成的Go源文件的可选输出路径(未设置= stdout)

这将为令牌合约生成类型安全的Go绑定。生成的代码看起来像token.go,但是请生成您自己的代码,因为随着更多的工作投入到生成器中,这将改变。

访问以太坊合约

要与部署在区块链上的合约进行交互,您需要了解address 合约本身,并需要指定backend访问以太坊的方式。绑定生成器提供了开箱即用的RPC后端,您可以通过该后端通过IPC,HTTP或WebSockets连接到现有的以太坊节点。

我们将使用部署在测试网上的基金会的Unicorn令牌合同来演示调用合同的方法。它部署在该地址 0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576

要运行以下代码段,请确保Geth实例正在运行并已连接到已部署上述合同的Morden测试网络上。另外,请将您下面的IPC套接字的路径更新为您自己的本地Geth节点报告的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"log"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)

func main() {
// Create an IPC based RPC connection to a remote node
conn, err := ethclient.Dial("/home/karalabe/.ethereum/testnet/geth.ipc")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
// Instantiate the contract and display its name
token, err := NewToken(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), conn)
if err != nil {
log.Fatalf("Failed to instantiate a Token contract: %v", err)
}
name, err := token.Name(nil)
if err != nil {
log.Fatalf("Failed to retrieve token name: %v", err)
}
fmt.Println("Token name:", name)
}

和输出(是):

1
Token name: Testnet Unicorn

如果查看调用的读取令牌名称的方法token.Name(nil),则即使原始的Solidity合同不需要任何参数,也需要传递一个参数。这是一种*bind.CallOpts类型,可用于微调呼叫。

  • Pending:访问待定合同状态还是当前稳定状态
  • GasLimit:限制呼叫可能消耗的计算资源

用以太坊合约进行交易

由于需要授权实时交易并将其广播到网络中,因此涉及一种更改合同状态(即交易)的方法,这涉及更多。与在我们所连接的节点中存储帐户和密钥的常规方式相反,Go绑定要求在本地签名交易,并且不将其委托给远程节点。这样做是为了促进以太坊社区的总体发展方向,在该社区中,帐户对DApp保持私有状态,并且在它们之间不共享(默认情况下)。

因此,为了允许与合同进行交易,您的代码需要实现一种方法,该方法给定输入事务,对其进行签名并返回授权的输出事务。由于大多数用户的密钥都是Web3秘密存储格式的,因此该bind软件包包含一个小型实用程序方法(bind.NewTransactor(keyjson, passphrase)),可以从密钥文件和关联的密码创建授权的事务处理程序,而无需用户自己进行密钥签名。

更改之前的代码段以将一个独角兽发送到零地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"log"
"math/big"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)

const key = `paste the contents of your *testnet* key json here`

func main() {
// Create an IPC based RPC connection to a remote node and instantiate a contract binding
conn, err := ethclient.Dial("/home/karalabe/.ethereum/testnet/geth.ipc")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
token, err := NewToken(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), conn)
if err != nil {
log.Fatalf("Failed to instantiate a Token contract: %v", err)
}
// Create an authorized transactor and spend 1 unicorn
auth, err := bind.NewTransactor(strings.NewReader(key), "my awesome super secret password")
if err != nil {
log.Fatalf("Failed to create authorized transactor: %v", err)
}
tx, err := token.Transfer(auth, common.HexToAddress("0x0000000000000000000000000000000000000000"), big.NewInt(1))
if err != nil {
log.Fatalf("Failed to request token transfer: %v", err)
}
fmt.Printf("Transfer pending: 0x%x\n", tx.Hash())
}

和输出(是):

1
Transfer pending: 0x4f4aaeb29ed48e88dd653a81f0b05d4df64a86c99d4e83b5bfeb0f0006b0e55b

请注意,您很有可能没有可供使用的测试网独角兽,因此上述程序将因错误而失败。将至少2.014 testnet(!)以太网发送0xDf7D0030bfed998Db43288C190b63470c2d18F50到基础testnet tipjar 以接收独角兽令牌,您将能够看到上面的代码运行没有错误!

类似于上一节中仅读取合同状态的方法调用,事务处理方法还需要一个强制性的第一个参数(一种*bind.TransactOpts类型),该参数授权交易并可能对其进行微调:

  • From:使用(强制)调用方法的帐户地址
  • Signer:在广播之前在本地签署交易的方法(强制性)
  • Nonce:用于交易订购的帐户随机数(可选)
  • GasLimit:限制呼叫可能消耗的计算资源(可选)
  • GasPrice:明确设置汽油价格以运行交易(可选)
  • Value:与方法调用一起转移的所有资金(可选)

bind如果使用构造auth选项,则软件包将自动设置两个必填字段bind.NewTransactor。如果未设置,则nonce和gas相关字段通过绑定自动导出。假定未设置值为零。

预先配置的合同会话

如前两节所述,读取和状态修改合同调用都需要强制性的第一个参数,该参数既可以授权也可以微调一些内部参数。但是,大多数情况下,我们希望使用相同的参数并使用相同的帐户进行交易,因此,始终构造调用/交易选项或将它们与绑定一起传递会变得很笨拙。

为了避免出现这些情况,生成器还创建了专用包装器,可以使用调整和授权参数对其进行预配置,从而允许调用所有Solidity定义的方法而无需额外的参数。

它们的名称类似于原始合同类型名称,后缀为Sessions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Wrap the Token contract instance into a session
session := &TokenSession{
Contract: token,
CallOpts: bind.CallOpts{
Pending: true,
},
TransactOpts: bind.TransactOpts{
From: auth.From,
Signer: auth.Signer,
GasLimit: big.NewInt(3141592),
},
}
// Call the previous methods without the option parameters
session.Name()
session.Transfer("0x0000000000000000000000000000000000000000"), big.NewInt(1))

将合约部署到以太坊

与现有合约进行交互很好,但是让我们提高一个档次并将全新的合约部署到以太坊区块链上!但是,这样做,我们用来生成绑定的合同ABI是不够的。我们也需要编译后的字节码以允许部署它。

要获取字节码,请返回您可以用来生成字节码的在线编译器,或者下载我们的token.bin。您还需要使用包含的字节码重新运行Go生成器,以创建部署代码:

1
$ abigen --abi token.abi --pkg main --type Token --out token.go --bin token.bin

这将生成类似于的内容token.go。如果您快速浏览此文件,则会发现DeployToken与以前的代码相比,它刚刚注入了一个额外的功能。除了Solidity指定的所有参数之外,它还需要常规的授权选项来部署合同,以太坊后端也需要部署合同。

将所有内容放在一起将导致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"log"
"math/big"
"strings"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
)

const key = `paste the contents of your *testnet* key json here`

func main() {
// Create an IPC based RPC connection to a remote node and an authorized transactor
conn, err := rpc.NewIPCClient("/home/karalabe/.ethereum/testnet/geth.ipc")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
auth, err := bind.NewTransactor(strings.NewReader(key), "my awesome super secret password")
if err != nil {
log.Fatalf("Failed to create authorized transactor: %v", err)
}
// Deploy a new awesome contract for the binding demo
address, tx, token, err := DeployToken(auth, conn), new(big.Int), "Contracts in Go!!!", 0, "Go!")
if err != nil {
log.Fatalf("Failed to deploy new token contract: %v", err)
}
fmt.Printf("Contract pending deploy: 0x%x\n", address)
fmt.Printf("Transaction waiting to be mined: 0x%x\n\n", tx.Hash())

// Don't even wait, check its presence in the local pending state
time.Sleep(250 * time.Millisecond) // Allow it to be processed by the local node :P

name, err := token.Name(&bind.CallOpts{Pending: true})
if err != nil {
log.Fatalf("Failed to retrieve pending name: %v", err)
}
fmt.Println("Pending name:", name)
}

并且代码按预期执行:它要求在以太坊区块链上创建一个全新的代币合约,我们可以等待其被开采,或者如上述代码那样在未决状态下开始在其上调用方法:)

1
2
3
4
Contract pending deploy: 0x46506d900559ad005feb4645dcbb2dbbf65e19cc
Transaction waiting to be mined: 0x6a81231874edd2461879b7280ddde1a857162a744e3658ca7ec276984802183b

Pending name: Contracts in Go!!!

直接绑定实体

如果您一直遵循该教程,那么您可能已经意识到,每次合同修改都需要重新编译,将生成的ABI和字节码(特别是如果您需要多个合同)分别保存到文件中,然后为它们执行绑定。在第N次迭代后,这可能会变得很麻烦,因此该abigen命令支持直接从Solidity源文件(--sol)进行绑定,后者首先将源代码(通过--solc,默认为solc)编译为其组成组件,然后使用该组件进行绑定。

绑定官方令牌合同token.sol 将需要运行:

1
$ abigen --sol token.sol --pkg main --out token.go

注:建筑从密实度(--sol)是具有单独设置的绑定组件(互斥--abi--bin--type),因为所有这些都是从密实度代码提取并生成结果直接产生。

直接从Solidity构建合同具有很好的副作用,即Solidity源文件中包含的所有合同均已构建并绑定,因此,如果您的文件包含许多合同源,则Go代码将提供每个合同源。样本Token可靠性文件的结果为token.go

项目整合(即go generate

abigen命令的制作方式使其可以与现有Go工具链完美地结合在一起:无需记住将以太坊合约绑定到Go项目中所需的确切命令,我们可以利用它go generate来记住所有细节。

将绑定生成命令放在包定义之前的Go源文件中:

1
//go:generate abigen --sol token.sol --pkg main --out token.go

之后,无论何时修改Solidity合同,无需记住并运行上述命令,我们都可以简单地调用go generate包(甚至通过调用整个源代码树go generate ./...),它将为我们正确生成新绑定。

区块链模拟器

能够从本机Go代码中部署和访问已经部署的以太坊合约是一项非常强大的功能,但是开发本机代码有一个方面,甚至连testnet都不适合:自动单元测试。通过使用以太坊内部构造,可以创建测试链并进行验证,但是使用这种低级机制进行高级合约测试是不可行的。

为了解决最后一个将导致难以运行(和测试)本地DApp的问题,我们还实现了一个模拟的区块链,可以将其设置为本地合同的后端,方法与实时RPC后端相同:backends.NewSimulatedBackend(genesisAccounts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"log"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
)

func main() {
// Generate a new random account and a funded simulator
key, _ := crypto.GenerateKey()
auth := bind.NewKeyedTransactor(key)

sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})

// Deploy a token contract on the simulated blockchain
_, _, token, err := DeployMyToken(auth, sim, new(big.Int), "Simulated blockchain tokens", 0, "SBT")
if err != nil {
log.Fatalf("Failed to deploy new token contract: %v", err)
}
// Print the current (non existent) and pending name of the contract
name, _ := token.Name(nil)
fmt.Println("Pre-mining name:", name)

name, _ = token.Name(&bind.CallOpts{Pending: true})
fmt.Println("Pre-mining pending name:", name)

// Commit all pending transactions in the simulator and print the names again
sim.Commit()

name, _ = token.Name(nil)
fmt.Println("Post-mining name:", name)

name, _ = token.Name(&bind.CallOpts{Pending: true})
fmt.Println("Post-mining pending name:", name)
}

和输出(是):

1
2
3
4
Pre-mining name: 
Pre-mining pending name: Simulated blockchain tokens
Post-mining name: Simulated blockchain tokens
Post-mining pending name: Simulated blockchain tokens

请注意,我们不必等待本地私有链矿工或testnet矿工集成当前待处理的交易。当我们决定开采下一个区块时,我们只是Commit()模拟器。

行动API

以太坊区块链及其两个扩展协议Whisper和Swarm最初被概念化为web3的支持支柱,为称为DApps的新一代分布式(实际上是去中心化)应用程序提供共识,消息传递和存储主干。

实现web3梦想的第一个体现是一个命令行客户端,它提供了对等协议中的RPC接口。该客户端很快就得到了类似于Web浏览器的图形用户界面的扩展,从而使开发人员可以基于久经考验的HTML / CSS / JS技术编写DApp。

由于许多DApp的要求比浏览器环境所能满足的要求更为复杂,因此很明显,提供对web3支柱的编程访问将打开通往新型应用程序的大门。因此,网络梦想的第二个体现是将我们的所有技术作为可重用的组件开放给其他项目。

从的1.5版本家族开始go-ethereum,我们从仅提供完整的以太坊客户端开始过渡,并开始交付可嵌入第三方桌面和服务器应用程序的官方Go软件包。从这里开始迁移代码到移动平台只花了很小的一步。

快速概述

与我们的可重用Go库类似,移动包装器还专注于四个主要使用领域:

  • 简化的客户账户管理
  • 通过不同的传输方式进行远程节点接口
  • 通过自动生成的绑定进行合同交互
  • 处理中的以太坊,耳语和Swarm对等节点

您可以在2016年9月(上海)的以太坊Devcon2开发者大会上发表的彼得(@karalabe)演讲“导入Geth:来自以太坊之外的以太坊”中观看有关这些内容的快速概述。幻灯片可在此处获得

图书馆捆绑

go-ethereum移动库分布无论是作为一个Android .aar归档(含有二进制文件arm-7arm64x86x64); 或作为在iOS的XCode框架(包含用于二进制文件arm-7arm64x86)。我们暂时不提供Windows Phone的库捆绑包。

Android档案

go-ethereum在Android项目中使用的最简单方法是通过Maven依赖项。我们通过Maven Central提供了所有稳定发行版(从v1.5.0开始)的捆绑软件,还通过Sonatype OSS存储库提供了最新的开发捆绑软件。

稳定的依赖关系(Maven Central)

要将Android依赖项添加到的稳定库版本中go-ethereum,您需要确保在Android项目中启用了Maven Central存储库,并且该go-ethereum代码已列为应用程序的必需依赖项。您可以通过编辑build.gradleAndroid应用程序文件夹中的脚本来完成这两项操作:

1
2
3
4
5
6
7
8
repositories {
mavenCentral()
}

dependencies {
// All your previous dependencies
compile 'org.ethereum:geth:1.5.2' // Change the version to the latest release
}

发展依赖性(Sonatype)

要将Android依赖项添加到当前版本的go-ethereum,您需要确保在您的Android项目中启用了Sonatype快照存储库,并且该go-ethereum代码已作为SNAPSHOT应用程序的必需依赖项列出。您可以通过编辑build.gradleAndroid应用程序文件夹中的脚本来完成这两项操作:

1
2
3
4
5
6
7
8
9
10
repositories {
maven {
url "https://oss.sonatype.org/content/groups/public"
}
}

dependencies {
// All your previous dependencies
compile 'org.ethereum:geth:1.5.3-SNAPSHOT' // Change the version to the latest release
}

自定义依赖

如果您不希望依赖于Maven Central或Sonatype;或想以在线依赖关系的形式访问不再可用的较旧的开发版本,则可以直接从我们的网站下载任何捆绑包,然后通过将其插入Android Studio中的项目中File -> New -> New module... -> Import .JAR/.AAR Package

您还需要配置gradle将移动库捆绑包链接到您的应用程序。可以通过dependenciesbuild.gradle脚本部分 添加新条目,将其指向刚添加的模块(geth默认命名)来完成。

1
2
3
4
dependencies {
// All your previous dependencies
compile project(':geth')
}

手动构建

最后,如果您想修改go-ethereum移动代码和/或自己在本地构建它而不是下载预先构建的捆绑包,则可以使用 make命令来完成。这将geth.aarbuild/bin 文件夹中创建一个名为的Android存档,您可以如上所述将其导入到Android Studio中。

1
2
3
4
$ make android
[...]
Done building.
Import "build/bin/geth.aar" to use the library.

iOS框架

go-ethereum在iOS项目中使用的最简单方法是通过 CocoaPods依赖项。我们提供了所有稳定版本(从v1.5.3开始)和最新开发版本的捆绑包。

自动依赖

要将iOS依赖项添加到的当前稳定版本或最新开发版本中go-ethereum,您需要确保将iOS XCode项目配置为使用CocoaPods。详细说明超出了本文档的范围,但是您可以在上游的“ 使用CocoaPods”页面中找到指南 。之后,您可以编辑Podfile要列出go-ethereum的依赖项:

1
2
3
4
target 'MyApp' do
# All your previous dependencies
pod 'Geth', '1.5.4' # Change the version to the latest release
end

另外,如果您要使用最新的开发版本,请用替换软件包的版本1.5.4~> 1.5.5-unstable以切换到预发行版本,并始终从特定发行版系列中获取最新的捆绑软件。

自定义依赖

如果您不想依靠CocoaPods;或想以在线依赖关系的形式访问不再可用的较旧的开发版本,可以直接从我们的网站下载任何捆绑包, 然后通过将其插入XCode中的项目中Project Settings -> Build Phases -> Link Binary With Libraries

不要忘记从压缩.tar.gz档案中提取框架。您可以使用GUI工具或通过命令行通过(使用下载的文件替换存档)来执行此操作:

1
tar -zxvf geth-ios-all-1.5.3-unstable-e05d35e6.tar.gz

手动构建

最后,如果您想修改go-ethereum移动代码和/或自己在本地构建它而不是下载预先构建的捆绑包,则可以使用 make命令来完成。这将Geth.frameworkbuild/bin文件夹中创建一个iOS XCode框架,您可以如上所述将其导入XCode。

1
2
3
4
$ make ios
[...]
Done building.
Import "build/bin/Geth.framework" to use the library.

手机账户管理

为了为您的移动应用程序提供以太坊集成,您首先应该对帐户管理感兴趣。

尽管当前所有领先的以太坊实施都提供内置的帐户管理,但是建议不要将帐户保留在多个应用程序和/或多个人之间共享的任何位置。您不用以相同的方式将登录凭据委托给ISP(毕竟您是进入互联网的网关);您也不应该用凭据委托以太坊节点(您是进入以太坊网络的网关)。

在移动应用程序中处理用户帐户的正确方法是执行客户端帐户管理,所有这些操作都独立存在于您自己的应用程序中。这样,您可以确保在认为必要时对敏感数据进行细粒度(或粗略)访问,而无需依赖任何第三方应用程序的功能和/或漏洞。

为此,go-ethereum提供了一个简单而全面的帐户库,该库为您提供了所有工具,以通过加密的密钥库和受密码保护的帐户来进行适当的安全帐户管理。您可以利用go-ethereum 加密实施的所有安全性,同时在自己的应用程序中运行所有内容。

加密的密钥库

尽管在自己的移动设备上本地处理用户的帐户确实提供了一定的安全保证,但是以太坊帐户的访问密钥绝不应以明文形式存在。这样,我们提供了一个加密的密钥库,可以为您提供适当的安全保证,而无需您对关联的加密原语的全面了解。

使用加密密钥库时要知道的重要一点是,其中使用的加密原语可以在标准轻量模式下运行。前者以增加的计算负担和资源消耗为代价提供了更高级别的安全性:

  • 标准需要256MB内存和现代CPU上1秒的处理才能访问密钥
  • light需要4MB内存和现代CPU上100毫秒的处理才能访问密钥

因此,更适合于移动应用,但是您仍应注意这些取舍。

对于那些对密码和/或实现细节感兴趣的人,密钥库使用secp256k1椭圆加密曲线,该曲线在高效加密标准中定义,由libsecp256k图书馆实施并由包裹 github.com/ethereum/go-ethereum/accounts。帐户以Web3秘密存储格式存储在磁盘上。

Android(Java)上的密钥库

Android上的加密密钥库由包中的KeyStore类 实现org.ethereum.geth。配置常量(用于上述标准 安全性模式)位于Geth抽象类中,与org.ethereum.geth包类似。因此,要在Android上进行客户端帐户管理,您需要将两个类导入Java代码:

1
2
import org.ethereum.geth.Geth;
import org.ethereum.geth.KeyStore;

之后,您可以通过以下方式创建新的加密密钥库:

1
KeyStore ks = new KeyStore("/path/to/keystore", Geth.LightScryptN, Geth.LightScryptP);

密钥库文件夹的路径必须是本地移动应用程序可写的位置,但对于其他已安装的应用程序则不可读(显然出于安全原因),因此我们建议将其放置在应用程序的数据目录中。如果您是KeyStore在扩展Android对象的类中创建from的,则很可能可以Context.getFilesDir()通过访问该方法this.getFilesDir(),因此可以将keystore路径设置为this.getFilesDir() + "/keystore"

KeyStore构造函数的最后两个参数是crypto参数,用于定义密钥库加密应消耗的资源。您可以选择 Geth.StandardScryptN, Geth.StandardScryptPGeth.LightScryptN, Geth.LightScryptP或指定您自己的号码(请确保您了解这个底层加密)。我们建议使用简易版。

iOS上的密钥库(Swift 3)

iOS上的加密密钥库由 框架中的GethKeyStore类实现Geth。配置常量(用于上述标准安全模式)位于与全局变量相同的名称空间中。因此,要在iOS上进行客户端帐户管理,您需要将框架导入到您的Swift代码中:

1
import Geth

之后,您可以通过以下方式创建新的加密帐户管理器:

1
let ks = GethNewKeyStore("/path/to/keystore", GethLightScryptN, GethLightScryptP);

密钥库文件夹的路径必须是本地移动应用程序可写的位置,但对于其他已安装的应用程序则不可读(显然出于安全原因),因此,我们建议将其放置在应用程序的文档目录中。您应该能够通过检索文档目录let datadir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0],因此可以将密钥库路径设置为datadir + "/keystore"

GethNewKeyStorefactory方法的最后两个参数是crypto参数,用于定义密钥库加密应占用的资源。您可以选择 GethStandardScryptN, GethStandardScryptPGethLightScryptN, GethLightScryptP或指定您自己的号码(请确保您了解这个底层加密)。我们建议使用简易版。

帐户生命周期

为您的以太坊帐户创建了加密的密钥库后,您可以将其用于移动应用程序的整个帐户生命周期要求。这包括创建新帐户和删除现有帐户的基本功能;以及更新访问凭证,导出现有帐户并将其导入另一台设备的更高级功能。

尽管密钥库定义了用于存储帐户的加密强度,但是没有可以授予所有帐户访问权限的全局主密码。而是每个帐户都是单独维护的,并以加密格式 分别存储在磁盘上,从而确保更干净,更严格地分离凭据。

但是,这种个性意味着,任何需要访问帐户的操作都需要以密码形式为该特定帐户提供必要的身份验证凭据:

  • 创建新帐户时,调用者必须提供密码短语来加密帐户。以后的任何访问都需要使用此密码,如果缺少该密码,则使用新创建的帐户将永远失去该密码。
  • 删除现有帐户时,呼叫者必须提供密码来验证帐户的所有权。这在密码上不是必需的,而是一种防止意外丢失帐户的保护措施。
  • 更新现有帐户时,呼叫者必须提供当前密码和新密码。完成操作后,将无法再通过旧密码访问该帐户。
  • 导出现有帐户时,在将密钥文件返回给用户之前,调用者必须提供当前密码来解密该帐户,并提供导出密码来对其进行重新加密。这是必需的,以允许在设备之间移动帐户而不共享原始凭据。
  • 导入新帐户时,调用者必须提供要导入的密钥文件的加密密码短语以及用于存储帐户的新密码短语。这要求允许存储具有与用于移动帐户的凭据不同的凭据的帐户。

请注意,没有丢失密码短语的恢复机制。加密的密钥库的加密属性(如果使用提供的参数)可确保在任何有意义的时间内都不会强制使用帐户凭据。

Android(Java)上的帐户

Android上的以太坊帐户由包中的Account类 实现org.ethereum.geth。假设我们已经有上一节中KeyStore被调用 的实例ks,我们可以通过少量的函数调用轻松地执行所有描述的生命周期操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create a new account with the specified encryption passphrase.
Account newAcc = ksm.newAccount("Creation password");

// Export the newly created account with a different passphrase. The returned
// data from this method invocation is a JSON encoded, encrypted key-file.
byte[] jsonAcc = ks.exportKey(newAcc, "Creation password", "Export password");

// Update the passphrase on the account created above inside the local keystore.
ks.updateAccount(newAcc, "Creation password", "Update password");

// Delete the account updated above from the local keystore.
ks.deleteAccount(newAcc, "Update password");

// Import back the account we've exported (and then deleted) above with yet
// again a fresh passphrase.
Account impAcc = ks.importKey(jsonAcc, "Export password", "Import password");

尽管的实例Account可用于访问有关特定以太坊账户的各种信息,但它们不包含任何敏感数据(例如密码短语或私钥),而仅充当客户端代码和密钥库的标识符。

iOS上的帐户(Swift 3)

iOS上的以太坊帐户由 框架中的GethAccount类实现Geth。假设我们已经有上一节中GethKeyStore被调用的实例ks,我们可以通过少量的函数调用轻松地执行所有描述的生命周期操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create a new account with the specified encryption passphrase.
let newAcc = try! ks?.newAccount("Creation password")

// Export the newly created account with a different passphrase. The returned
// data from this method invocation is a JSON encoded, encrypted key-file.
let jsonKey = try! ks?.exportKey(newAcc!, passphrase: "Creation password", newPassphrase: "Export password")

// Update the passphrase on the account created above inside the local keystore.
try! ks?.update(newAcc, passphrase: "Creation password", newPassphrase: "Update password")

// Delete the account updated above from the local keystore.
try! ks?.delete(newAcc, passphrase: "Update password")

// Import back the account we've exported (and then deleted) above with yet
// again a fresh passphrase.
let impAcc = try! ks?.importKey(jsonKey, passphrase: "Export password", newPassphrase: "Import password")

尽管的实例GethAccount可用于访问有关特定以太坊账户的各种信息,但它们不包含任何敏感数据(例如密码短语或私钥),而仅充当客户端代码和密钥库的标识符。

签名授权

如上所述,账户对象不持有相关的以太坊账户的敏感私钥,而只是用来标识加密密钥的占位符。所有需要授权(例如,交易签名)的操作都由客户经理授予其私钥访问权限后执行。

有几种方法可以授权客户经理执行签名操作,每种方法都有其优点和缺点。由于不同的方法具有完全不同的安全性保证,因此必须清楚每种方法的工作原理:

  • 单一授权:通过密钥库签署交易的最简单方法是,在每次需要签名的东西时提供帐户的密码,这将暂时解密私钥,执行签名操作并立即丢弃解密后的密钥。缺点是每次都需要向用户查询密码短语,如果经常这样做会很烦人。或应用程序需要将密码短语保留在内存中,如果操作不正确,可能会导致安全后果;并且根据密钥库的配置强度,不断解密密钥可能会导致不可忽略的资源需求。
  • 多种授权:通过密钥库进行交易签名的一种更复杂的方法是通过其密码短语一次解锁帐户,并允许帐户管理器缓存解密的私钥,从而使所有后续签名请求都无需密码即可完成。缓存的私钥的生存期可以手动(通过显式锁定帐户备份)或自动(通过在解锁期间提供超时)进行管理。对于用户可能需要签署许多交易或应用程序无需用户输入就签署的交易的情况,此机制非常有用。要记住的关键方面是,有权解锁帐户的任何人都可以在解锁特定帐户时签署交易(例如,设备无人看管;应用程序运行不受信任的代码)。

请注意,创建事务不在此处讨论范围之内,因此本节的其余部分将假定我们已经有一个事务要签名,并且将仅着眼于创建它的授权版本。创建实际有意义的交易将在后面介绍。

在Android(Java)上签名

假设我们已经有上一节中的KeyStore被叫实例ks,我们可以创建一个新帐户来通过已经演示过的newAccount方法签署交易。并且为了避免现在就进行交易创建,我们可以对一个随机交易进行硬编码以进行签名。

1
2
3
4
5
6
// Create a new account to sign transactions with
Account signer = ks.newAccount("Signer password");
Transaction tx = new Transaction(
1, new Address("0x0000000000000000000000000000000000000000"),
new BigInt(0), new BigInt(0), new BigInt(1), null); // Random empty transaction
BigInt chain = new BigInt(1); // Chain identifier of the main net

有了样板,我们现在可以使用上述授权机制来签署交易:

1
2
3
4
5
6
7
8
9
10
11
// Sign a transaction with a single authorization
Transaction signed = ks.signTxPassphrase(signer, "Signer password", tx, chain);

// Sign a transaction with multiple manually cancelled authorizations
ks.unlock(signer, "Signer password");
signed = ks.signTx(signer, tx, chain);
ks.lock(signer.getAddress());

// Sign a transaction with multiple automatically cancelled authorizations
ks.timedUnlock(signer, "Signer password", 1000000000);
signed = ks.signTx(signer, tx, chain);

在iOS上签名(Swift 3)

假设我们已经有上一节中的GethKeyStore被叫实例ks,我们可以创建一个新帐户来通过已经演示过的newAccount方法签署交易。并且为了避免现在就进行交易创建,我们可以对一个随机交易进行硬编码以进行签名。

1
2
3
4
5
6
7
// Create a new account to sign transactions with
var error: NSError?
let signer = try! ks?.newAccount("Signer password")

let to = GethNewAddressFromHex("0x0000000000000000000000000000000000000000", &error)
let tx = GethNewTransaction(1, to, GethNewBigInt(0), GethNewBigInt(0), GethNewBigInt(0), nil) // Random empty transaction
let chain = GethNewBigInt(1) // Chain identifier of the main net

注意,尽管Swift通常NSError将返回的值重写为throws,但是由于某种原因(可能是因为它是构造函数),似乎错过了这个特定实例。当在gomobile项目的上游实现适当的修复程序时,它将在更高版本的iOS绑定中修复。

现在,有了样板,我们就可以使用上述授权方法签署交易了:

1
2
3
4
5
6
7
8
9
10
11
// Sign a transaction with a single authorization
var signed = try! ks?.signTxPassphrase(signer, passphrase: "Signer password", tx: tx, chainID: chain)

// Sign a transaction with multiple manually cancelled authorizations
try! ks?.unlock(signer, passphrase: "Signer password")
signed = try! ks?.signTx(signer, tx: tx, chainID: chain)
try! ks?.lock(signer?.getAddress())

// Sign a transaction with multiple automatically cancelled authorizations
try! ks?.timedUnlock(signer, passphrase: "Signer password", timeout: 1000000000)
signed = try! ks?.signTx(signer, tx: tx, chainID: chain)


Ω