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

Evan JIANG
2024-08-20 16:39
发布于 Mirror

作为一个程序员,在币圈都想成为科学家,今年才开始写脚本做链上交互。我取得了一些收获,很快就获得了一些空投收入。五月后,没什么有趣项目,我想看看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,挽回了我一半的损失,在此表示感谢。

0
粉丝
0
获赞
2
精选
数据来源区块链,不构成投资建议!
网站只展示作者的精选文章
2022 Tagge. With ❤️ from Lambda