Michael.W基于Foundry精读Openzeppelin第30期——ECDSA.sol

  • Michael.W
  • 更新于 2023-08-23 01:17
  • 阅读 1105

ECDSA(Elliptic Curve Digital Signature Algorithm)是椭圆曲线数字签名算法的简称。ECDSA库十分重要且使用广泛,其作用是在链上验证某message是否由给定的地址的私钥持有者进行签名的。简而言之,ECDSA库是一个验证地址真实身份的工具库。

0. 版本

[openzeppelin]:v4.8.3,[forge-std]:v1.5.6

0.1 ECDSA.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/cryptography/ECDSA.sol

ECDSA(Elliptic Curve Digital Signature Algorithm)是椭圆曲线数字签名算法的简称。ECDSA库十分重要且使用广泛,其作用是在链上验证某message是否由给定的地址的私钥持有者进行签名的。简而言之,ECDSA库是一个验证地址真实身份的工具库。

1. 目标合约

封装ECDSA library成为一个可调用合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/utils/cryptography/MockECDSA.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";

contract MockECDSA {
    using ECDSA for bytes32;

    function tryRecover(bytes32 hash, bytes memory signature) external pure returns (address, ECDSA.RecoverError) {
        return hash.tryRecover(signature);
    }

    function recover(bytes32 hash, bytes memory signature) external pure returns (address) {
        return hash.recover(signature);
    }

    function tryRecover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) external pure returns (address, ECDSA.RecoverError) {
        return hash.tryRecover(r, vs);
    }

    function recover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) external pure returns (address){
        return hash.recover(r, vs);
    }

    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external pure returns (address, ECDSA.RecoverError) {
        return hash.tryRecover(v, r, s);
    }

    function recover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external pure returns (address){
        return hash.recover(v, r, s);
    }

    function toEthSignedMessageHash(bytes32 hash) external pure returns (bytes32) {
        return hash.toEthSignedMessageHash();
    }

    function toEthSignedMessageHash(bytes memory s) external pure returns (bytes32) {
        return ECDSA.toEthSignedMessageHash(s);
    }

    function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) external pure returns (bytes32) {
        return domainSeparator.toTypedDataHash(structHash);
    }
}

全部foundry测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/cryptography/ECDSA.t.sol

测试数据:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/cryptography/data/ECDSA_test.json

测试数据的生成脚本:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/cryptography/ECDSA_test.ts

import {ethers} from 'ethers'
import {writeFileSync} from 'fs'

// for:
//      function toEthSignedMessageHash(bytes32 hash)
const digestHash = ethers.keccak256(ethers.toUtf8Bytes('Michael.W'))
const ethSignedMessageHashFromHash = ethers.hashMessage(ethers.getBytes(digestHash))

// for:
//      function toEthSignedMessageHash(bytes memory s)
const ethSignedMessageHashFromBytes = ethers.hashMessage('Michael.W')

// for:
//      function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash)
const domain = {
    'name': 'test name',
    'version': '1',
    'chainId': 1024,
    'verifyingContract': '0x7a41fc8b73D6F307830b88878caf48D077128F63',
}

const types = {
    'Student': [
        {'name': 'address', 'type': 'address'},
        {'name': 'age', 'type': 'uint256'},
    ]
}

const value = {
    'address': ethers.ZeroAddress,
    'age': 18,
}

const structHash = ethers.TypedDataEncoder.from(types).hash(value)
const typedDataHash = ethers.TypedDataEncoder.hash(domain, types, value)

// generate signature
const wallet = new ethers.Wallet(ethers.toBeHex(1024, 32))
const signature = wallet.signMessageSync('Michael.W')

// generate compact signature following EIP-2098
const signatureCompact = wallet.signingKey.sign(ethers.hashMessage('Michael.W'))
const signatureCompactR = signatureCompact.r
const signatureCompactVS = signatureCompact.yParityAndS

const output = {
    eth_signed_msg_hash_from_hash: ethSignedMessageHashFromHash,
    eth_signed_msg_hash_from_bytes: ethSignedMessageHashFromBytes,
    struct_hash: structHash,
    typed_data_hash: typedDataHash,
    valid_signature: signature,
    compact_signature_r: signatureCompactR,
    compact_signature_vs: signatureCompactVS
}

writeFileSync('test/utils/cryptography/data/ECDSA_test.json', JSON.stringify(output))

2. 代码精读

2.1 toEthSignedMessageHash(bytes32 hash) && toEthSignedMessageHash(bytes memory s)

  • toEthSignedMessageHash(bytes32 hash):将传入的摘要hash值转换为Ethereum特定的签名msg;
  • toEthSignedMessageHash(bytes memory s):将传入的字节数组s转换为Ethereum特定的签名msg。

以上方法生成的Ethereum特定签名msg正是ethereum json-rpc中用于签名的eth_sign()方法内部进行签名的真实内容。细节见:https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign

注:Ethereum特定的签名msg结构为:

"\x19Ethereum Signed Message:\n" + 原始签名内容字节长度 + 原始签名内容

通过在msg中添加前缀使后续计算出的签名可以被识别为Ethereum特定的签名,这样做是防止滥用。如果solidity内置的验签功能默认是对原有msg进行验证(例如签名msg为Ethereum的transaction),那么一些恶意的dapp就可以直接从区块中获取到历史transaction的签名进而伪装成发送者。细节内容见:EIP-191

    // 使用枚举定义本库内部不同的错误类型
    enum RecoverError {
        // 无错误
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS,
        // 注:InvalidSignatureV在Openzeppelin v4.8中已遭弃用
        InvalidSignatureV
    }

    // 根据传入的error的类别的不同,触发不同的revert msg
    function _throwError(RecoverError error) private pure {
        if (error == RecoverError.NoError) {
            // 如果error为RecoverError.NoError,表示无错误发生,直接返回
            return; 
        } else if (error == RecoverError.InvalidSignature) {
            // 如果使用solidity内置的ecrecover()方法还原出的签名者地址为零地址,以该msg revert
            revert("ECDSA: invalid signature");
        } else if (error == RecoverError.InvalidSignatureLength) {
            // 如果用于验签的签名长度不是65字节,以该msg revert
            revert("ECDSA: invalid signature length");
        } else if (error == RecoverError.InvalidSignatureS) {
            // 如果用于验签的签名的s值大于secp256k1n/2,以该msg revert
            revert("ECDSA: invalid signature 's' value");
        }
    }

    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
    // 由于keccak256的hash值为固定的32字节,所以原始签名内容字节长度写死成32。首先为原始hash添加前缀"\x19Ethereum Signed Message:\n32",然后返回整个msg的hash值
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }

    function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
        // Strings.toString(s.length):将字节数组s的长度转换为对应字符串
        // 为原始msg(字节数组s)添加前缀"\x19Ethereum Signed Message:\n" + 原始msg的字节长度,然后返回整个msg的hash值
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s));
    }

foundry代码验证

contract ECDSATest is Test {
    using stdJson for string;

    MockECDSA me = new MockECDSA();
    string jsonTestData = vm.readFile("test/utils/cryptography/data/ECDSA_test.json");

    function test_ToEthSignedMessageHash() external {
        // case 1: hash digest
        bytes32 digestHash = keccak256("Michael.W");
        bytes32 ethSignedMessageHash = me.toEthSignedMessageHash(digestHash);
        bytes32 expectedEthSignedMessageHash = jsonTestData.readBytes32(".eth_signed_msg_hash_from_hash");
        assertEq(expectedEthSignedMessageHash, ethSignedMessageHash);

        // case 2: bytes digest
        bytes memory digestBytes = bytes("Michael.W");
        ethSignedMessageHash = me.toEthSignedMessageHash(digestBytes);
        expectedEthSignedMessageHash = jsonTestData.readBytes32(".eth_signed_msg_hash_from_bytes");
        assertEq(expectedEthSignedMessageHash, ethSignedMessageHash);
    }
}

2.2 toTypedDataHash(bytes32 domainSeparator, bytes32 structHash)

通过传入的domainSeparator和structHash计算Ethereum Signed Typed Data的Ethereum特定签名msg。

该方法生成的Ethereum特定签名msg正是EIP-712中提出的ethereum json-rpc方法——eth_signTypedData的内部进行签名的真实内容。细节见:https://eips.ethereum.org/EIPS/eip-712

注:Ethereum特定的签名msg结构为:

"\x19\x01" + domainSeparator + 结构数据的hash值

    function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
        // 连接"\x19\x01"、传入domainSeparator以及structHash,并取hash值返回
        return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
    }

foundry代码验证

contract ECDSATest is Test {
    using stdJson for string;

    MockECDSA me = new MockECDSA();
    string jsonTestData = vm.readFile("test/utils/cryptography/data/ECDSA_test.json");

    function test_ToTypedDataHash() external {
        // set chain id
        vm.chainId(1024);
        // get fixed address of TargetEIP712 contract
        vm.setNonce(address(1024), 1024);
        vm.prank(address(1024));
        TargetEIP712 te = new TargetEIP712();
        // fixed contract address is 0x7a41fc8b73D6F307830b88878caf48D077128F63
        assertEq(0x7a41fc8b73D6F307830b88878caf48D077128F63, address(te));

        bytes32 structHash = jsonTestData.readBytes32(".struct_hash");
        bytes32 typedDataHash = me.toTypedDataHash(te.getDomainSeparator(), structHash);
        bytes32 expectedTypedDataHash = jsonTestData.readBytes32(".typed_data_hash");
        assertEq(expectedTypedDataHash, typedDataHash);
    }
}

contract TargetEIP712 is EIP712("test name", "1") {
    function getDomainSeparator() external view returns (bytes32){
        return _domainSeparatorV4();
    }
}

2.3 tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) && recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)

  • tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s):使用传入的摘要hash和各自独立的签名v、r、s值,还原出签名者的地址以及错误类型。注:该过程不会触发revert;
  • recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s):使用传入的摘要hash和各自独立的签名v、r、s值,还原出签名者的地址。注:该过程是对tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)的封装,如果产生错误会触发revert。

ps:

EIP-2中提到:签名的s值大于secp256k1n/2的交易都将被认作是无效的。但是solidity内置方法ecrecover()却无需加入此限制。这样做的目的是为了使以太坊合约依旧可以recover老版Bitcoin的签名。细节见:https://eips.ethereum.org/EIPS/eip-2

同时,以太坊黄皮书的附录F定义了有效的s值范围为(0,secp256k1n÷2+1),有效的v值为27或28。当前常用的椭圆曲线签名库大多都会生成一个唯一签名,其s值处于(0,secp256k1n÷2+1)。如果你使用的库生成了具有延展性的签名,其s值处于[secp256k1n÷2+1,secp256k1n),可以利用如下方式计算出新的s值:

s_new = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s,并将v值从27变成28(或将28变成27)。

如果你使用的库生成的v值是0或1,请将其加上27变为27或28。

以太坊黄皮书:https://ethereum.github.io/yellowpaper/paper.pdf

    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address, RecoverError) {
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            // 如果签名的s值大于secp256k1n/2,说明此签名无效(EIP-2规定),直接返回零地址和错误类型InvalidSignatureS
            return (address(0), RecoverError.InvalidSignatureS);
        }

        // 利用solidity内置的ecrecover方法,通过摘要hash和签名还原出签名者的地址
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            // 如果还原出的签名者地址为零地址说明此签名无效。返回零地址错误类型InvalidSignature
            return (address(0), RecoverError.InvalidSignature);
        }

    // 返回签名者地址以及无错误标识NoError
        return (signer, RecoverError.NoError);
    }

    function recover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address) {
        // 使用传入的摘要hash和各自独立的签名v、r、s值,还原出签名者的地址以及错误类型
        (address recovered, RecoverError error) = tryRecover(hash, v, r, s);
        // 根据上面返回的错误类型触发相应revert msg
        _throwError(error);
        // 如果tryRecover()的过程中无错误产生,返回还原出签名者的地址
        return recovered;
    }

foundry代码验证

contract ECDSATest is Test {
    using stdJson for string;

    MockECDSA me = new MockECDSA();
    uint signerPrivateKey = 1024;
    address signerAddress = vm.addr(signerPrivateKey);
    string jsonTestData = vm.readFile("test/utils/cryptography/data/ECDSA_test.json");

    function test_TryRecover_WithVRS() external {
        // case 1: pass tryRecover() with no RecoverError
        bytes32 ethSignedMessageHash = me.toEthSignedMessageHash(bytes("Michael.W"));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, ethSignedMessageHash);
        (address signerRecovered, ECDSA.RecoverError error) = me.tryRecover(ethSignedMessageHash, v, r, s);
        assertEq(signerAddress, signerRecovered);
        assertTrue(error == ECDSA.RecoverError.NoError);

        // case 2: return InvalidSignatureS error with an s value > secp256k1n/2
        bytes32 sInvalid = bytes32(type(uint).max);
        (signerRecovered, error) = me.tryRecover(ethSignedMessageHash, v, r, sInvalid);
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignatureS);

        // case 3: return InvalidSignature error with zero v/r/s
        (signerRecovered, error) = me.tryRecover(ethSignedMessageHash, 0, 0, 0);
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignature);

        // case 4: return an arbitrary signer and no RecoverError for another hash digest
        (signerRecovered, error) = me.tryRecover(me.toEthSignedMessageHash(bytes("Michael.W/Michael.W")), v, r, s);
        assertNotEq(signerAddress, signerRecovered);
        assertTrue(error == ECDSA.RecoverError.NoError);
    }

    function test_Recover_WithVRS() external {
        // case 1: pass recover() with no RecoverError
        bytes32 ethSignedMessageHash = me.toEthSignedMessageHash(bytes("Michael.W"));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, ethSignedMessageHash);
        address signerRecovered = me.recover(ethSignedMessageHash, v, r, s);
        assertEq(signerAddress, signerRecovered);

        // case 2: revert with an s value > secp256k1n/2
        bytes32 sInvalid = bytes32(type(uint).max);
        vm.expectRevert("ECDSA: invalid signature 's' value");
        me.recover(ethSignedMessageHash, v, r, sInvalid);

        // case 3: revert with zero v/r/s
        vm.expectRevert("ECDSA: invalid signature");
        me.recover(ethSignedMessageHash, 0, 0, 0);

        // case 4: return an arbitrary signer for another hash digest
        signerRecovered = me.recover(me.toEthSignedMessageHash(bytes("Michael.W/Michael.W")), v, r, s);
        assertNotEq(signerAddress, signerRecovered);
    }
}

2.4 tryRecover(bytes32 hash, bytes memory signature) && recover(bytes32 hash, bytes memory signature)

  • tryRecover(bytes32 hash, bytes memory signature):使用传入的摘要hash和签名(r、v和s值合并在一起)还原出签名者的地址以及错误类型。注:该过程不会触发revert;
  • recover(bytes32 hash, bytes memory signature):使用传入的摘要hash和签名(r、v和s值合并在一起)还原出签名者的地址。

ps:生成签名使用的工具库如下:

    function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
        // 该库可验签的签名长度必须为65字节
        if (signature.length == 65) {
            // 由于椭圆曲线验签的内置函数ecrecover()无法直接传入整个签名。所以按照标准,需要将65字节签名分割成32字节的r和s以及1字节的v
            // 定义r,s,v,它们的长度比——32:32:1
            bytes32 r;
            bytes32 s;
            uint8 v;

            /// @solidity memory-safe-assembly
            // 在内联汇编中进行bytes数组的切割
            assembly {
                // 在memory中,signature开始的前32字节存放的是signature的字节长度
                // mload(add(signature, 0x20)):取出signature后第2个字(32字节)的内容给r
                r := mload(add(signature, 0x20))
                // mload(add(signature, 0x40)):取出signature后第3个字(32字节)的内容给s
                s := mload(add(signature, 0x40))
                // mload(add(signature, 0x60)):取出signature后第4个字(32字节)的内容;
                // byte(0, mload(add(signature, 0x60))):取上述内容(32字节)的第1个字节内容给v
                v := byte(0, mload(add(signature, 0x60)))
            }

            // 将分割后的r,s,v及hash传入tryRecover()并return执行结果
            return tryRecover(hash, v, r, s);
        } else {
            // 如果传入签名不是65字节,直接返回零地址和InvalidSignatureLength错误
            return (address(0), RecoverError.InvalidSignatureLength);
        }
    }

    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        // 使用传入的摘要hash和签名(r、v和s值合并在一起)还原出签名者的地址以及错误类型
        (address recovered, RecoverError error) = tryRecover(hash, signature);
        // 根据上面返回的错误类型触发相应revert msg
        _throwError(error);
        // 如果tryRecover()的过程中无错误产生,返回还原出签名者的地址
        return recovered;
    }

foundry代码验证

contract ECDSATest is Test {
    using stdJson for string;

    MockECDSA me = new MockECDSA();
    uint signerPrivateKey = 1024;
    address signerAddress = vm.addr(signerPrivateKey);
    string jsonTestData = vm.readFile("test/utils/cryptography/data/ECDSA_test.json");

    function test_TryRecover_WithSignature() external {
        // case 1: pass tryRecover() with no RecoverError
        bytes memory validSig = jsonTestData.readBytes(".valid_signature");
        bytes32 digestHash = me.toEthSignedMessageHash(bytes("Michael.W"));
        (address signerRecovered, ECDSA.RecoverError error) = me.tryRecover(digestHash, validSig);
        assertEq(signerAddress, signerRecovered);
        assertTrue(error == ECDSA.RecoverError.NoError);

        // case 2: return InvalidSignatureLength if signature's length != 65
        (signerRecovered, error) = me.tryRecover(digestHash, '0x');
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignatureLength);

        // case 3: return InvalidSignatureS if s value (second bytes32) in signature > secp256k1n/2
        bytes memory invalidSig = abi.encodePacked(bytes32(0), type(uint).max, uint8(0));
        (signerRecovered, error) = me.tryRecover(digestHash, invalidSig);
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignatureS);

        // case 4: return InvalidSignature error with signature of zero v/r/s
        invalidSig = abi.encodePacked(bytes32(0), bytes32(0), uint8(0));
        (signerRecovered, error) = me.tryRecover(digestHash, invalidSig);
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignature);

        // case 5: return an arbitrary signer and no RecoverError for another hash digest
        (signerRecovered, error) = me.tryRecover(me.toEthSignedMessageHash(bytes("Michael.W/Michael.W")), validSig);
        assertNotEq(signerAddress, signerRecovered);
        assertTrue(error == ECDSA.RecoverError.NoError);
    }

    function test_Recover_WithSignature() external {
        // case 1: pass recover() with no RecoverError
        bytes memory validSig = jsonTestData.readBytes(".valid_signature");
        bytes32 digestHash = me.toEthSignedMessageHash(bytes("Michael.W"));
        address signerRecovered = me.recover(digestHash, validSig);
        assertEq(signerAddress, signerRecovered);

        // case 2: revert if signature's length != 65
        vm.expectRevert("ECDSA: invalid signature length");
        me.recover(digestHash, '0x');

        // case 3: revert if s value (second bytes32) in signature > secp256k1n/2
        bytes memory invalidSig = abi.encodePacked(bytes32(0), type(uint).max, uint8(0));
        vm.expectRevert("ECDSA: invalid signature 's' value");
        me.recover(digestHash, invalidSig);

        // case 4: revert with signature of zero v/r/s
        invalidSig = abi.encodePacked(bytes32(0), bytes32(0), uint8(0));
        vm.expectRevert("ECDSA: invalid signature");
        me.recover(digestHash, invalidSig);

        // case 5: return an arbitrary signer and no RecoverError for another hash digest
        signerRecovered = me.recover(me.toEthSignedMessageHash(bytes("Michael.W/Michael.W")), validSig);
        assertNotEq(signerAddress, signerRecovered);
    }
}

2.5 tryRecover(bytes32 hash, bytes32 r, bytes32 vs) && recover(bytes32 hash, bytes32 r, bytes32 vs)

  • tryRecover(bytes32 hash, bytes32 r, bytes32 vs):使用传入的摘要hash、签名r值以及合并的vs值,还原出签名者的地址以及错误类型。注:该过程不会触发revert;
  • recover(bytes32 hash, bytes32 r, bytes32 vs):使用传入的摘要hash、签名r值以及合并的vs值,还原出签名者的地址。注:该过程是对tryRecover(bytes32 hash, bytes32 r, bytes32 vs)的封装,如果产生错误会revert。

ps:关于为何要将v和s合并到一个bytes32中以及如何合并vs值,参见:https://eips.ethereum.org/EIPS/eip-2098

    function tryRecover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) internal pure returns (address, RecoverError) {
        // 从合并的vs中解析出s值:过滤出vs中的低255位作为s
        bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
        // 从合并的vs中解析出v值:过滤出vs中的最高位+27作为v值
        uint8 v = uint8((uint256(vs) >> 255) + 27);
        // 使用传入的摘要hash和各自独立的签名v、r、s值,还原出签名者的地址以及错误类型
        return tryRecover(hash, v, r, s);
    }

    function recover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) internal pure returns (address) {
        // 使用传入的摘要hash、签名r值以及合并的vs值,还原出签名者的地址以及错误类型
        (address recovered, RecoverError error) = tryRecover(hash, r, vs);
        // 根据上面返回的错误类型触发相应revert msg
        _throwError(error);
        // 如果tryRecover()的过程中无错误产生,返回还原出签名者的地址
        return recovered;
    }

foundry代码验证

contract ECDSATest is Test {
    using stdJson for string;

    MockECDSA me = new MockECDSA();
    uint signerPrivateKey = 1024;
    address signerAddress = vm.addr(signerPrivateKey);
    string jsonTestData = vm.readFile("test/utils/cryptography/data/ECDSA_test.json");

    function test_TryRecover_WithRAndVS() external {
        // case 1: pass tryRecover() with no RecoverError
        bytes32 ethSignedMessageHash = me.toEthSignedMessageHash(bytes("Michael.W"));
        bytes32 r = jsonTestData.readBytes32(".compact_signature_r");
        bytes32 vs = jsonTestData.readBytes32(".compact_signature_vs");
        (address signerRecovered, ECDSA.RecoverError error) = me.tryRecover(ethSignedMessageHash, r, vs);
        assertEq(signerAddress, signerRecovered);
        assertTrue(error == ECDSA.RecoverError.NoError);

        // case 2: return InvalidSignatureS error with an s value > secp256k1n/2
        bytes32 vsInvalid = bytes32(type(uint).max);
        (signerRecovered, error) = me.tryRecover(ethSignedMessageHash, r, vsInvalid);
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignatureS);

        // case 3: return InvalidSignature error with zero r/vs
        (signerRecovered, error) = me.tryRecover(ethSignedMessageHash, 0, 0);
        assertEq(address(0), signerRecovered);
        assertTrue(error == ECDSA.RecoverError.InvalidSignature);

        // case 4: return an arbitrary signer and no RecoverError for another hash digest
        (signerRecovered, error) = me.tryRecover(me.toEthSignedMessageHash(bytes("Michael.W/Michael.W")), r, vs);
        assertNotEq(signerAddress, signerRecovered);
        assertTrue(error == ECDSA.RecoverError.NoError);
    }

    function test_Recover_WithRAndVS() external {
        // case 1: pass recover() with no RecoverError
        bytes32 ethSignedMessageHash = me.toEthSignedMessageHash(bytes("Michael.W"));
        bytes32 r = jsonTestData.readBytes32(".compact_signature_r");
        bytes32 vs = jsonTestData.readBytes32(".compact_signature_vs");
        address signerRecovered = me.recover(ethSignedMessageHash, r, vs);
        assertEq(signerAddress, signerRecovered);

        // case 2: revert with an s value > secp256k1n/2
        bytes32 vsInvalid = bytes32(type(uint).max);
        vm.expectRevert("ECDSA: invalid signature 's' value");
        me.recover(ethSignedMessageHash, r, vsInvalid);

        // case 3: revert with zero r/vs
        vm.expectRevert("ECDSA: invalid signature");
        me.recover(ethSignedMessageHash, 0, 0);

        // case 4: return an arbitrary signer for another hash digest
        signerRecovered = me.recover(me.toEthSignedMessageHash(bytes("Michael.W/Michael.W")), r, vs);
        assertNotEq(signerAddress, signerRecovered);
    }
}

ps:\ 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

公众号名称:后现代泼痞浪漫主义奠基人

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Michael.W
Michael.W
0x93E7...0000
狂热的区块链爱好者