[계정 추상화 시리즈] part1 계정 추상화는 어떻게 만들어질까?

molly·2023년 3월 9일
0

계정추상화

목록 보기
2/2
post-thumbnail

지금의 EOA 시스템은 오프 체인에서 개인키가 만들어지고 관리가 되고 언제든지 EOA의 개인키로 서명을 하면 자산 전송이 가능하다. 자산이 EOA에 포함되어야 한다는 것이 당연한 것처럼 흘러가고 있다.

지금 억대의 자산을 보유하고 있는 많은 어드레스가 개인키를 잃어버린 채 코인계를 떠돌아다니고 있다. 이는 아마 개인키를 잃어버렸다면 평생 찾을 수 없을 것이다.

만약 자산을 EOA가 아닌 CA가 보관하게 된다면?이라는 답을 계정 추상화에서 찾아보자

아래는 지갑 컨트랙트이다. 유저가 요청한 기능을 수행할 수 있도록 해주는 함수 하나와 그 기능을 나타내는 데이터(user operation)를 구현하고 있다.

contract Wallet {
  function executeOp(UserOperation op);
}
struct UserOperation {
  address to;
  bytes data;
  uint256 value; // Amount of wei sent
  uint256 gas;
	bytes signature;
  uint256 nonce;
}

UserOperation 의 내용은 유저가 요청한 기능을 수행할 수 있는 eth_sendTransaction에 전달되어야 하는 모든 파라미터들이 기록되어야 한다.

추가로 데이터를 서명한 시그니처와 리플레이 공격 방지를 위한 넌스값이 필요하다.

위의 구현체로 구현된 컨트랙트 지갑은 전자서명이 없으면 절대로 다른 사람에겐 전송될 수 없을 것이다.

그렇다면 executeOp(op) 는 누가 호출할까?

컨트랙트의 함수는 EOA가 실행을 해야한다. 다시 말해 트랜잭션을 실행할 수 있는 어드레스는 EOA뿐이다. EOA는 이더를 가스비로 사용하여 executeOp(op) 를 실행 해야 한다.

하지만 위의 방법은 EOA를 만들어야 하고 이더리움이 필요하다. 그렇다면 유저 입장에서는 굳이 CA지갑이 필요 없다고 생각할 수도 있을 것 같다. 보안상의 강점을 가지고 있다고 해도 굳이 두 지갑을 관리하기엔 번거로운 일이긴 하다.

만약 내가 가스비를 이더로 지불해야 하는 것은 맞지만 EOA를 만들기 싫다면 어떻게 해야 할까?

executeOp(op) 의 OP에는 이미 사인 된 시그니처가 들어가 있음으로 누구나 executeOp(op) 을 실행하는 것에는 제약이 없을 것이다. 그럼 누군가가 대신 executeOp(op) 를 실행할 수 있다는 것이다.

대신 실행해 주는 사람을 번들러라고 부르는 데 지금은 번들을 제출하는 것이 아니므로 그냥 실행자라고 정의하고 번들에 대해선 다음 포스팅에서 알아보자.

실행자가 공짜로 다른 사람의 트랜잭션을 대신 실행해 주는 경우는 거의 없다고 생각한다. 그렇다면 이더를 실행자에게 보내야 하는데 지갑 컨트랙트에게 일정한 이더를 넣어 놓고 실행자가 실행한 후 지갑 컨트랙트에서 실행자에게 보내는 방식을 생각할 수 있을 것 같다.

그렇다면 여기서 executeOp(op) 함수가 수정이 되어야 할 것이다.

위의 방법은 서로 간의 신뢰를 바탕으로 한다면 잘 동작이 될 것이다. 하지만 잘 모르는 사람들의 컨트랙트를 어떻게 믿을 수 있을까? 그렇기에 장치들을 마련해야 한다.

그 장치로 아래와 같은 새로운 스마트 컨트랙트를 만들 수 있다.

바로 신뢰 컨트랙트(보안감사 및 오픈소스)를 도입하는 것인데 EntryPoint 컨트랙트이고 다음과 같은 메서드를 실행자가 실행할 수 있게 한다.

contract EntryPoint {
  function handleOp(UserOperation op);

  // ...
}

handleOp(op) 의 역할은 다음과 같다.

  • 컨트랙트 지갑에 가스비를 지급할 여력이 있는 지를 확인(user op에 있는 gas 확인)
  • 지갑의 executeOp(op) 메서드를 호출하고 실제 사용되는 가스를 추적한 후 실행자에게 가스비에 맞는 이더를 전송

그렇다면 이더를 EntryPoint 컨트랙트에서 가지고 있어야 한다. 즉, EntryPoint 컨트랙트에 송출금 기능이 추가가 되어야 한다.

contract EntryPoint {
  function handleOp(UserOperation op);
	function deposit(address wallet) payable;
  function withdrawTo(address payable destination);
}

여기서 지갑이 직접 가스비를 내야 하는 구조로는 만들 수 없냐라고 생각할 수 있다. 할 수는 있지만 대납 기능 및 이후의 ERC-4337을 만족하긴 위해선 송출금 기능이 EntryPoint 컨트랙트에 필요하다고 한다. 그 이유는 아래서 살펴보자.

다시 컨트랙트 지갑으로 돌아가 보자

현재 지갑은 아래와 같은 구조이다.

contract Wallet {
  function executeOp(UserOperation op);
}

executeOp(op) 메서드는 두 가지 기능을 수행한다. 사용자 요청의 유효성을 검사하고 그 요청을 실행하는 일 두 가지이다.

두 가지를 분류한 이유는 사용자가 가스비를 내는 것이 아닌 실행자에게 제출을 부탁하기 때문이다.

생각을 해보면 executeOp(op) 함수를 실행하기 위해선 가스비가 들지만 유효성 검사에서 실패를 해도 가스비를 실행자는 내게 된다. 만약 악의적인 누군가 이러한 요청을 한 번에 계속 보내게 된다면 실행자의 이더는 바닥이 날것이다.

반면, 유효성 검사를 통과해도 트랜잭션이 실패한 경우에도 가스비가 나가게 된다.

위의 두 가지 경우를 해결하기 위해 지갑 컨트랙트는 두 가지의 함수로 쪼개지게 된다.

contract Wallet {
  function validateOp(UserOperation op);
  function executeOp(UserOperation op);
}

위와 같이 구현한다면 EntryPoint컨트랙트에서 validateOp과 executeOp 따로 호출하고 소모되는 가스비를 추적하여 이더를 미리 확보할 수 있게 된다.

하지만 실행자는 validateOp 실패 시 가스비를 보존 받을 수 없게 된다.

그러기 위해서 validateOp 는 아래와 같은 표준을 지켜야 하고 실행자는 아래의 조건을 만족하지 않으면 아예 user op를 거절할 것이다.

  • 금지 목록에 포함되는 opcode(TIMESTAMP, BLOCKHASH) 를 사용하지 않는다.
  • 접근할 수 있는 스토리지는 지갑 컨트렉트와 관련된 스토리지(associated storage)이고, 다음과 같이 정한다.
    • 지갑 컨트랙트의 스토리지
    • 다른 컨트랙트 스토리지 중에서 mapping(address => value)과 같이 지갑 주소에 해당하는 슬롯(역주: 다른 컨트랙트에서 지갑 주소를 mapping 타입의 키로 하는 스토리지가 있을 때 그 스토리지에 접근은 허용된다는 의미)
    • 지갑 주소와 동일한 스토리지 슬롯에 있는 다른 컨트랙트의 스토리지(솔리디티에서는 나타나지 않는, 일반적이지 않은 스토리지 구조).

지갑에서 직접 가스비를 내고 싶다면 validateOp 을 실패하게 하면 된다.

contract Wallet {
  function validateOp(UserOperation op, uint256 requiredPayment);
  function executeOp(UserOperation op);
}

앞에 EntryPoint컨트랙트에서 입출금은 구현이 되어야 된다 하였다.

만약 가스비를 제출하고 남는 이더가 생겼다면 해당 이더는 컨트랙트 지갑으로 환불이 되어야 한다. 하지만 환불은 재진입 공격, 예상치 못한 변수 등의 이유로 실패하게 될 수도 있기에 입출금 시스템을 구축하여 직접 찾아갈 수 있게 하는 것이다.

그러면 실행자는 어떤 보상을 받고 가스비를 내게 되는 것일까?

실행자에게 보상을 주기 위해 maxPriorityFeePerGas를 설정하여 제출할 수 있다.

흡사 블록에 트랜잭션을 검증하고 제출하는 밸리데이터와 비슷해 보인다.

struct UserOperation {
  // ...
  uint256 maxPriorityFeePerGas;
}

실행자는 maxPriorityFeePerGas를 확인하여 자신에게 유리한 요청을 먼저 처리함으로써 이득을 취할 수 있을 것이다.

지갑과 실행자는 그렇다면 어떤 Entrypoint컨트랙트와 커넥이 되어야 할까?

ERC-4337에서는 모든 지갑과 실행자가 동일한 EntryPoint에 연결될 것이라고 한다

즉 EntryPoint가 전체 시스템에서 싱글톤이 될 것이라고 한다.

그렇다며 EntryPoint 컨트랙트는 hedleop에 오는 요청을 어느 지갑으로 유효성 검사와 실행을 해야 하는지에 대한 데이터가 있어야 한다. 따라서 아래와 같이 useroperation에서 항목을 추가한다.

struct UserOperation {
  // ...
  address sender;
}

다음 포스팅에서 번들러에 대해 포스팅하겠다.

본 글은 알케미 블로그를 참고하였습니다.
계정 추상화가 워낙 복잡하고 어려운 개념이다 보니 제가 적은 내용 중 잘못된 내용이 있을 수 있습니다. 해당 내용은 알려주시면 신속하게 정정하겠습니다.

https://www.alchemy.com/blog/account-abstraction

profile
BlockChain R&D

0개의 댓글