# 代码错误如何导致我亏损2400个AR币的经历 **Published by:** [Evan JIANG](https://paragraph.com/@firstfan/) **Published on:** 2024-06-11 **URL:** https://paragraph.com/@firstfan/2400-ar ## Content 作为一个程序员,在币圈都想成为科学家,今年才开始写脚本做链上交互。我取得了一些收获,很快就获得了一些空投收入。五月后,没什么有趣项目,我想看看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,挽回了我一半的损失,在此表示感谢。 ## Publication Information - [Evan JIANG](https://paragraph.com/@firstfan/): Publication homepage - [All Posts](https://paragraph.com/@firstfan/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@firstfan): Subscribe to updates - [Twitter](https://twitter.com/evan_loser): Follow on Twitter