How a Simple Code Mistake Cost Me 2400 AR Coins in Just a Few Seconds

The original article was written in Chinese and published at the following address. This article was translated using ChatGPT.

As a programmer, I aspire to make a mark in the cryptocurrency space. This year, I started writing scripts to interact with the blockchain, and the results have been quite rewarding. I quickly earned some airdrop rewards. However, after May, there weren't many exciting projects, so I turned my attention to Arweave (AR), which had been trending lately. Last year, I bought over 2400 AR coins and thought about transferring them to an on-chain wallet to see if I could get any AO-related airdrop rewards.

I began exploring this in early May, but I encountered a persistent problem: I couldn't generate the same address from the same mnemonic that matched with the one in wallets like ARConnect. I reviewed ARConnect's source code and discovered that it converts mnemonics into JWK files. The official SDK only supports deriving addresses and performing signatures from JWK files, not directly from mnemonics.

ARConnect's relevant source code can be found at:

/**
 * 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
  };
}

The code is fairly straightforward, but the addresses generated were consistently incorrect.

After some online research, I suspected that the issue might be that it called window.crypto.subtle instead of crypto.subtle in a non-browser environment, which might lead to format inconsistencies. After two days of research without results, I set it aside.

On May 30th, there was news about an upcoming AO airdrop, suggesting that AR coins should be transferred from the exchange to an on-chain wallet. I usually prefer to operate with multiple wallets for such tasks. Although the addresses generated didn't match those in ARConnect, I assumed that as long as the same mnemonic produced a consistent address, I could later consolidate these AR coins using code.

I used the official jssdk to write a script and tested transferring from Wallet A to Wallet B, and from Wallet B to Wallet C, using just 0.5 AR for the test. The program executed without issues, and the transactions were successful on the blockchain explorer. Confident, I proceeded to transfer 2400 AR from the CEX to the first wallet and began the transfer process. Arweave is slow when generating wallets or making transfers. When I transferred to the second wallet, I noticed something was wrong. I recalled from previous tests that the second wallet should start with "oo". I had printed all wallet information during debugging, including exporting JWK files to the log, but decided to comment out these log statements as they made the log too lengthy and hard to track progress. By the time I stopped, it was too late; a transaction had already transferred 2398 coins to the next address.

At that point, I needed to investigate why the addresses were inconsistent, even though the A wallet address was correct. I decided to test whether the address generation was stable (the mnemonic in the code is a newly generated test one):

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();

In a MacOS + WebStorm environment, this code generates three different addresses for the same mnemonic. Multiple runs revealed that the first result is always consistent, but subsequent results vary. Below are the results of two runs (uncertain why the second output remains consistent):

First run:

  • yFyxBemQEZWvQsDSxORR-BbHLTnNIIMWl1tzBqKhURU

  • 1Hac552aQktwuS_ovaEsV9ccMV4IjsZrbZFMK7RuFlw

  • dqn65rI3XRuZ0sObxw4JoPRjfBCJOEF6wLSJ2UqUEdg

Second run:

  • yFyxBemQEZWvQsDSxORR-BbHLTnNIIMWl1tzBqKhURU

  • 1Hac552aQktwuS_ovaEsV9ccMV4IjsZrbZFMK7RuFlw

  • FABeY-3K4cR-d1ghvHZpYJdYi0aaWJPGSBJWw4ZvM3c

I then investigated which step caused the problem. Initially, I didn't suspect the getKeyPairFromMnemonic method from the human-crypto-keys library, as it's an old library and seemed stable due to its age.

However, detailed debugging revealed that the values returned from this method were inconsistent. The issue lay in:

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

The output here was inconsistent. Python's default print doesn't display all thousands of digits, so initially comparing only the first few hundred digits looked consistent, leading to time-consuming troubleshooting. In reality, the array returned was 8,000+ digits long, and while the first 6,000+ digits were always the same, the rest appeared to be corrupted data. This explained why the program was stable initially; the memory region was likely zeroed out. However, the library had no initialization or destroy methods, indicating no need for re-initialization.

Upon closer inspection, I realized that the human-crypto-keys project had only 32 stars, and this bug had been reported in an issue last year. I didn't expect to encounter problems just by copying ARConnect's code and didn't thoroughly review the libraries it referenced.

In conclusion, my testing was inadequate. Even if the code seemed fine, I shouldn't have transferred large amounts of tokens in one go. Instead, I should have divided the transfers into multiple batches. Additionally, logging all outputs, including JWK files, could have helped me recover the lost tokens.

However, I must criticize the ARweave project for not providing a method to create wallets from mnemonics in their official SDK. The related web wallets also use the same codebase. I haven't further investigated why the web wallet didn't have this issue, but using the human-crypto-keys library poses potential risks. It also raises concerns about the actual number of developers working on the ARweave blockchain.

Update: After publishing my article, the ArConnect team reached out to me, and we worked together to pinpoint the issue. I also submitted a PR with a suggested fix. The ArConnect team rewarded me with 1,200 AR for reporting the bug, which helped recover half of my losses. I want to express my gratitude for this.

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.