Multi-threaded Arweave wallet mining with OpenSSL and Rust.

February 15, 2021

Recently, Tate had live streamed on the Verto discord server where he built a wallet miner. It worked well but it would take ages to get an ideal wallet address. So I got inspired and re-wrote it with Rust using threadpool and openssl.

Arweave uses RSA-4096 for private key generation. It took an average of ~3 secs using Node.js crypto module, so clearly we had to switch for something more faster.

Go’s crypto/rsa

I had heard a lot about the Golang crypto package so I decided to try it out. I ended up implementing a complete JWK utility but I was not satisfied with the performance overall.

package main

import (
  "crypto/rsa"
  "crypto/rand"
  "crypto"
  "math/big"
  "encoding/base64"
  "encoding/json"
  "strings"
  "fmt"
)

func safeEncode(p []byte) string {
	data := base64.URLEncoding.EncodeToString(p)
	return strings.TrimRight(data, "=")
}

type Key struct {
	Keys []*Key `json:"keys,omitempty"`

	Kty string `json:"kty"`
	Use string `json:"use,omitempty"`
	Kid string `json:"kid,omitempty"`
	Alg string `json:"alg,omitempty"`

	Crv string `json:"crv,omitempty"`
	X   string `json:"x,omitempty"`
	Y   string `json:"y,omitempty"`
	D   string `json:"d,omitempty"`
	N   string `json:"n,omitempty"`
	E   string `json:"e,omitempty"`
	K   string `json:"k,omitempty"`
}


// Create a JWK from a public key
func PublicKey(key crypto.PublicKey) (*Key, error) {
	switch key := key.(type) {
	case *rsa.PublicKey:
		jwt := &Key{
			Kty: "RSA",
			N:   safeEncode(key.N.Bytes()),
			E:   safeEncode(big.NewInt(int64(key.E)).Bytes()),
		}

		return jwt, nil

	case []byte:
		jwt := &Key{
			Kty: "oct",
			K:   safeEncode(key),
		}

		return jwt, nil

	default:
		return nil, fmt.Errorf("Unknown key type %T", key)
	}
}

func PrivateKey(key crypto.PrivateKey) (*Key, error) {
	switch key := key.(type) {
	case *rsa.PrivateKey:
		jwt, err := PublicKey(&key.PublicKey)
		if err != nil {
			return nil, err
		}

		jwt.D = safeEncode(key.D.Bytes())
		return jwt, err


	case []byte:
		return PublicKey(key)

	default:
		return nil, fmt.Errorf("Unknown key type %T", key)
	}
}

func main() {
  reader := rand.Reader
  key, _ := rsa.GenerateKey(reader, 4096)
  jwk, _ := PrivateKey(key)
  b, err := json.Marshal(jwk)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(b))    
}

It took an average of ~4 secs to generate one JWK. My next move was to try out OpenSSL with cgo but I knew I could do this better with Rust.

And yeah, Rust.

Rust doesn’t have a standard crypto library. In fact, there are tons of alternatives to choose from.

OpenSSL’s state-of-the-art RSA implementation made me wanna use the openssl-rust crate:

use openssl::bn::BigNum;
use openssl::rsa::Rsa;

#[derive(Debug)]
struct Key {
    kty: String,
    n: String,
    e: String,
    d: String,
}

// ...

let exponent = BigNum::from_u32(65537).unwrap();
let key = Rsa::generate_with_e(4096, &exponent).unwrap();
let n = key.n().to_vec();
let jwk = Key {
  kty: "RSA".to_string(),
  n: safe_encode!(n.clone()),
  e: safe_encode!(key.e().to_vec()),
  d: safe_encode!(key.d().to_vec()),
};

And tada! --release builds took an average of ~500ms which was way faster than Go or Node.

We are not done yet. Let’s make mining parellal using threadpools :D