5
\$\begingroup\$

The application leverages the bitcoinj library to generate a vanity bitcoin address. A vanity address is simply a bitcoin address that contains a personalized string. I would like some critical feedback, specifically code correctness, code smells, and my usage of futures

Sample Invocation:

java -jar vanitas-1.0-SNAPSHOT-jar-with-dependencies.jar 1Love

Output:

Searching for a bitcoin address that contains: 1Love
Found in 1 minutes
Address: 1LoveEbV9B5iRzKU63PKh1tXNk7vh865B7
Private Key: 45777959218638374115925337441855471702901360693577031567674250991838132852058

If you would like to build and execute the application, it's available at GitHub.

Vanitas.java

package com.gmail.lifeofreilly.vanitas;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.MoreExecutors;

import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.MainNetParams;

import org.apache.log4j.Logger;

import java.util.concurrent.Callable;
import java.util.concurrent.Executors;

import javax.annotation.ParametersAreNonnullByDefault;

import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;

class Vanitas {
    private static final Logger log = Logger.getLogger(Vanitas.class);
    private static final NetworkParameters NET_PARAMS = MainNetParams.get(); //use production bitcoin network

    /**
     * An application for generating a vanity bitcoin address.
     * As a best practice you should not reuse bitcoin addresses.
     * Address reuse harms your privacy, as well as the privacy of others.
     * For more information see: https://en.bitcoin.it/wiki/Address_reuse
     *
     * @param args required argument. The desired bitcoin address substring.
     */
    public static void main(String[] args) {
        final String targetPhrase;

        if (args.length == 1) {
            targetPhrase = args[0];

            try {
                generateAddress(targetPhrase);
                System.out.println("Searching for a bitcoin address that contains: " + targetPhrase);
                System.out.println("Status is available at: " + System.getProperty("user.dir") + "/logs/error.log");
            } catch (IllegalArgumentException ex) {
                System.out.println("Your target phrase '" + targetPhrase + "' contains illegal characters. " +
                        "Please see: https://en.bitcoin.it/wiki/Address#Address_validation");
                System.exit(-1);
            }

        } else {
            System.out.println("Invalid number of arguments. Usage: Vanitas [phrase]");
            System.exit(-1);
        }

    }

    /**
     * Establishes a thread pool based on the number of available processors, then executes an AddressGenerator for each.
     * The resulting bitcoin address and the associated private key will be written to the standard output stream.
     *
     * @param targetPhrase The desired bitcoin address substring.
     */
    private static void generateAddress(final String targetPhrase) {
        final int cores = Runtime.getRuntime().availableProcessors();
        log.info("Searching for a bitcoin address that contains: " + targetPhrase);
        log.info("Number of threads that will be used: " + cores);

        final ListeningExecutorService execService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(cores));
        final long timeStart = System.nanoTime();

        for (int i = 0; i < cores; i++) {
            Callable<ECKey> callable = new AddressGenerator(targetPhrase, NET_PARAMS);
            ListenableFuture<ECKey> future = execService.submit(callable);
            Futures.addCallback(future, new FutureCallback<ECKey>() {

                @Override
                public void onSuccess(ECKey key) {
                    if (key.toAddress(NET_PARAMS).toString().contains(targetPhrase)) {
                        System.out.println("Found in " + MINUTES.convert((System.nanoTime() - timeStart), NANOSECONDS) + " minutes");
                        System.out.println("Address: " + key.toAddress(NET_PARAMS));
                        System.out.println("Private Key: " + key.getPrivKey());

                    }
                    execService.shutdownNow();
                }

                @Override
                @ParametersAreNonnullByDefault
                public void onFailure(Throwable thrown) {
                    log.error(thrown.getMessage());
                }
            });

        }
    }

}

AddressGenerator.java

package com.gmail.lifeofreilly.vanitas;

import com.google.common.base.CharMatcher;
import org.apache.log4j.Logger;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;

import java.text.NumberFormat;
import java.util.Locale;
import java.util.concurrent.Callable;

class AddressGenerator implements Callable<ECKey> {
    private static final Logger log = Logger.getLogger(AddressGenerator.class);
    private static final int BTC_ADDRESS_MAX_LENGTH = 35;
    private final NetworkParameters netParams;
    private long attempts;
    private String targetPhrase;

    /**
     * Sole constructor for AddressGenerator
     *
     * @param targetPhrase the desired bitcoin address substring
     * @param netParams    the target bitcoin network e.g production or testnet
     */
    public AddressGenerator(final String targetPhrase, final NetworkParameters netParams) {
        this.netParams = netParams;

        if (isValidBTCAddressSubstring(targetPhrase)) {
            this.targetPhrase = targetPhrase;
        } else {
            throw new IllegalArgumentException("The requested phrase is not a valid bitcoin address substring.");
        }

    }

    /**
     * Attempts to compute a bitcoin address that contains the target phrase.
     *
     * @return An ECKey which represents an elliptic curve public key (bitcoin address) and private key
     * @throws Exception
     */
    @Override
    public ECKey call() throws Exception {
        ECKey key;

        do {
            key = new ECKey();
            attempts++;
            logAttempts();
        } while (!(key.toAddress(netParams).toString().contains(targetPhrase)) &&
                !Thread.currentThread().isInterrupted());

        log.debug("Exiting thread " + Thread.currentThread().getName() +
                ", Attempts made: " + NumberFormat.getNumberInstance(Locale.US).format(attempts));
        return key;
    }

    /**
     * Logs progress every 1M attempts
     */
    private void logAttempts() {
        if (attempts % 1000000 == 0) {
            log.debug("Thread " + Thread.currentThread().getName() + " is still working, # of attempts: " +
                    NumberFormat.getNumberInstance(Locale.US).format(attempts));
        }
    }

    /**
     * Verifies that the requested phrase represents a valid bitcoin address substring
     *
     * @param substring the requested phrase
     * @return true if the requested phrase is a valid bitcoin address substring
     */
    private static boolean isValidBTCAddressSubstring(final String substring) {
        boolean validity = true;

        if (!CharMatcher.JAVA_LETTER_OR_DIGIT.matchesAllOf(substring) ||
                substring.length() > BTC_ADDRESS_MAX_LENGTH ||
                CharMatcher.anyOf("OIl0").matchesAnyOf(substring)) {
            validity = false;
        }

        return validity;
    }
}
\$\endgroup\$
5
\$\begingroup\$

Java 8 Streams have a number of features that make them ideal for parallel processing, and for reducing more complicated class structures down to simpler functions. You can use this to your advantage and it makes a big difference in this particular case.

Consider your problem, you want to generate many ECKey instances until one of them matches a particular phrase. You want to monitor your progress, and you want to stop when you find one. You also want to do this all in parallel, and as efficiently as practical.

There's really just one trick to know in this instance, when applying streams, and that is using an infinite stream of increasing values as an "infinite loop". This can be modeled as:

LongStream.iterate(0, i -> i + 1);

That will create a stream of long values that increases from 0 (and loops, and so on... it will create the stream 0, 1, 2, 3, 4, 5, 6, 7, .....

Now, you can 'peek' in to a stream, and see the current value, and do things with it, so, we can have a progress function:

private static final void progress(long count) {
    if (count % 1000000 == 0) {
        System.out.println("Handling generation " + count);
    }
}

Every time count is a multiple of a million, report it. You can incorporate this in to a stream as follows:

LongStream.iterate(0, i -> i + 1)
          .peek(count -> progress(count))

Now, here we have a stream of increasing long values, and we can report progress, so let's convert that stream of counts, to a stream of ECKey values...

LongStream.iterate(0, i -> i + 1)
          .peek(count -> progress(count))
          .mapToObj(count -> new ECKey())

That last line basically ignores the count, but swaps it for a key.

Now we filter out keys that don't match our criteria:

LongStream.iterate(0, i -> i + 1)
          .peek(count -> progress(count))
          .mapToObj(count -> new ECKey())
          .filter(key -> key.toAddress(NET_PARAMS).toString().contains(targetPhrase))

So, we have thrown out all keys other than ones that have our phrase in it... now all we need to do is to return the first one we find....

LongStream.iterate(0, i -> i + 1)
          .peek(count -> progress(count))
          .mapToObj(count -> new ECKey())
          .filter(key -> key.toAddress(NET_PARAMS).toString().contains(targetPhrase))
          .findAny()

That findAny returns an Optional, which may, or may not contain a key, but it will only ever get there if it did find a key, because the stream is infinite.... So, we can just use it as our successful find.

But, what about the multi-threaded requirement? Well, that's easy, just add parallel() as the first stream step. That will use all available CPU's and run part of the stream on each.

Wrap that all up in a nice function, and you get code like:

private static void generateAddress(final String targetPhrase) {

    validateBTCAddressSubstring(targetPhrase);

    final long timeStart = System.nanoTime();
    Optional<ECKey> found = LongStream.iterate(0L, c -> c + 1)
            .parallel()
            .peek(i -> progress(i))
            .mapToObj(ignore -> new ECKey())
            .filter(key -> key.toAddress(NET_PARAMS).toString().contains(targetPhrase))
            .findAny();

    long time = System.nanoTime() - timeStart;

    ECKey key = found.get();
    System.out.println("Found in " + MINUTES.convert(time, NANOSECONDS) + " minutes");
    System.out.println("Address: " + key.toAddress(NET_PARAMS));
    System.out.println("Private Key: " + key.getPrivKey());

}

private static final void progress(long count) {
    if (count % 1000000 == 0) {
        System.out.println("Handling generation " + count);
    }
}

private static void validateBTCAddressSubstring(final String substring) {

    // no lIO0
    if (!substring.matches("^[a-km-zA-HJ-NP-Z1-9]+$") ||
            substring.length() > BTC_ADDRESS_MAX_LENGTH) {
        throw new IllegalArgumentException("target phrase '" + substring + "' is not valid in an address.");
    }
}

No need for the Guava structures for concurrency, no need for execution services, no need for Callables, etc.

Using the tools you already have available is always a good option when writing maintainable code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.