对消息签名

优质
小牛编辑
135浏览
2023-12-01

Signing messages can be used for various method of authentication and off-chain operations, which can be put on-chain if necessary.


字符串签名

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.

Solidity Contract
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);
    }
}
JavaScript
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 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.

Solidity Contract
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);
    }
}
JavaScript
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"