Skip to content

Private and Public Keys

A PrivateKey is a secp256k1 secret with two pieces of metadata attached to it: the network it is meant for, and whether its Public Key is compressed. It turns out that the metadata only matters when we write the key out as WIF. Signing, and the Public Key itself, are the same on every network.

We create a random Private Key as follows:

// A new random Private Key. It is compressed, and tagged for mainnet.
PrivateKey privateKey = new PrivateKey();
// For a testnet key we wrap a new ECKey and say which network it belongs to.
PrivateKey testnetKey = new PrivateKey(new ECKey(), true, NetworkType.TEST);

In the preceding example:

  • the no-argument constructor draws from SecureRandom, and tags the key as mainnet,
  • the three-argument constructor takes an ECKey (the BitcoinJ key class the library builds on), a compression flag and a NetworkType, which is one of MAIN, TEST, REGTEST or SCALINGTEST.

Wallet Import Format is the usual way to store a Private Key as text, or move it between wallets. A WIF string encodes the network (mainnet keys start with K, L or 5, testnet keys with c or 9) as well as the compression flag.

PrivateKey privateKey = PrivateKey.fromWIF("cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs");
// toWIF() encodes for the network the key was read from (testnet, here).
String wif = privateKey.toWIF();
// toWif(NetworkType) encodes the same secret for another network.
String mainnetWif = privateKey.toWif(NetworkType.MAIN);

fromWIF() reads the network from the string, and toWIF() writes it back the same way. To move the same secret to another network, we pass the network to toWif(NetworkType).

Please note the case. toWIF() and toWif(...) are two different methods.

A string that is not valid WIF (wrong length, wrong prefix or a bad checksum) throws InvalidKeyException, which is a checked exception.

assertThrows(InvalidKeyException.class,
() -> PrivateKey.fromWIF("not-a-wif-key"));

There is no hex factory on PrivateKey. Instead we go through ECKey, which takes the secret either as a BigInteger or as 32 bytes.

// We start from a 32-byte secret in hex...
BigInteger secret = new BigInteger("ec3da55fea673d63e760a604ff50ae65e2edaa23e5ecddc5ca8ea437fa71baa6", 16);
PrivateKey privateKey = new PrivateKey(ECKey.fromPrivate(secret), true, NetworkType.TEST);
// ...and get back to hex through the underlying ECKey.
String hex = privateKey.getKey().getPrivateKeyAsHex();
PrivateKey privateKey = PrivateKey.fromWIF("cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs");
PublicKey publicKey = privateKey.getPublicKey();
String hex = publicKey.getPubKeyHex(); // 33-byte compressed encoding, in hex
byte[] bytes = publicKey.getPubKeyBytes(); // the same, as bytes
byte[] hash160 = publicKey.getPubKeyHash(); // RIPEMD160(SHA256(pubkey)), used by P2PKH
// Reading a Public Key that someone gave us.
PublicKey theirs = PublicKey.fromHex(hex);

getPubKeyHash() gives us the 20-byte hash that a P2PKH Address and Locking Script contain. PublicKey.fromHex() and PublicKey.fromBytes() accept both the 33-byte compressed and the 65-byte uncompressed encodings.

A key read from an uncompressed WIF (an old one, starting with 5 on mainnet) keeps that form. Its Public Key is the 65-byte encoding, so the Address we derive is the one that holds the old wallet’s coins, and toWIF() writes it back unchanged. isCompressed() tells us which form a key uses.

// An uncompressed mainnet WIF (these start with 5).
PrivateKey privateKey = PrivateKey.fromWIF("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ");
// The key remembers the form the WIF asked for, and its Public Key, Address and WIF all keep it.
assertFalse(privateKey.isCompressed());
assertEquals(65, privateKey.getPublicKey().getPubKeyBytes().length);
assertEquals("1GAehh7TsJAHuUAeKZcXf5CnwuGuGgyX2S",
Address.fromKey(NetworkAddressType.MAIN_PKH, privateKey.getPublicKey()).toString());
assertEquals("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ", privateKey.toWIF());

PrivateKey.sign(byte[]) signs a 32-byte hash and returns a DER-encoded ECDSA Signature. One rarely needs it directly, since TransactionSigner signs Transactions and signMessage() signs text.

Do note that Signatures are deterministic (RFC 6979). The same key and hash always give the same Signature.