Off-chain message Signing 검증하기 (EIP-712)

Natreeum's Blog·2026년 1월 7일

Blockchain

목록 보기
2/6

https://eips.ethereum.org/EIPS/eip-712

EIP-712

EIP-712 는 트랜젝션 데이터가 아닌 모든 데이터를 어떤 구조로 서명할 지에 대해 정의한 제안이며 사용자에게 서명할 메세지를 사람이 읽을 수 있는 구조로 보여주고, 그 구조에 따라 서명하여 신뢰성보안성을 높이는데 초점을 둡니다.

핵심 요소

TYPE_HASH

TYPE_HASH 는 기능이름(parameters) 로 된 문자열을 keccak256 으로 해싱 한 값입니다.

EIP712Domain 에 대한 TYPE_HASH 는 다음과 같습니다.

keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

domainSeperator

어떤 네트워크의 어떤 컨트랙트에서 사용되는(검증되는) 서명인지 구분하기 위한 구분자.

DomainSeperator 의 구성은 다음과 같습니다.

  • string name : 컨트랙트(DApp 혹은 Protocol)의 이름
  • string version : 컨트랙트 버전
  • uint256 chainId : 컨트랙트가 배포된 네트워크 아이디
  • address verifyingContract : 서명을 검증하는 컨트랙트의 주소(배포된 컨트랙트 주소)

Openzeppelin 에서 DomainSeperator를 생성하는 코드는 다음과 같습니다. (EIP712.sol#L90)

function _buildDomainSeparator() private view returns (bytes32) {
    return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
}

encodeType

Parameter 로 전달되는 인자의 type을 정의한 정의서. encodeType을 통해 해당 함수에 대한 TYPE_HASH 가 생성됩니다.

예시

const type = {
	transfer: [
      {name:"to", type:"address"},
      {name:"amount", type:"uint256"},
    ]
};

encodeData

Parameter 로 전달되는 인자의 값

예시

const data = {
	to : wallet.address,
  	amount : 1000000000000000000n
};

검증 과정

  1. signer는 private key 를 사용하여 메세지를 서명합니다.
    서명된 메세지는 다음 데이터들로부터 생성됩니다.

    • domainSeperator
    • encodeType
    • encodeData
  2. 서명된 메세지(signature)는 v, r, s 로 분리됩니다.

    • v, r, s 는 ECDSA(타원곡선을 이용한 전자서명알고리즘)을 위한 값
  3. 함수 호출자는 서명된 데이터(v,r,s) 와 arguments 를 함께 전달합니다.

  4. 함수에서는 전달받는 arguments 와 v,r,s 값으로 ecrecover 함수를 통해 해당 서명을 검증합니다.

    a. 서명에 사용된 값과 arguments 값이 일치하면 ecrecover서명자의 주소를 반환합니다.

    b. 서명에 사용된 값과 arguments 값이 일치하지 않을경우 ecrecover엉뚱한 주소를 반환합니다.

예시
1. owner 가 to:0x1234, amount:100 값에 대해 서명하여 Signature 생성
2. 함수호출자가 to:0x1234, amount:9000000 라는 값을 Owner 의 Signature 와 함께 호출
-> 검증 과정에서 ecrecover 가 엉뚱한 주소를 반환하여 검증 실패

1. owner 가 to:0x1234, amount:100 값에 대해 서명하여 Signature 생성
2. 함수호출자가 to:0x1234, amount:100 라는 값을 Owner 의 Signature 와 함께 호출
-> 검증 과정에서 ecrecover 가 owner의 주소를 반환하여 검증 성공

컨트랙트 코드를 함께 보며 이해도를 높이면 좋을것 같습니다.
다음은 설명을 위한 컨트랙트의 함수 코드입니다.

// mintWithApproval() 은 owner의 private key 로 sign 된 메세지(signature)가 정상일 때 민팅을 할 수 있는 함수입니다.
function mintWithApproval(
    address to,
    uint256 amount,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    // 함수의 TYPE_HASH 와 전달받은 arguments 로 structHash 를 생성합니다.
    bytes32 structHash = keccak256(
        abi.encode(
            keccak256("mintWithApproval(address to,uint256 amount)"), // TYPE_HASH
            to, // arg1
            amount // arg2
        )
    );

    bytes32 hash = _hashTypedDataV4(structHash);
      
    // 서명된 데이터(v,r,s) 에 사용된 데이터와 arguments(to, amount) 값이 같다면 ecrecover 는 서명한 지갑의 주소를 반환합니다.
    address signer = ecrecover(hash, v, r, s);
      
    
    if (signer != owner) {
        revert("Invalid signature");
    }

    _mint(to, amount);
}

EIP-712 를 따르는 서명 생성하기

EIP-712 를 따르는 서명을 만드는 방법은 다양하겠지만 이 글에서는 ehters.js 라이브러리로 서명을 생성하는 방법만 다뤄보겠습니다.

ethers.js

도메인 지정

domainSeperator 생성을 위한 도메인을 지정해줍니다.

const domain = {
	name: "Test",
  	version: "1",
  	chainId: 1,
  	verifyingContract: "0x1234contractAddress"
};

Type 지정

parameter 로 전달할 인자들의 타입을 지정해줍니다.

const types = {
	mintWithApproval:[
      {name: "to", type: "address"},
      {name: "amount", type: "uint256"},
    ]
};

Data 지정

parameter 로 전달할 인자들의 값을 지정해줍니다.

const data = {
	to: "0x9876walletAddress",
  	amount: 1000000000000000000n
};

Signature 생성 + vrs 로 분리

ethers 의 Wallet instance 로 서명을 생성합니다.

const owner = ethers.Wallet.createRandom();

const signature = owner.signTypedData(domain, types, data);

const {v, r, s} = ethers.Signature.from(signature);
profile
BlockChain DEV

0개의 댓글