OpenZeppelin ECDSA 라이브러리 분석

frenchkebab·2023년 5월 9일
0

EIP / Open Source

목록 보기
1/9
post-thumbnail

항상 OZ의 ECDSA라이브러리를 사용하라는 얘기만 들었고 코드를 볼때마다 항상 뭔가가 조금 아리송 해서, 이번 기회에 싹 정리를 해보려고 한다.

[1] Error 처리

    enum RecoverError {
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS,
        InvalidSignatureV // Deprecated in v4.8
    }

    function _throwError(RecoverError error) private pure {
        if (error == RecoverError.NoError) {
            return; // no error: do nothing
        } else if (error == RecoverError.InvalidSignature) {
            revert("ECDSA: invalid signature");
        } else if (error == RecoverError.InvalidSignatureLength) {
            revert("ECDSA: invalid signature length");
        } else if (error == RecoverError.InvalidSignatureS) {
            revert("ECDSA: invalid signature 's' value");
        }
    }

역시 OZ라그런지... CustomError로 정의하지를 않고 enum으로 정의하는 새로운 패턴을 알게되었다.

기본적으로 tryRecover에서 error code를 return하면 recover에서 해당 error code와 함께 _thorwError함수를 통해 revert를 해주는 패턴이다.

[2] functions

우선은 어떤 함수들이 있는지 한번 슥 훑어보자

    function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError)

	function recover(bytes32 hash, bytes memory signature) internal pure returns
    
    function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError)

	function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address)

	function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError)

	function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address)

	function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message)

	function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32)

	function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns
    
   	function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32)

생각보다 이것저것 많은데 하나씩 살펴보자!

1) tryRecover / recover 오버로딩

다시 쭉 살펴보니 오버로딩으로 3쌍의 tryRecoverrecover이 있다.

tryRecover / recover 1 - signature 통짜로

    function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            /// @solidity memory-safe-assembly
            assembly {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength);
        }
    }

    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        (address recovered, RecoverError error) = tryRecover(hash, signature);
        _throwError(error);
        return recovered;
    }

v, r, s를 따로 나누지 않고 bytes로 한 번에 signature를 받는 경우를 위한 함수 정의이다.

2) tryRecover / recover 2 - r, vs

    function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) {
        bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
        uint8 v = uint8((uint256(vs) >> 255) + 27);
        return tryRecover(hash, v, r, s);
    }

    function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature);
        }

        return (signer, RecoverError.NoError);
    }

r, vs로 나누는 케이스는 처음 보는 것 같다.
주석을 보면 ERC2098에 따른 signature이다.

ERC-2098: Compact Signature Representation

ERC-2098을 살펴보면 v값은 항상 27 혹은 28이라 이 이 부분에 대해 압축을 해서 signature의 output을 65바이트 r(32) + v(32) + s(1)를 64바이트 r(32) + vs(32)로 줄이고자 하는 것 같다.

calldata의 1byte의 가스가 얼마나 들려나 하고 봤더니 (여기) zero 1byte의 경우 4 gas, non-zero일 경우 16 gas라고 한다.

조금 더 자세히 읽어보니 기존의 경우 256 bit (32byte) 정렬로 만약 0이 padding이 들어가게 되면 실제로 32 * 3 byte 만큼의 calldata가 사용된다.

따라서 ERC2098에 따라 vs값을 압축하면 1개의 256bit calldata를 줄일 수 있게 되고, 32 * 16 = 512 gas 만큼을 아낄 수 있게 된다.

gas 소비량 외에도, RLP인코딩 된 실제 트랜잭션에서도 1.5바이트를 절약할 수 있다고 한다.

v, r, s값에 대해서는 Mastering Ethereum을 보면 자세히 알 수 있다.

3) tryRecover / recover 3 - v, r, s

    function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature);
        }

        return (signer, RecoverError.NoError);
    }

    function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        (address recovered, RecoverError error) = tryRecover(hash, v, r, s);
        _throwError(error);
        return recovered;
    }

signature를 v,r,s로 나누어서 받는 가장 익숙한 방식이다.

오버로딩된 위의 2가지의 경우에도 결국 assembly로 v, r, s를 쪼개서 이 함수를 다시 호출해준다.

그 외 hashing 함수들

toEthSignedMessageHash(bytes32)

    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) {
        // 32 is the length in bytes of hash,
        // enforced by the type signature above
        /// @solidity memory-safe-assembly
        assembly {
            mstore(0x00, "\x19Ethereum Signed Message:\n32")
            mstore(0x1c, hash)
            message := keccak256(0x00, 0x3c)
        }
    }

"\x19Ethereum Signed Message:\n32" + hash를 다시 keccak256 hash로 만들어준다.

"\x19Ethereum Signed Message:\n32"를 붙이는 이유는 실제 RLP encoded된 transaction과 구분하기 위해서이다.

toEthSignedMessageHash(bytes)

    function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s));
    }

위의 경우에는 이미 keccak256해싱이 된 bytes32 값을 파라미터로 받는 경우였고, 이 경우에는 bytes로 받은 값을 해싱하는 경우이다. 단순히 문자열 메세지를 해싱하거나 하는 경우인 것 같고, off-chain 서명의 경우에는 크게 쓰이지 않을 것 같다.

toTypedDataHash

    function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) {
        /// @solidity memory-safe-assembly
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, "\x19\x01")
            mstore(add(ptr, 0x02), domainSeparator)
            mstore(add(ptr, 0x22), structHash)
            data := keccak256(ptr, 0x42)
        }
    }

EIP712의 domainSeperator의 hash값, struct의 hash값을 다시 hash해서 묶어주는 역할이다.

EIP712에 대해서는 이후에 조금 더 다뤄볼 생각이다.

toDataWithIntendedValidatorHash

    function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19\x00", validator, data));
    }

EIP-191의 경우를 따라 validator를 넣어서 서명하는 경우이다. EIP-191을 살펴보면 Multisig Wallet에서 많이 사용되는 것 같다.

EIP-191에 대해서도 Signing에 대한 표준인 만큼 한 번 다루어보아야 겠다.

profile
Solidity에 대해 공부하고 있습니다.

0개의 댓글