I trying to generate Ethereum addresses for the HD Wallet keys implemented with bitcoinj library, but I got confused:
DeterministicSeed seed = new DeterministicSeed("some seed code here", null, "", 1409478661L);
DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).build();
DeterministicKey addrKey = chain.getKeyByPath(HDUtils.parsePath("M/44H/60H/0H/0/0"), true);
System.out.println("address from pub=" + Keys.getAddress(Sign.publicKeyFromPrivate(addrKey.getPrivKey())));
this code prints a correct Ethereum address accordingly to https://iancoleman.io/bip39/. Everything is fine here.
But when I trying to avoid private key usage and generate non-hardened keys using public keys only I getting different results, i.e. the call returns another result:
System.out.println("address from pub=" + Keys.getAddress(addrKey.getPublicKeyAsHex()));
And it looks like the issue is in the "different public keys", i.e. result of the Sign.publicKeyFromPrivate(addrKey.getPrivKey()) and addrKey.getPublicKeyAsHex() are different.
I'm not experienced with cryptography, thus it may be a silly question... but I would appreciate any advice here.
Like Bitcoin, Ethereum uses secp256k1. Ethereum addresses are derived as follows:
For the examples used here, the key is generated with:
This corresponds to the following public key and Ethereum address:
as can also be verified with the website https://iancoleman.io/bip39/.
Step 1:
In the posted question, the expressions
Sign.publicKeyFromPrivate()andaddrKey.getPublicKeyAsHex()provide different results. Both functions return the public key in different types. WhileSign.publicKeyFromPrivate()uses aBigInteger,addrKey.getPublicKeyAsHex()provides a hex string. For a direct comparison,BigIntegercan be converted to a hex string withtoString(16). When the results of both expressions are displayed with:the following result is obtained:
The output of
Sign.publicKeyFromPrivate()has a length of 64 bytes and corresponds to the concatenated x and y coordinate as defined in step 1. Therefore, the address generated with this is a valid Ethereum address, as also described in the posted question.The output of
addrKey.getPublicKeyAsHex(), on the other hand, corresponds to the x coordinate prefixed with a 0x02 value. This is the compressed format of the public key. The leading byte has either the value 0x02 if the y value is even (as in this example), or the value 0x03. Since the compressed format does not contain the y coordinate, this cannot be used to directly infer the Ethereum address, or if it is done anyway, it will result in a wrong address (indirectly, of course, it would be possible since the y coordinate can be derived from a compressed public key).The uncompressed format of the public key can be obtained, e.g. with
addrKey.decompress():which gives this result:
The uncompressed format consists of a leading marker byte with the value 0x04 followed by the x and y coordinates. So if the leading marker byte is removed, just the data according to step 1 is obtained, which is needed for the derivation of the Ethereum address:
which results in:
Steps 2 and 3:
Steps 2 and 3 are performed by
Keys.getAddress(). This allows the Ethereum address to be obtained using the uncompressed public key as follows:which gives the Ethereum address:
Overloads of
Keys.getAddress():Keys.getAddress()provides various overloads for the data typesBigInteger, hex string andbyte[]. If the uncompressed key is given asbyte[], e.g. withaddrKey.getPubKeyPoint().getEncoded(false), thebyte[]can be used directly after removing the marker byte. Alternatively, thebyte[]can be converted to aBigIntegerwith the marker byte removed:which as expected returns the same Ethereum address:
One thing to note here is that
Keys.getAddress(byte[]) does not pad the passedbyte[], while the overloads forBigIntegeror hex strings implicitly pad. This can be relevant e.g. when converting aBigInteger(e.g. provided bySign.publicKeyFromPrivate(addrKey.getPrivKey())) to abyte[], since the result can also have less than 64 bytes (which would lead to different Keccak-256 hashes). IfKeys.getAddress(byte[])is used in this case, it must beexplicitlypadded with leading 0x00 values up to a length of 64 bytes.