怎样实现以太坊交易可靠提交

在真实环境下的以太坊Dapp开发,是一定涉及到链上链下逻辑的交互的。那么开发者可能会遇到这样一种场景,当用户使用metamask签名交易并提交后,Dapp的中心服务端需要拿到这个交易ID,并跟踪这个交易的执行,甚至会根据这笔交易去触发后端逻辑(当然使用event可以一定程度避开这个问题,但这种回避式的解决方案不在此讨论),但现实情况往往是令人痛苦的,因为很可能会因为种种原因,我们无法取到metamask的回调,导致开发者因此”丢失”掉这笔交易。

那么,如果我们要直面这个问题,要怎么样实现交易可靠提交呢?

提炼问题

首先,我再次描述下问题的发生场景: 在metamask环境下,我们需要拿到用户提交的交易来触发后续中心化逻辑,但是在现实情况下很可能拿不到这个回调交易ID.

看起来,问题的核心并不是交易可靠提交,而是可靠地拿到交易提交的回调。那么解决问题,就有两种思路:

1. 最显而易见,metamask能够提供可靠回调

这个解决方案最无痛,然而完全依赖于metamask团队的开发意愿。所幸的是metamask团队在接收开发者的反馈后,有意愿往这方面努力。但开发时间不确定,甚至于我认为,在浏览器环境下,可能无法完美解决。所以短期内,这个方向是无法在生产环境实施的。

2. metamask将交易签名和发送拆分开来

如果没做过以太坊Dapp开发,可能不清楚metamask提交交易其实是串行执行了两步操作: 一.先对交易裸数据签名,得到签名后的交易,二.将签名后的交易提交到以太坊。

实际上,在metamask完成第一步签名后,这个交易就已经是一笔以太坊合法交易了,任何一个以太坊节点都可以拿着这笔交易进行全网广播,要求矿工打包。试想,如果metamsk将这两个操作拆分开来,那么开发者就可以要求metamask先对裸交易签名,然后客户端将这笔交易发送给服务端,让服务端向以太坊节点提交交易,这样应用开发者就能够使用各种传统手段保证交易提交,并且能够实施后续各种中心化逻辑了。

看似很美好,然而metamask目前不提供这样的接口。虽然web3js已经有这样的接口,但metamask并没有对接。不过前景还是可以期望的,metamask团队表示已经会进行操作拆分,将来可以这样做。详细可以参考Issue#3475.

3. 弄脏手自己做

既然靠不了别人,就自己来解决。这第3种解决方案,其实和第二种思路是一样的,只是达到这个目的有些纠结。

首先,metamsk支持web3js一个比较原始的签名方法web3.eth.sign,他是对一段数据进行以太坊签名,看起来可以满足我们的需求,不过为了使用这个方法我们还需要做很多工作。

该方法输入是交易的hash,但web3并没有提供从裸交易数据计算hash的方法,所以我选择让前端提交裸交易数据到服务端,服务端计算出hash值返还给前端;

前端拿到这个交易hash后,就可以调用web3.eth.sign唤起metamask签名,然后将签名字段和裸交易数据再次发送给服务端,服务端负责验证签名并且将交易和签名拼装好后发送到以太坊。

关键实现

1.前端获取裸交易数据

前端直面用户,可以拿到裸交易全部数据

1
2
3
4
5
6
7
8
9
{
    from: "0x...",
    to: "0x....",
    value: "0x...",
    gas: 10000,
    gasPrice: 21000,
    data: "0x...",
    nonce: "0x..."
}

2.后端计算裸交易hash

后端拿到前端的裸交易json,可以很容易计算出交易hash,下面给出计算的golang代码

1
2
tx := types.NewTransaction(....)
hash := types.HomesteadSigner{}.Hash(tx).Hex()

然后将计算出的hash返回给前端

3.前端唤起metamask签名

1
2
3
// 第一个参数是返回的交易hash
// 第二个参数是用户地址
web3.eth.sign("0x...", "0x...", "").then(console.log);

这里将唤起metamask.

4.后端发送交易

前端将裸交易数据和第3步得到的签名发送给后端,后端验证签名并发送到以太坊,关键go代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sign := common.Hex2Bytes(txSign)
if len(sign) != 65 {
	return nil, errors.New("bad sign")
}
if sign[64] != 27 && sign[64] != 28 {
	return nil, errors.New("invalid Ethereum signature (V is not 27 or 28)")
}
sign[64] -= 27
signer := types.HomesteadSigner{}
signedTx, err := tx.WithSignature(signer, sign)
if err != nil {
	return nil, err
}
ethConn.SendTransaction(context.Background(), signedTx)

反思

看起来,上面的技术方案好像完美解决了问题,实则不然,这只是当前环境下的较优方案罢了,并且这个方案还是存在诸多问题:

1.安全性

这是最大的问题,因为调用web3.eth.sign进行数据签名时,metamask无法展示签名的数据,所以用户根本不了解他到底是在对什么授权签名。这是非常可怕的,这可能被骇客利用,让用户对一笔转出自己账户所有余额的交易进行签名,导致资金盗窃。

2.用户体验

还是因为签名的方法,metamask在签名时会展示一段红色警告,导致用户体验下降。

3.时效性

因为这个安全原因,metamask团队将来也许会放弃对这个方法的支持,不过我倒是觉得,保留对这个方法的支持,将签名数据做详细展示,让开发者自己做安全性的权衡。

最后聊一点感想吧,目前区块链上簇拥了太多投机者,真正致力于深耕技术的人不多,metamask团队算一个,区块链是个有意思的技术,因为它的技术背后,隐含了人类社会化的意识,它目前的基础组件都还不够完善,需要我们热爱技术的所有人,去投入,去发展。

Comments