개인키 하나가 당신의 자산 전부를 통제한다
“개인키를 잃으면 자산을 영영 못 찾는다.” 블록체인을 배우다 보면 반드시 듣는 말이다. 하지만 왜 그런지 수학적으로 이해하는 사람은 많지 않다. 단순히 “중요하니까 잘 보관해”가 아니라, 왜 복구가 원천적으로 불가능한지 이해해야 한다. 그래야 개인키 관리의 진짜 무게를 알 수 있다.
개인키에서 주소까지 — 단방향 과정
이더리움 주소가 만들어지는 과정은 세 단계다. 그리고 이 과정은 단방향(one-way)이다.
개인키(256bit 숫자)
↓ ECDSA secp256k1 타원곡선 연산
공개키(64바이트)
↓ keccak256 해시 → 마지막 20바이트
이더리움 주소(0x...)
1단계: 개인키
개인키는 1부터 n-1 사이의 256비트 무작위 정수다(n은 secp256k1 곡선의 차수). 사실상 어떤 256비트 숫자든 유효한 개인키가 된다. 총 가능한 개인키 수는 2^256 — 우주의 원자 수(약 10^80)보다 많다.
# Python으로 개인키 생성
import secrets
private_key = secrets.token_hex(32) # 32바이트 = 256비트 무작위 hex
print(private_key)
# 예: "a1b2c3d4e5f6...8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8"
2단계: 공개키 (타원곡선 연산)
개인키(k)에 secp256k1 곡선의 생성점 G를 타원곡선 곱셈으로 곱한다.
공개키 = k × G
타원곡선 곱셈의 핵심 특성: k × G는 쉽게 계산되지만, 결과값에서 k를 역산하는 것은 수학적으로 불가능하다(이산 로그 문제). 이것이 개인키 보안의 수학적 기반이다.
결과로 나온 공개키는 (x, y) 좌표로 이뤄진 64바이트 숫자다. 앞에 0x04 접두사를 붙여 비압축 형식으로 표현하기도 한다.
3단계: 이더리움 주소
공개키 64바이트를 keccak256으로 해시한 뒤, 마지막 20바이트(40 hex 문자)를 취한다. 여기에 0x를 붙이면 이더리움 주소가 완성된다.
from eth_hash.auto import keccak
# 공개키 64바이트 (0x04 접두사 제외)
address = "0x" + keccak(public_key_bytes).hex()[-40:]
왜 개인키를 잃으면 복구가 불가능한가
이더리움 주소만 알아도 잔액은 누구나 조회할 수 있다. 하지만 출금(트랜잭션 서명)을 하려면 개인키가 반드시 필요하다. 그리고 주소 → 공개키, 공개키 → 개인키 역산은 수학적으로 불가능하다.
중앙화된 복구 서버가 없다. 이더리움 재단도, 어떤 거래소도 당신의 개인키를 대신 만들어줄 수 없다. 이 점이 전통 금융과 블록체인의 가장 큰 차이점이다.
은행은 비밀번호를 잃어도 신원 확인 후 재설정해준다. 블록체인에는 그런 서버가 존재하지 않는다.
트랜잭션 서명 과정
트랜잭션을 보낼 때 개인키가 어떻게 사용되는지 단계별로 보자.
1. 트랜잭션 내용 구성
{nonce, to, value, data, gasLimit, maxFeePerGas, ...}
2. RLP(Recursive Length Prefix) 인코딩
→ 직렬화된 바이트열
3. keccak256 해시 생성
→ 32바이트 해시값
4. 개인키로 ECDSA 서명
→ (v, r, s) 세 값 생성
5. 서명된 트랜잭션을 네트워크에 브로드캐스트
노드가 서명을 검증하는 방법
흥미로운 점은, 노드가 서명을 검증할 때 개인키를 알 필요가 없다는 것이다. ECDSA의 특성을 이용해 (v, r, s)와 트랜잭션 내용만으로 서명자의 공개키를 역산할 수 있다. 그리고 공개키에서 이더리움 주소를 도출해 트랜잭션의 from 필드와 비교한다.
# 서명 검증 (의사 코드)
recovered_public_key = ecrecover(tx_hash, v, r, s)
recovered_address = keccak256(recovered_public_key)[-20:]
assert recovered_address == tx.from_address # 검증 완료
개인키 없이는 올바른 (v, r, s)를 만들 수 없다. 이것이 수학적 보장이다.
시드구문(Seed Phrase)의 원리
하드웨어 지갑이나 MetaMask를 설치하면 12~24개의 영단어(시드구문)를 보여준다. 이것은 개인키를 사람이 읽을 수 있는 형태로 표현한 것이다(BIP-39 표준).
시드구문 12단어
↓ BIP-39 → 512비트 시드
↓ BIP-32 계층적 결정론적(HD) 지갑
개인키_1, 개인키_2, 개인키_3, ... (무한히 생성 가능)
하나의 시드구문에서 수천 개의 개인키를 결정론적으로 생성할 수 있다. 그래서 MetaMask에서 “계정 추가”를 눌러도 시드구문 하나로 모든 계정을 복구할 수 있다.
ethers.js로 실제 구현해보기
import { ethers } from "ethers";
// 새 지갑 생성
const wallet = ethers.Wallet.createRandom();
console.log("주소:", wallet.address);
console.log("개인키:", wallet.privateKey);
console.log("시드구문:", wallet.mnemonic.phrase);
// 개인키로 지갑 복구
const recovered = new ethers.Wallet(wallet.privateKey);
console.log("복구된 주소:", recovered.address); // 동일함
// 트랜잭션 서명
const tx = {
to: "0x...",
value: ethers.parseEther("0.1"),
gasLimit: 21000n,
};
const signedTx = await wallet.signTransaction(tx);
console.log("서명된 트랜잭션:", signedTx);
운용 시스템에서의 개인키 관리
프로덕션 시스템에서 개인키를 어떻게 관리해야 할까? 절대로 코드베이스나 환경변수 파일에 평문으로 저장하면 안 된다. 실수로 GitHub에 올라가는 순간 자산이 탈취된다.
권장하는 방법:
- AWS KMS / GCP Cloud KMS: 개인키를 HSM(Hardware Security Module)에 저장하고, 서명만 요청하는 방식
- HashiCorp Vault: 시크릿 관리 전용 솔루션
- MPC(Multi-Party Computation): 개인키를 여러 조각으로 나눠 분산 보관. 단일 실패 지점 제거
- 하드웨어 지갑: 개인 용도. Ledger, Trezor
기업 환경에서는 MPC 기반 커스터디 솔루션이 표준이 되고 있다. 단일 개인키 방식은 보안 감사를 통과하기 어렵다.
핵심 정리
- 개인키 → 공개키 → 주소: 단방향, 역산 불가
- 트랜잭션 서명 = 개인키로 ECDSA (v, r, s) 생성
- 노드 검증 = 서명에서 주소 역산 후 from 필드와 비교
- 시드구문 = 개인키들을 결정론적으로 생성하는 마스터 키
- 프로덕션에서는 KMS/MPC로 개인키를 절대 직접 노출하지 않는다