Skip to content

Instantly share code, notes, and snippets.

@djma
Last active September 25, 2023 06:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save djma/386c2dcf91fefc004b14e5044facd3a9 to your computer and use it in GitHub Desktop.
Save djma/386c2dcf91fefc004b14e5044facd3a9 to your computer and use it in GitHub Desktop.
Ethereum EIP-712 message signing and address recovery with web3j
import java.math.BigInteger;
import java.security.SignatureException;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;
/**
* This runnable demo summarizes my understanding of this gist after spending a
* lot of time getting it to work:
* https://gist.github.com/megamattron/94c05789e5ff410296e74dad3b528613
*
* The main points of confusion were:
* 1. Whether a message needs to be prefixed with the Ethereum prefix
* 2. Whether the prefix msg length is 32 or the number of bytes in the message
* to sign.
* 3. Whether the message that is signed is a hash of the message or the message
* itself.
*
* Post EIP-712, https://eips.ethereum.org/EIPS/eip-712, the
* answer is:
* 1. The message needs to be prefixed with the Ethereum prefix.
* 2. The prefix msg length is the number of bytes in the message to sign. Some
* online sources say "It's always 32 because the message is hashed before
* signing." may have been true before EIP-712, but it is not true anymore.
* 3. The message that is signed is the original message.
*
* Different clients may not always implement EIP-712, but they should be moving
* towards it. I tested it with myCrypto, MetaMask, and Etherscan.
*
* This was written on 2022-10-27, with web3j 5.0.0 and java 17.
*/
public class EthereumMessageSigningDemo {
public static void main(String[] args) throws Exception {
String msg = "foobar";
System.out.println("message: " + msg);
// WARNING: IF YOU IMPORT THIS PRIVATE KEY, DO NOT SEND ANYTHING TO IT. IT WILL
// BE STOLEN IMMEDIATELY.
String privateKeyHexString = "0x1234567890123456789012345678901234567890123456789012345678901234";
// Get the public key address from the private key.
ECKeyPair aPair = ECKeyPair.create(Numeric.hexStringToByteArray(privateKeyHexString));
BigInteger publicKeyInBT = aPair.getPublicKey();
String publicKeyHexString = publicKeyInBT.toString(16);
String publicAddress = Keys.getAddress(publicKeyHexString);
System.out.println("publicAddress: " + publicAddress + " == 0x2e988a386a799f506693793c6a5af6b54dfaabfb");
// Using Sign.signPrefixedMessage is EIP-712 compliant. It:
// 1) adds a \x19Ethereum Signed Message\n prefix,
// 2) adds the byte-length of the message, and
// 3) Does NOT hash the message.
Sign.SignatureData signatureData = Sign.signPrefixedMessage(msg.getBytes(), aPair);
byte[] signedMessageBytes = new byte[32 + 32 + 1]; // 32 bytes for r, 32 bytes for s, 1 byte for v
System.arraycopy(signatureData.getR(), 0, signedMessageBytes, 0, 32);
System.arraycopy(signatureData.getS(), 0, signedMessageBytes, 32, 32);
System.arraycopy(signatureData.getV(), 0, signedMessageBytes, 64, 1);
String signedMessageHex = Numeric.toHexString(signedMessageBytes);
// Check with your preferred client (myCrypto, MM, etherscan, etc) that this is the correct signature. It may
// not be the same if the client does not follow EIP-712.
String expectedSignedMessageHex = "0xbe99c4f74b22db3f9f157d7c88b16ff064fba843b2b84d94a5c2b2653e76362126f5897bd2a559186a0dc797926c6048d7fb506c9f54f38e05e375fee15927e51c";
System.out.println("Signature Hash: " + signedMessageHex + " == " + expectedSignedMessageHex);
// Finally, verify the signature with the reverse of the signing process.
String recoveredAddress = getAddressUsedToSignHashedMessage(expectedSignedMessageHex, msg);
System.out.println("Recovered address: " + recoveredAddress + " == " + publicAddress);
}
/**
* This method is the reverse of the signing process.
*
* @param signedMessageInHex
* The signature in hex format. It is 65 bytes long,
* 32 bytes for r, 32 bytes for s, and 1 byte for v.
* May or may not be pre-pended with "0x".
* @param originalMessage
* The original message that was signed. Not hashed.
* @return
* The address that was used to sign the message.
* @throws SignatureException
*/
public static String getAddressUsedToSignHashedMessage(String signedMessageInHex, String originalMessage)
throws SignatureException {
if (signedMessageInHex.startsWith("0x")) {
signedMessageInHex = signedMessageInHex.substring(2);
}
// No need to prepend these strings with 0x because
// Numeric.hexStringToByteArray() accepts both formats
String r = signedMessageInHex.substring(0, 64);
String s = signedMessageInHex.substring(64, 128);
String v = signedMessageInHex.substring(128, 130);
// Using Sign.signedPrefixedMessageToKey for EIP-712 compliant signatures.
String pubkey = Sign.signedPrefixedMessageToKey(originalMessage.getBytes(),
new Sign.SignatureData(
Numeric.hexStringToByteArray(v)[0],
Numeric.hexStringToByteArray(r),
Numeric.hexStringToByteArray(s)))
.toString(16);
return Keys.getAddress(pubkey);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment