체인의정석

ERC4337) 계정추상화 필수 로직, 페이마스터와 Account Wallet에 맞는 유효한 서명 및 검증로직 만들기 (UserOp 생성 및 검증 로직) 및 체크할 사항들 본문

블록체인/계정추상화

ERC4337) 계정추상화 필수 로직, 페이마스터와 Account Wallet에 맞는 유효한 서명 및 검증로직 만들기 (UserOp 생성 및 검증 로직) 및 체크할 사항들

체인의정석 2024. 12. 19. 18:28
728x90
반응형

계정추상화를 이용한 코드를 3번째 짜면서 이제 좀 익숙해졌기에 잊지 않기 위해서 여기에 핵심 내용들을 다시 정리해둔다.

구현 전에 체크해야할 사항들

먼저 계정추상화의 현재 최신 버전인 0.7.0과 0.6.0 은 userOp의 구성요소가 다르다. 0.7.0이 userOp를 더 간소화 시켰기에 가스비가 더 효율적이지만 0.6.0으로 구현된 프로젝트들도 많기 때문에 버전을 항상 체크해야한다.

"@account-abstraction/contracts": "^0.6.0",

해당 모듈을 사용하면 된다. 해당 코드는 eip 4337을 제안한 팀에서 만들다 보니 교과서처럼 보고 쓸 수 있다.

또한 각 버전별로 호환되는 오픈제플린 버전이 필요한데 0.6.0 버전의경우

"@openzeppelin/contracts": "^4.9.3",
 

해당 버전을쓰면 된다.

그리고 entryPoint를 직접 배포해서 테스트한다면 옵티마이져의 run 값을 100정도로 두고 해야 컨트렉트가 사이즈 리밋에 안걸리고 배포가 잘된다. (이더리움 기준) 안그러면 오류가 나서 나도 좀 해멨다.

먼저 두 버전 모두 entryPoint는 네트워크 별로 배포되어 있는것을 사용하던지 직접 사용할 수 있게 미리 구현이 완성 상태로 되어있다. 그리고 0.7.0에서는 SimpleWalletFactory, SimpleWallet, SimplePaymaster 등을 제공하고 있어 사실상 그대로 써도 문제가 없고 0.6.0 버전에서는 인터널 함수를 오버라이딩하여 사용해야 한다. 여기서의 요지는 결국 컨트렉트 보다는 서명을 만드는 과정이 좀 복잡하다는 것인데 그 순서가 중요하다. 실제로 실행은 EntryPoint의 handleOps 부분을 통해서 실행하지만 userOp를 유효하게 만드는것 자체가 컨트렉트에 대한 이해도가 없다면 만드는게 쉽지 않다.

그리고 주의해야할 점 또한가지 ethers의 최신버전의 getAddress 명령어와 SimpleWallet에 기본적으로 있는 getAddress함수의 경우 따로 구분을 안해서 쓰면 오류가 난다. 그래서 월렛의 지갑주소 생성 조회 함수를 바꾸거나 따로 abi를 넣어서 지정해서 호출해 주어야한다. 여기서도 오류가 나서 시간을 몇번이나 날렸다.

1. 검증로직 - validateUserOp

먼저 https://eips.ethereum.org/EIPS/eip-4337 여기 보면 필수적인 검증 로직을 알 수 있다.

페이마스터가 빠진 경우 위처럼 실행이 되는데 보는것처럼 validatUserOp가 중요하다.

문서도 예제도 친절한 것이 없기에 일단 제대로 된 서명을 하기 위해서는 직접 모듈로 받은 컨트렉트 구조를 보고 그에 맞는 서명을 해야한다. 그래서 vs code에서

일단 이런식으로 모듈 안의 코드 검색을 쉽게 하게 해준다.

일단 기본 예제로 들어가 있는것은 eip191 서명이다. 해당 서명은 사실 signMessage를 하면 기본적으로 이루어지는 서명으로 별도의 과정 없이 가장 보편적으로 만들 수 있다. 그치만 어떤 서명의 방식을 쓰던지 해당 서명을 만들고 검증하는 과정은 컨트렉트에서 똑같이 이루어져야 한다.

먼저 EntryPoint를 보도록하겠다.

보면

using UserOperationLib for UserOperation;

이런 식으로 라이브러리를 현재 사용하고 있음을 알 수 있다. 

실제로 userOp를 가져와서 어떤식으로 해시를 취하고 검증하는지 알아야 그에 맞는 userOp 서명을 생성하고 만들 수 있으므로 

위의 함수에서 쓰이는 Pack과 hash를 먼저 찾아볼 것이다.

보면 pack에서는 userOp에 대한 내용들을 encode 하고 있으며 hash에서는 pack을 한 후 해시를  취하는것을 볼 수 있다.

그래서 외부에서는

await ethers.utils.solidityPack

해당 함수를 통해서 (실행은 ethers 버전별로 다르다) solidityPack을 하여 똑같이 userOp에 대한 pack된 값을 구해야 한다.

여기서 주의 할 점은 바로 signature에 대한 부분이다. signature는 위에서 pack할때 쓰이지 않기 때문에 "0x" 값을 넣어서 먼저 해시를 만들어주어야 한다. paymasterAndData의 경우 필수가 아니므로 뒤에서 다루겠다.

userOp hash의 경우 그대로 서명의 메세지로 쓰이면 안된다. 컨트렉트를 보면

해당 부분이 있다. 해당 부분을 컨트렉트에서 불러와서 검증하기 때문에 userOp의 해시를 뽑아낸 후에 엔트리포인트의 주소, 체인 아이디를 포함해서 서명을 해주어야 한다.

  const abiCoder = ethers.utils.defaultAbiCoder
   const enc = abiCoder.encode(
      ["bytes32", "address", "uint256"],
      [userOp의 해시, entryPoint주소, chainId]
    );
    const userOpHash = ethers.utils.keccak256(enc);

이런 식으로 인코딩을 하고 난 후에 해시 값을 뽑아내야지만 getUserOpHash와 똑같은 값을 뽑아낼 수 있다. 만약 안된다면 엔트리포인트에 있는 해당 함수를 호출한 후 체하여 같은 값이 나오는지 봐야한다.

그리고 이제 해당 메세지를 서명해 주어야 한다. 서명의 경우 사실 개별적으로 구현한 검증 로직에 대응되기만 하면 되지만 여기서 우리는 eip191서명을 쓰고 있다. 해당 서명 생성은 기본적으로 지원이 되기 때문에 간단하다.

  const networkProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); // Replace with your network
  const entryPoint = await ethers.deployContract("EntryPoint");
  const privateKey = "개인키"; // Test private key
  const wallet = new ethers.Wallet(privateKey, networkProvider);
  //userOpHash 만들기
  const signature = await wallet.signMessage(ethers.utils.arrayify(userOpHash));

이런 식으로 개인키를 넣고 wallet을 만든 후에 signMessage만 해주면 된다. 그렇게 나온 값이 signature이다.

signature가 나오면 마지막으로 userOp에서 원래 "0x"값으로 있던 부분을 위에서 도출한 서명값으로 바꾼 후 이를 통해서 엔트리포인트에 보내버리면 된다.

그럴 경우 자체적으로 wallet에서 정의한

    function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)

를 오버라이딩하여 상속하면 된다.

여기서는 191서명을 썼기때문에

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

이 컨트렉트의

요걸 쓰면된다. 그럼 signMessage와 동일한 eip191 서명이 나오게 된다.

그리고 검증의 경우에는

이 함수를 쓰면 된다. 

    function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) 
        internal virtual override returns (uint256 validationData) 
    {
        bytes32 hash = ECDSA.toEthSignedMessageHash(userOpHash);
        address signer = ECDSA.recover(hash, userOp.signature);
        require(signer == owner, "Invalid signature");
        return 0;
    }

그 후 이런식으로 검증 로직을 넣어주게 되면 ethers의 signMessage여부를 체크하고 리턴하게 된다.

참고로 valdationData는 0 값을 리턴하고, 여기서 validUntil, validAfter를 넣어서 체크하는 부분도 공식 문서상에서 넣을 수 있기 때문에 해당 내용이 들어갈 경우 헤당 내용을 고려해서 추가해주어야 한다.

 

2. 검증로직 - validatePaymasterUserOp

자 이제 paymaster를 살펴보겠다.

Paymaster의 경우 표준의 필수 사항은 아니지만 추가가 가능한 부분이다. 만약 추가를 하게 된다면 누구에게나 가스비를 다 대납해줄 수는 없기 때문에 가스비를 얘가 쓸거야 라고 말해주는 검증 로직이 하나 더 필요하다. 그게 바로 vaidatePaymasterUserOp이다.

해당 부분은 페이마스터의 관리자의 서명이 들어가야 하기 때문에 우리가 paymaster에서 자체적으로 구현한 해시를 넣어주어야한다.

여기에서 볼 수 있듯이 내부적으로는 _copyUserOpToMemory를 사용하고 있는데 여기서 payMasterAndData를 보면 주소값을 앞에 20개에서 뽑아내는 것을 볼 수 있다.

다만 paymaster와 상호작용하는 코드를 보면 payMasterAndData 자리에 주소값과 paymaster서명값이 합쳐진 상태로 쓰여지는 것을 볼 수 있다. 따라서 컨트렉트 상에서 접근할때 

payMasterMessageHash.toEthSignedMessageHash().recover(userOp.paymasterAndData[20:])

이런식으로 주소값을 빼고 뒤에 20자리를 불러와서 서명을 가져와야 한다.

  const paymasterAndData = await ethers.utils.solidityPack(["address","bytes"],[paymasterAddress, signature])

따라서 이런식으로 ethers에서 paymasterAndData를 넣어주면 된다. 여기까지 되었다면 이젠 Paymaster 컨트렉트에서 규격에 맞는 응답이 이루어지도록 _validatePaymasterUserOp를 구현해야한다.

페이마스터 검증에 대한 예시는 아래와 같다.

    function _validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) internal virtual override returns (bytes memory context, uint256 validationData) {
		//paymaster검증시 체크할 요소들 

        bytes32 messageHash = keccak256(abi.encodePacked(페이마스터 검증시 체크할 요소들 넣기));

        require(
            messageHash.toEthSignedMessageHash().recover(userOp.paymasterAndData[20:]) == 페이마스터 관리자 키,
            "Paymaster: invalid signature"
        );

        return (context, 0);
    }

먼저 페이마스터에서의 서명은 당연히 페이마스터의 관리자가 해야한다. 페이마스터에서 서명을 하고 검증하는 행위가 관리자가 대신 대납을 해줄 주소를 지정하고 컨트렉트에서 이를 입증하는 것이다. 서명의 내용을 검증하기 위해 컨트렉트 내부에서 userOp에 있는 정보들을 넣어서 체크할 요소를 지정해준 후에 내부적으로 191서명을 진행한다. 이후에 paymasterAndData에서 우리가 앞에는 주소를 뒤에는 서명을 넣어두었기 때문에 주소를 제외한 서명 부분만 빼서 서명과 메세지 해시로부터 주소를 도출해 내어 관리자가 허용한 서명인지를 볼 수 있다.

validationData의 경우  eip공식문서를 봐야 알 수 있는데 필수적으로 validUntil과 validAfter를 명시해야 한다고 써져있다. 근데 0이면 모두 패스가 된다고 한다. (아래 이미지 참고) 이런 부분의 경우 직접 코드를 뜯어보고 나야 제대로 활용이 가능하기 때문에 다시 EntryPoint 소스 코드를 사용해보기로 하겠다.

여기서도 원래는 시간 검사가 있다면 서명앞에 다른 작업이 포함되어야 하지만 아무것도 안넣고 주소부터 시작하면 해당 값은 기본값으로 넘어가게 돈다. 이 부분은 역시 entryPoint 소스코드와 eip 문서를 봐야지 알 수 있는데

EntryPoint에서는 위와 같이 validatePaymasterUserOp를 불러와서 쓰고 있고 응답 값으로 validationData를 받아와서 리턴해 주고 있다. 그리고 그 값은

_getValidationData를 거쳐 _parseValidationData에게 들어가고 응답 값에서 data.validUntil과 data.validAfter, data.aggregator 등을 가져와서 시간은 체크하고 aggergator는 설정 값을 리턴한다.

이 값들을 설정하는것은 Helpers.sol에 있는 _parseValidationData인데 

보면 validationData에서 앞에 부분에서 aggregator의 조소 값을 뽑아내고 vlaidUntil을 뒤에 부분에서 뽑아낸다. 그리고 validAfter값을 뽑아내서 리턴해준다. 따라서 만약 validationData를 0으로 둔다면 시간 검사는 validUntil은 max로 되고 After는 0번 블록 이후는 모두 유효하게 처리되어 주기 때문에 aggregator나 추가 기간에 대한 검증 로직이 없다면 0만 리턴해도 되는 것이다. 만약 기간에 대한 검증로직이나 aggragator가 있다면? 컨트렉트에서 validate함수를 구현할때 인코딩하여 해당 정보를 넣어주면 여기서 파싱이 되어서사용이 될 것이다.

또한 위에 표중 6번을 보면 postOp라는 함수도 설정이 되었다. 해당 부분은 페이마스터 로직에서 마지막에 추가되어 무언가를 하고 싶을때 쓰이는데 대납이 완료되고 이벤트를 넣거나 무언가를 하고 싶다면 여기서 설정해 주면된다. 만약 아무것도 안하고 싶다고 해도 오버라이딩을 해주어야한다. 0.6.0 버전 기준으로는 오버라이딩을 안하면 오류가 나기 때문이다.

만약 여기까지 이해하였다면 Paymaster의 기본 구조를 익힌것이다.

대납은 그럼 어디서 어디로 되는건가?

 paymaster에는 addStake와 addDeposit 등이 있다. addStake는 뭔가 페이마스터의 신뢰를 얻기 위해 넣어둔 것인데 사실상 안쓰인다고 봐도 된다. (뭔가 정확히 명시된 곳은 못찾았지만 슬래싱 로직등이 아직 없고 스테이크만 되어 있음) 반면 addDepoist은 EntryPoint에 페이마스터가 쓸 돈을 넣어 두는 것인데 이 작업을 해야지 대납을 해줄 수 있게 되고 EntryPoint의 balanceOf 함수를 통해서 여기서의 잔고가 부족하면 EntryPoint에서 트랜잭션을 쏴줄 수 없게 된다. 이 또한 EntryPoint의 내부 로직에서 확인이 가능하다.

또한 paymaster에서 토큰 대납이 존재한다. 토큰 대납의 경우 오라클을 사용해서 다른토큰을 유저로부터 받아서 가격을 산정한 후 이를 이더리움 가스비로 바꾸어서 addDeposit을 해주면 되는데 보통 이것보다는 스왑을 써서 구현하면 간단하게 만들 수 있다.

마무리 하며

위의 내용은 계정 추상화를 구현하면서 가장 혼동이 되었던 서명로직 구현및 검증을 다시한번 정리해둔 것이다. 여기서 더 나아가 다른 서명 로직을 구현하면 Eip712 검증, 패스키 검증 , JWT검증, circom등을 활용한 영지식 증명과 혼합한 서명도 사용할 수 있다. 하지만 기초적인 eip191 서명을 먼저 공부해야 다른 구조도 알 수 있게 된다. 추후 Pectra 업데이트에서 eip7702가 채택이 되면 더이상 계정추상화에 대한 공부는 선택이 아니며 필수이다. 따라서 개인적으로는 블록체인 개발자라면 꼭 알고 가야할 기술이라는 생각이 들었다. 아래는 내가 작성한 학습용 예제이며 버전은 0.7.0 이다. 그 외에도 따로 논문 작성에 사용중인 코드도 잇는데 (0.6.0 버전) 해당 버전은 논문이 출간되면 같이 공유하도록 하겠다.

https://github.com/hyunkicho/blockchain101/tree/update2024/accountAbstraction

 

blockchain101/accountAbstraction at update2024 · hyunkicho/blockchain101

체인의정석 : 학습용 핵심 컨트렉트 코드. Contribute to hyunkicho/blockchain101 development by creating an account on GitHub.

github.com

https://it-timehacker.tistory.com/494

 

ERC4337 컨트렉트 핵심 요소 및 커스터마이징 해야할 부분 정리

공식문서 : https://eips.ethereum.org/EIPS/eip-4337 ERC-4337: Account Abstraction Using Alt Mempool An account abstraction proposal which completely avoids consensus-layer protocol changes, instead relying on higher-layer infrastructure. eips.ethereum.o

it-timehacker.tistory.com

https://it-timehacker.tistory.com/497

 

계정 추상화 참고할만한 자료

https://luniverse.io/ko/articles/tech-talk-understanding-aa-account-abstraction-and-erc-4337 Tech Talk: Understanding AA (Account Abstraction) and ERC-4337 Intro At the ETHDenver 2023 event held earlier this month in the United States, a new Ethereum updat

it-timehacker.tistory.com

 

 

728x90
반응형
Comments