代码错误如何导致我亏损2400个AR币的经历

作为一个程序员,在币圈都想成为科学家,今年才开始写脚本做链上交互。我取得了一些收获,很快就获得了一些空投收入。五月后,没什么有趣项目,我想看看AR,因为最近还比较火,我去年就买了2400+多个AR币,想着是否要把AR币转到链上钱包,以获取可能的AO空投收益。

其实五月初就开始看,一直有个问题解决不了,就是同样助记词无法获得和ARConnect等钱包一致的地址,我看了ARConnect的源码,从助记词转换到Jwk文件,而官方SDK只支持从Jwk文件获得地址和进行签名等操作,并不支持直接从助记词生成钱包或者jwk文件。

ARConnect对应的源码地址在

/**
 * 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。可能中间格式不一致。我研究了两天没什么结果就放一边了。

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。

这时候我停下来要看看到底是为什么会地址不一致,而且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这个库,这个库都是五年前的了,一般认为这么久没有改动应该很稳定。

但在反复调试过程中,发现就是这一步返回的值就是不稳定的。通过源码更精确的来看,是在

let seedBuffer = await bip39.mnemonicToSeed(mnemonic);
let seed = new Uint8Array(seedBuffer.buffer);

这里返回就是不一致的,因为python打印默认不会打印几千位的数组全部内容,一开始比较头部几百位看起来一致,所以定位问题还花了不少时间。实际上在我这里的情况是,八千多位的数组,前六千多位每次都是一致的,后面看起来是脏数据。那么也解释了为什么每次程序刚运行都是稳定的,因为内存片区应该都是0. 但这个库很奇怪,完全没有初始化或者destroy方法,也就是不觉得需要重新调用初始化。

当我再认真看时,就发现这个human-crypto-keys项目只有32个star,并且这个bug在去年就被提了issue

我是觉得我照着ARConnect代码直接复制,完全没有想到会再去一个个看它引用的库的情况。

总结这次经历:首先,我的测试不够充分,如果多做一轮打印余额的检查也能复现问题;二是即便代码没问题,也不该总是直接用大金额的token直接一次性操作,可以分成几次;三是所有log还是尽可能打印,如果输出了jwk文件内容就可以恢复了。

然而,我也想对ARweave项目提出批评,这个项目的官方SDK本身完全没考虑提供从助记词创建钱包的方法,并且,相关的web钱包实际上都用了上面同一套代码,我没有继续研究为什么在web钱包中没有出错,但看起来使用了human-crypto-keys库都是有潜在风险的。也让我怀疑,到底是多少真实开发者在ARweave链上做开发。

最后更新,因为发布文章,ArConnect团队联系了我,我们一起定位了问题,我也提了修改的PR。ArConnect团队按照发现bug报告的奖励给了我1200个AR,挽回了我一半的损失,在此表示感谢。

Subscribe to Evan JIANG
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.