代码错误如何导致我亏损2400个AR币的经历
作为一个程序员,在币圈都想成为科学家,今年才开始写脚本做链上交互。我取得了一些收获,很快就获得了一些空投收入。五月后,没什么有趣项目,我想看看AR,因为最近还比较火,我去年就买了2400+多个AR币,想着是否要把AR币转到链上钱包,以获取可能的AO空投收益。
其实五月初就开始看,一直有个问题解决不了,就是同样助记词无法获得和ARConnect等钱包一致的地址,我看了ARConnect的源码,从助记词转换到Jwk文件,而官方SDK只支持从Jwk文件获得地址和进行签名等操作,并不支持直接从助记词生成钱包或者jwk文件。
ARConnect对应的源码地址在
https://github.com/arconnectio/ArConnect/blob/production/src/wallets/generator.ts
/**
* Credits to arweave.app for the mnemonic wallet generation
*
* https://github.com/jfbeats/ArweaveWebWallet/blob/master/src/functions/Wallets.ts
* https://github.com/jfbeats/ArweaveWebWallet/blob/master/src/functions/Crypto.ts
*/
/**
* Generate a JWK from a mnemonic seedphrase
*
* @param mnemonic Mnemonic seedphrase to generate wallet from
* @returns Wallet JWK
*/
export async function jwkFromMnemonic(mnemonic: string) {
const { privateKey } = await getKeyPairFromMnemonic(
mnemonic,
{
id: "rsa",
modulusLength: 4096
},
{ privateKeyFormat: "pkcs8-der" }
);
const jwk = pkcs8ToJwk(privateKey as any);
return jwk;
}
/**
* Convert a PKCS8 private key to a JWK
*
* @param privateKey PKCS8 private key to convert
* @returns JWK
*/
async function pkcs8ToJwk(privateKey: Uint8Array): Promise<JWKInterface> {
const key = await window.crypto.subtle.importKey(
"pkcs8",
privateKey,
{ name: "RSA-PSS", hash: "SHA-256" },
true,
["sign"]
);
const jwk = await window.crypto.subtle.exportKey("jwk", key);
return {
kty: jwk.kty!,
e: jwk.e!,
n: jwk.n!,
d: jwk.d,
p: jwk.p,
q: jwk.q,
dp: jwk.dp,
dq: jwk.dq,
qi: jwk.qi
};
}
实际上代码还挺简单的,但一直生成的地址不对。通过网上搜索,我认为问题在于它调用了window.crypto.subtle,而非非浏览器环境中的crypto.subtle。可能中间格式不一致。我研究了两天没什么结果就放一边了。
https://stackoverflow.com/a/62990139/1304867
5月30日,有消息说AO要空投啦,应该把AR从交易所转到链上,我习惯于所有操作都多钱包,本来想分几百个钱包存一下的,所以就没想过用浏览器钱包一个个操作。当时想到,虽然生成的地址和ARConnect不一致,但反正一个助记词对应一个地址,那最多以后也只用代码来重新归集这些AR币。
我使用官方jssdk写代码首先测试了下从A钱包转到B钱包,从B钱包转到C钱包,只用了0.5AR做测试,程序调空没有问题,链上浏览器看到状态都成功,觉得可以实施了。这里提供A钱包地址,大家可以跟踪看到测试过程,也能看到后续转错的交易:82X057LFL7CYkzwmKOQTuPPcgOd_rTmULfLkJzGoaoY。就从CEX提取了2400AR到第一个钱包,开始转移。AR链不管生成钱包还是转一笔币都很慢。但执行到转移到第二个钱包后,我当时马上就注意到不对了,因为我上轮测试大概对这些地址有些印象,第二个钱包应该是个oo开头的地址。这里又是我一个特别大的错误,调试过程中,我本身打印了所有钱包信息,有输出jwk文件到log,但测试后觉得没问题,log实在太长,会影响查看进度,就把这些log语句注释掉了。但此时已经晚了,交易已将2398个币转入了下一个地址,在当时正好价值10万USDT。
https://viewblock.io/arweave/tx/WGzn7lmYMTw6Zw-oMoOOwkXbTDpRubYMlDYEdEIa4RA
这时候我停下来要看看到底是为什么会地址不一致,而且A钱包地址是正确的。
那么我就开始测试,是否生成地址是稳定的。(代码中的助记词是新生成测试用的)
import Arweave from "arweave";
import * as bip39 from 'bip39';
import {getKeyPairFromMnemonic, getKeyPairFromSeed} from "human-crypto-keys";
import {webcrypto} from "crypto"
const arweave = Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https'
});
const keys = [];
export async function jwkFromMnemonic(mnemonic) {
let seedBuffer = await bip39.mnemonicToSeed(mnemonic);
let seed = new Uint8Array(seedBuffer.buffer);
const { privateKey } = await getKeyPairFromSeed(
seed,
{
id: "rsa",
modulusLength: 4096
},
{ privateKeyFormat: "pkcs8-der" }
);
// console.log(privateKey);
let key = await webcrypto.subtle.importKey(
"pkcs8",
privateKey,
{ name: "RSA-PSS", hash: "SHA-256" },
true,
["sign"]
);
// console.log(key);
const jwk = await webcrypto.subtle.exportKey("jwk", key);
// console.log(jwk);
return {
'kty': jwk.kty,
'e': jwk.e,
'n': jwk.n,
'd': jwk.d,
'p': jwk.p,
'q': jwk.q,
'dp': jwk.dp,
'dq': jwk.dq,
'qi': jwk.qi
};
}
async function main() {
const mn = 'teach grab street first maze tip assault family unfold mistake mean weasel';
for (let i=0;i<3;i++) {
let key = await jwkFromMnemonic(mn);
let curr_addr = await arweave.wallets.jwkToAddress(key);
console.log(curr_addr);
}
}
await main();
在MacOS + WebStorm环境下,这代码对同一个助记词,生成三次地址。多次运行后,第一个结果总是一致的,而后面的结果会发生变化,以下我给出我这里两次运行的结果(这里不确定为什么第二个输出也一致,就是很不稳定)
第一次:yFyxBemQEZWvQsDSxORR-BbHLTnNIIMWl1tzBqKhURU 1Hac552aQktwuS_ovaEsV9ccMV4IjsZrbZFMK7RuFlw dqn65rI3XRuZ0sObxw4JoPRjfBCJOEF6wLSJ2UqUEdg
第二次:yFyxBemQEZWvQsDSxORR-BbHLTnNIIMWl1tzBqKhURU 1Hac552aQktwuS_ovaEsV9ccMV4IjsZrbZFMK7RuFlwFABeY-3K4cR-d1ghvHZpYJdYi0aaWJPGSBJWw4ZvM3c
然后我就开始看,到底是哪一步导致这个问题的,最开始我完全没有怀疑到getKeyPairFromMnemonic这个方法来自于getKeyPairFromMnemonic这个库,这个库都是五年前的了,一般认为这么久没有改动应该很稳定。
https://github.com/ipfs-shipyard/js-human-crypto-keys
但在反复调试过程中,发现就是这一步返回的值就是不稳定的。通过源码更精确的来看,是在
let seedBuffer = await bip39.mnemonicToSeed(mnemonic);
let seed = new Uint8Array(seedBuffer.buffer);
这里返回就是不一致的,因为python打印默认不会打印几千位的数组全部内容,一开始比较头部几百位看起来一致,所以定位问题还花了不少时间。实际上在我这里的情况是,八千多位的数组,前六千多位每次都是一致的,后面看起来是脏数据。那么也解释了为什么每次程序刚运行都是稳定的,因为内存片区应该都是0. 但这个库很奇怪,完全没有初始化或者destroy方法,也就是不觉得需要重新调用初始化。
当我再认真看时,就发现这个human-crypto-keys项目只有32个star,并且这个bug在去年就被提了issue
https://github.com/ipfs-shipyard/js-human-crypto-keys/issues/28
我是觉得我照着ARConnect代码直接复制,完全没有想到会再去一个个看它引用的库的情况。
总结这次经历:首先,我的测试不够充分,如果多做一轮打印余额的检查也能复现问题;二是即便代码没问题,也不该总是直接用大金额的token直接一次性操作,可以分成几次;三是所有log还是尽可能打印,如果输出了jwk文件内容就可以恢复了。
然而,我也想对ARweave项目提出批评,这个项目的官方SDK本身完全没考虑提供从助记词创建钱包的方法,并且,相关的web钱包实际上都用了上面同一套代码,我没有继续研究为什么在web钱包中没有出错,但看起来使用了human-crypto-keys库都是有潜在风险的。也让我怀疑,到底是多少真实开发者在ARweave链上做开发。
最后更新,因为发布文章,ArConnect团队联系了我,我们一起定位了问题,我也提了修改的PR。ArConnect团队按照发现bug报告的奖励给了我1200个AR,挽回了我一半的损失,在此表示感谢。