Skip to content
Go back

검증 코드 한 줄의 값어치 — 엔트로피 0과 EIP-7702 스윕

Wallet SDK의 createWallet에는 니모닉을 만들 때 엔트로피를 검증하는 코드가 한 줄 들어있다. 처음 볼 땐 “이게 실제로 걸릴 일이 있나” 싶었다. 정상적인 난수 생성기라면 절대 안 걸릴 조건이기 때문이다. 그런데 이 코드가 막으려는 게 뭔지 실감하려면, 걸렸을 때 무슨 일이 벌어지는지를 봐야 했다. 그래서 직접 재현해봤다.

무엇을 했나

엔트로피가 전부 0이면

검증 코드는 이렇게 생겼다.

const entropy = mnemonicToEntropy(mnemonic, wordlist);
if (entropy.every((b) => b === 0) || entropy.every((b) => b === entropy[0])) {
  throw new KeyringError("Entropy generation failed: insufficient randomness");
}

엔트로피는 니모닉을 만드는 원재료가 되는 난수 16바이트(128비트)다. 이게 전부 0x00이거나 전부 같은 값이면 생성을 거부한다.

전부 0인 엔트로피로 니모닉을 만들면 이렇게 나온다.

abandon abandon abandon abandon abandon abandon
abandon abandon abandon abandon abandon about

BIP-39 스펙의 테스트 벡터에 그대로 박제된, 세상에서 가장 유명한 니모닉이다. 흥미로운 건 마지막 단어만 다르다는 점이었다.

이유는 체크섬이다. 궁금해서 더 파보니 이렇다. BIP-39는 128비트 엔트로피에 체크섬 4비트를 붙여 132비트를 만들고, 이걸 11비트씩 12조각으로 잘라 각 조각을 2048개 단어의 인덱스로 쓴다. 엔트로피가 전부 0이면 앞 11조각은 모두 인덱스 0 = abandon이다. 마지막 조각만 엔트로피 7비트 뒤에 체크섬 4비트가 붙는데, 체크섬은 SHA-256(엔트로피)의 앞 4비트라서 0을 해시해도 0이 아니다. 그 값이 인덱스 3 = about이 된다.

즉 마지막 단어가 어긋나는 건 오타 감지용 체크섬의 부산물이었다.

공용 지갑에 돈을 보내봤다

이 니모닉을 복구하면 첫 계정으로 0x9858…Eda94가 뜬다. 니모닉이 공개돼 있으니 전 세계 누구나 이 키를 안다. MetaMask에 임포트하는 것 자체는 아무 문제 없이 됐다. 체크섬이 유효한 정상 니모닉이기 때문이다.

Sepolia로 0.1 ETH를 보냈다. 그리고 잔액을 조회했다.

curl -s -X POST <SEPOLIA-RPC> \
  -d '{"method":"eth_getBalance","params":["0x9858...","latest"],...}'
# → {"result":"0x0"}

잔액이 정확히 0이었다. 보낸 0.1 ETH가 이미 사라진 것이다. 그런데 이더스캔(이더리움 블록 탐색기)의 Transactions 탭에는 내가 보낸 입금(IN)만 있고, 나간 기록(OUT)이 없었다. 돈은 없는데 출금 전표가 없는 상황이었다.

출금 기록이 없는 이유 — EIP-7702

계정 상태를 더 파봤다. eth_getCode를 날렸더니 일반 지갑이라면 비어 있어야 할 값(0x) 대신 이게 나왔다.

# eth_getCode(0x9858...)
# → 0xef0100 386fd05a0e6a8c51f2abf3d6af4a86b1e2f1972a

0xef0100은 EIP-7702 위임 표식이다. 2025년 Pectra 업그레이드로 생긴 기능으로, EOA(일반 지갑)에 “이 컨트랙트의 코드를 내 것처럼 실행하라”는 위임을 붙일 수 있게 됐다. 뒤의 주소가 위임받은 컨트랙트다.

정리하면 이렇다. 봇은 공개된 키로 미리 이 지갑에 “덫 컨트랙트로 위임”을 설치해 뒀다. 위임 서명을 만드는 데 필요한 건 프라이빗 키뿐인데, 그 키가 공개돼 있으니까. 이후 내가 입금하자, 봇은 이 주소를 목적지로 트랜잭션을 하나 보내 위임된 코드를 실행시켰고, 그 코드가 잔액을 봇에게 이체했다.

돈이 나간 사건은 내가 서명한 것도, 이 지갑이 서명한 것도 아니라 봇의 트랜잭션이 실행되는 도중에 일어난 내부 이동이다. 그래서 일반 Transactions 탭이 아니라 Internal Transactions 탭에만 기록된다. 확인해보니 내 입금과 같은 블록에서 0.1 ETH가 봇 주소로 빠져나가 있었다. 멤풀에서 입금을 감지한 봇이 확정되기도 전에 같은 블록에 회수를 끼워 넣은 것이다.

같은 블록에 끼워 넣는 법 — MEV 번들

“어떻게 확정되기도 전에 같은 블록에 넣나” 싶었는데, 여기가 MEV의 영역이다. 멤풀은 비밀 창구가 아니라 누구나 실시간으로 들여다보는 공개 대기 줄이다. 봇은 자체 노드로 멤풀을 24시간 구독하다가, 유출 지갑으로 입금이 들어오는 걸 확정 전에 감지한다.

문제는 감지 후 “먼저 보내기”만으로는 순서가 보장되지 않는다는 점이다. 회수 트랜잭션도 결국 대기 줄에 서는 것이라, 자칫 입금보다 먼저 실행되면 쓸어담을 잔액이 없어 실패하고 가스비만 날린다. 그래서 요즘 봇은 MEV 번들을 쓴다.

MEV 번들은 트랜잭션을 멤풀에 던지는 대신, 블록을 만드는 주체(block builder — 노드로 블록을 고르는 밸리데이터와 분리된 전문 조립자)에게 “이 트랜잭션들을 이 순서로 묶어서, 전부 성공할 때만 블록에 넣어라”라는 조건과 팁을 붙여 직접 건네는 방식이다. 핵심은 두 가지다. 순서를 확률이 아니라 확정으로 지정하고(“입금 다음에 회수”), 실패할 상황이면 번들째 폐기되어 가스비가 0이다. block builder는 팁만큼 블록 수익이 늘어나니 받아준다. Flashbots가 이 중개를 담당한다.

그래서 입금과 회수가 같은 블록에, 정확한 순서로 찍힐 수 있었다. 사람이 반응해서 이기는 경기가 애초에 아니었다.

회고

검증 코드가 걸리는 조건 — 엔트로피가 전부 0 — 이 정상 난수에서 나올 확률은 2⁻¹²⁸다. 사실상 절대 안 나온다. 그런데 이 값이 나온다면 그건 우연이 아니라 난수 생성기가 고장 났다는 신호다. RNG가 망가진 채로 지갑을 만들면 사용자는 정상적으로 지갑이 생성됐다고 믿고 돈을 넣을 텐데, 실제로는 방금 그 “공용 지갑”을 만든 것이다. 그리고 그 돈은 입금하는 순간 증발한다.

검증 코드 한 줄은 정상일 땐 아무것도 안 하지만, 비정상일 때 사용자의 전 재산을 지킨다. 걸릴 일이 없어 보이는 방어 코드일수록, 걸렸을 때의 대가가 크다.

공개된 키의 지갑은 회수 불가능한 블랙홀이라는 것도 덤으로 배웠다. 위임을 내 걸로 덮어써도, 가스비를 넣는 순간 그것마저 털려서 회수 트랜잭션을 실행할 연료조차 남지 않는다. 테스트넷 0.1 ETH로 배우기엔 값싼 수업이었다.


Share this post on:

Comments


Previous Post
완전한 키가 존재하는 순간을 없애기 — MPC-TSS
Next Post
Wallet SDK 고도화 #5 — M-of-N 오프체인 멀티시그