Signing Messages¶
Signing messages can be used for various method of authentication and off-chain operations, which can be put on-chain if necessary.
Signing a String Message¶
By allowing a user to sign a string, which can be verified on-chain, interesting forms of authentication on-chain can be achieved. This is a quick example of how an arbitrary string can be signed by a private key, and verified on-chain. The Contract can be called by another Contract, for example, before unlocking functionality by the caller.
contract Verifier {
// Returns the address that signed a given string message
function verifyString(string message, uint8 v, bytes32 r,
bytes32 s) public pure returns (address signer) {
// The message header; we will fill in the length next
string memory header = "\x19Ethereum Signed Message:\n000000";
uint256 lengthOffset;
uint256 length;
assembly {
// The first word of a string is its length
length := mload(message)
// The beginning of the base-10 message length in the prefix
lengthOffset := add(header, 57)
}
// Maximum length we support
require(length <= 999999);
// The length of the message's length in base-10
uint256 lengthLength = 0;
// The divisor to get the next left-most message length digit
uint256 divisor = 100000;
// Move one digit of the message length to the right at a time
while (divisor != 0) {
// The place value at the divisor
uint256 digit = length / divisor;
if (digit == 0) {
// Skip leading zeros
if (lengthLength == 0) {
divisor /= 10;
continue;
}
}
// Found a non-zero digit or non-leading zero digit
lengthLength++;
// Remove this digit from the message length's current value
length -= digit * divisor;
// Shift our base-10 divisor over
divisor /= 10;
// Convert the digit to its ASCII representation (man ascii)
digit += 0x30;
// Move to the next character and write the digit
lengthOffset++;
assembly {
mstore8(lengthOffset, digit)
}
}
// The null string requires exactly 1 zero (unskip 1 leading 0)
if (lengthLength == 0) {
lengthLength = 1 + 0x19 + 1;
} else {
lengthLength += 1 + 0x19;
}
// Truncate the tailing zeros from the header
assembly {
mstore(header, lengthLength)
}
// Perform the elliptic curve recover operation
bytes32 check = keccak256(header, message);
return ecrecover(check, v, r, s);
}
}
let abi = [
"function verifyString(string, uint8, bytes32, bytes32) public pure returns (address)"
];
let provider = ethers.getDefaultProvider('ropsten');
// Create a wallet to sign the message with
let privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123';
let wallet = new ethers.Wallet(privateKey);
console.log(wallet.address);
// "0x14791697260E4c9A71f18484C9f997B308e59325"
let contractAddress = '0x80F85dA065115F576F1fbe5E14285dA51ea39260';
let contract = new Contract(contractAddress, abi, provider);
let message = "Hello World";
// Sign the string message
let flatSig = await wallet.signMessage(message);
// For Solidity, we need the expanded-format of a signature
let sig = ethers.utils.splitSignature(flatSig);
// Call the verifyString function
let recovered = await contract.verifyString(message, sig.v, sig.r, sig.s);
console.log(recovered);
// "0x14791697260E4c9A71f18484C9f997B308e59325"
Signing a Digest Hash¶
Signing a digest can be far more space efficient than signing an arbitrary string (as you probably notice when comparing the length of the Solidity source code), however, with this method, many Wallet UI would not be able to fully inform the user what they are about to sign, so this method should only be used in quite specific cases, such as in custom Wallet applications.
contract Verifier {
function verifyHash(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure
returns (address signer) {
bytes32 messageDigest = keccak256("\x19Ethereum Signed Message:\n32", hash);
return ecrecover(messageDigest, v, r, s);
}
}
let abi = [
"function verifyHash(bytes32, uint8, bytes32, bytes32) public pure returns (address)"
];
let provider = ethers.getDefaultProvider('ropsten');
// Create a wallet to sign the hash with
let privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123';
let wallet = new ethers.Wallet(privateKey);
console.log(wallet.address);
// "0x14791697260E4c9A71f18484C9f997B308e59325"
let contractAddress = '0x80F85dA065115F576F1fbe5E14285dA51ea39260';
let contract = new ethers.Contract(contractAddress, abi, provider);
// The hash we wish to sign and verify
let messageHash = ethers.utils.id("Hello World");
// Note: messageHash is a string, that is 66-bytes long, to sign the
// binary value, we must convert it to the 32 byte Array that
// the string represents
//
// i.e.
// // 66-byte string
// "0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba"
//
// ... vs ...
//
// // 32 entry Uint8Array
// [ 89, 47, 167, 67, 136, 159, 199, 249, 42, 194, 163,
// 123, 177, 245, 186, 29, 175, 42, 92, 132, 116, 28,
// 160, 224, 6, 29, 36, 58, 46, 103, 7, 186]
let messageHashBytes = ethers.utils.arrayify(messageHash)
// Sign the binary data
let flatSig = await wallet.signMessage(messageHashBytes);
// For Solidity, we need the expanded-format of a signature
let sig = ethers.utils.splitSignature(flatSig);
// Call the verifyHash function
let recovered = await contract.verifyHash(messageHash, sig.v, sig.r, sig.s);
console.log(recovered);
// "0x14791697260E4c9A71f18484C9f997B308e59325"