블록체인: HD Wallet과 ECC
블록체인에서 지갑은 디지털 자산을 관리하는 소프트웨어이고, 개인키를 소유한 사람이 지갑에 접근할 수 있습니다.HD(Hierarchical Deterministic) Wallet은 트랜잭션이 발생할 때마다 지갑의 주소가 새로 생성되는 지갑입니다. 이름에서 알 수 있듯 지
1. HD Wallet 🎯
블록체인에서 지갑은 디지털 자산을 관리하는 소프트웨어이고, 개인키를 소유한 사람이 지갑에 접근할 수 있습니다.
HD(Hierarchical Deterministic) Wallet은 트랜잭션이 발생할 때마다 지갑의 주소가 새로 생성되는 지갑입니다. 이름에서 알 수 있듯 지갑의 주소가 계층적으로 결정됩니다.
계층적으로 결정된다는 것이 무슨 말일까요? HD Wallet은 어떤 요구에 의해 등장하게 되었을까요?
1-1. HD Wallet 이론 ✅
초기 암호화폐 지갑의 Loose-Key 방식은 개인키를 N개 미리 생성하여 사용했습니다. 문제는 각 개인키를 모두 백업해야 했고, 하나라도 분실하면 자금을 영구적으로 잃는다는 것이었죠. 이를 해결하기 위해 HD Wallet이 등장했습니다. 하나의 루트 시드에서 체인 코드를 이용해 무한개의 키 쌍을 파생(Derivation) 할 수 있게 되었고, 시드 하나만 보관하면 모든 키를 복원할 수 있게 되었습니다.
HD Wallet의 루트 시드는 256비트 난수를 12~24개의 단어로 변환한 니모닉(BIP-39)입니다. 키 파생 방식은 두 가지로 나뉩니다.
Normal Derivation은 부모 확장 공개키로 자식 공개키를 생성할 수 있어 편리하지만, 자식 키 노출 시 부모 개인키 역추적 위험이 있습니다.
Hardened Derivation은 부모 개인키까지 해시에 포함시켜 이 위험을 차단합니다. 실제 지갑은 BIP-32 표준에 따라 상위 레벨에는 Hardened, 하위 레벨에는 Normal Derivation을 혼합 사용합니다.
1-2. HD Wallet 코드 ✅

import hashlib
PBKDF2_ROUNDS = 2048
def mnemonic_to_seed(mnemonic_words: list, passphrase: str = "") -> bytes:
"""
Convert a mnemonic phrase into a seed.
:param mnemonic_words: List of mnemonic words.
:param passphrase: An optional passphrase to enhance security.
:return: A 64-byte seed.
"""
mnemonic_str = " ".join(mnemonic_words).strip()
print(f"mnemonic_str: {mnemonic_str}")
salt = "mnemonic" + passphrase.strip()
print(f"salt: {salt}")
# Generate the seed using PBKDF2-HMAC-SHA512 (password-based key derivation function 2)
seed = hashlib.pbkdf2_hmac(
"sha512", mnemonic_str.encode("utf-8"), salt.encode("utf-8"), PBKDF2_ROUNDS
)
return seed
# Example usage
mnemonic_list = ['bring', 'boil', 'cattle', 'dawn', 'off', 'buyer', 'weird', 'plug', 'summer', 'federal', 'misery', 'ship']
passphrase = ""
seed = mnemonic_to_seed(mnemonic_list, passphrase)
print(f"Generated seed: {seed.hex()} ({len(seed)} bytes)")지갑 복구를 위한 마스터 시드(Seed)를 생성하는 코드입니다.
1. hashlib: SHA-256, SHA-512 등의 보안 알고리즘을 파이썬에서 사용할 수 있도록 제공하는 표준 라이브러리입니다.
2. PBKDF2_ROUNDS: PBKDF2(Password-Based Key Derivation Function 2) 함수가 실행될 반복 횟수를 뜻합니다. BIP-39 표준에서는 2048회로 설정할 것을 제안합니다.
3. mnemonic_str: 니모닉을 공백으로 연결하여 만든, 니모닉 구문 전체를 담고 있는 문자열입니다.
4. salt: PBKDF2 과정에서 니모닉에 추가되어 해시 결과를 무작위화하고 무차별 대입 공격을 방지하는 역할을 하는 선택적 암호입니다. 위 코드에서는 salt 값을 설정하지 않았습니다.
5. seed: 니모닉과 솔트를 이용해 2048회의 PBKDF2를 거쳐 최종적으로 파생된 64바이트 길이의 이진 데이터입니다. HD Wallet의 모든 키를 파생하는 루트 키입니다.

import hmac
def derive_master_key(seed: bytes) -> tuple[bytes, bytes]:
"""
Derive the master key and chain code from the seed.
:param seed: A 64-byte seed.
:return: A tuple containing the master private key and chain code.
"""
key = b"Bitcoin seed"
h = hmac.new(key, seed, hashlib.sha512).digest()
master_key = h[:32]
chain_code = h[32:]
return master_key, chain_code
# Example usage
print(f"Generate master key using seed {seed.hex()} ({len(seed)} bytes)")
master_key, chain_code = derive_master_key(seed)
print(f"Master Key: {master_key.hex()} ({len(master_key)} bytes)")
print(f"Chain Code: {chain_code.hex()} ({len(chain_code)} bytes)")BIP-32 표준에 따라 마스터 시드로부터, 마스터 개인키와 체인 코드를 파생하는 과정을 구현한 코드입니다.
1. hmac: 암호화 메세지 인증 코드(HMAC)을 구현하는 라이브러리입니다. 위에서는 HMAC-SHA512를 사용하여 키 파생을 안전하게 수행합니다.
2. key: HMAC 연산에 사용되는 마스터키 역할을 하는 고정된 문자열입니다. BIP-32 표준에서 정의한 "Bitcoin seed"를 사용합니다.
3. h: key를 HMAC 키로, seed를 메세지로 사용하여 해시한 64바이트(512비트) 출력값입니다.
4. master_key: h의 앞 32바이트(256비트)를 잘라낸 값으로, HD Wallet 구조의 최상위 마스터 개인키 역할을 합니다.
5. chain_code: h의 뒤 32바이트(256비트)를 잘라낸 값으로, 자식 키를 파생할 때 무작위성을 더해주는 추가적인 보안 매개변수입니다.

from ecdsa.util import string_to_number
from ecdsa import SigningKey, VerifyingKey, SECP256k1
import hashlib
import hmac
def derive_child_key(
parent_key: bytes,
parent_chain_code: bytes,
index: int
) -> tuple[bytes, bytes, bytes]:
"""
Derive a child key from a parent key and chain code.
:param parent_key: The parent private key (32 bytes).
:param parent_chain_code: The parent chain code.
:param index: The child index (use hardened index for added security).
:return: A tuple containing the child private key, chain code, and public key.
"""
# Convert index to 4-byte big-endian
hdi = index.to_bytes(4, "big")
print(f"Index (4 bytes): {hdi.hex()}")
if index & 0x80000000: # Hardened derivation
# Hardened: Use private key
data = b"\x00" + parent_key + hdi
else: # Normal derivation
parent_pub_key = derive_public_key_from_private_key(parent_key)
data = parent_pub_key + hdi
print(f"HMAC input data: {data.hex()}")
# Perform HMAC-SHA512
h = hmac.new(parent_chain_code, data, hashlib.sha512).digest()
child_tweak = string_to_number(h[:32]) # First 32 bytes
child_chain_code = h[32:] # Last 32 bytes
print(f"Child tweak: {h[:32].hex()}, Child chain code: {child_chain_code.hex()}")
# SECP256k1's curve order
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
# Compute child private key
parent_int = string_to_number(parent_key)
child_private_key = (parent_int + child_tweak) % n
if child_private_key == 0:
raise ValueError("Derived child private key is invalid (zero).")
child_private_key_bytes = child_private_key.to_bytes(32, "big")
print(f"Derived child private key: {child_private_key_bytes.hex()}")
# Compute child public key from private key
child_pub_key = derive_public_key_from_private_key(child_private_key_bytes)
return child_private_key_bytes, child_chain_code, child_pub_key
def derive_public_key_from_private_key(private_key: bytes) -> bytes:
"""
Derive the public key from the private key.
:param private_key: The private key (32 bytes).
:return: The compressed public key (33 bytes).
"""
sk = SigningKey.from_string(private_key, curve=SECP256k1)
vk = sk.verifying_key
x = vk.pubkey.point.x()
prefix = b"\x02" if vk.pubkey.point.y() % 2 == 0 else b"\x03"
return prefix + x.to_bytes(32, "big")BIP-32 표준에 따라 Hardened와 Normal 파생을 구분하여 자식 키를 파생하는 과정을 구현한 코드입니다.
1. derive_child_key: 부모의 확장 개인키와 인덱스를 사용하여 자식의 확장 개인키를 파생하는 BIP-32의 핵심 함수입니다.
2. derive_public_key_from_private_key: 주어진 개인키로부터 SECP256k1 곡선 연산을 통해 해당 개인키에 대응하는 33바이트 압축 공개키를 계산하는 헬퍼 함수입니다.
3. hdi: 인덱스($i$)를 4바이트 크기의 빅 엔디안 형식 바이트 배열로 변환한 값으로, HMAC 입력 데이터의 일부로 사용됩니다.
4. index & 0x80000000: 인덱스의 최상위 비트(MSB)를 확인하여 Hardened Derivation($i'$)을 수행할지 Normal Derivation($i$)을 수행할지 결정하는 조건문입니다.
5. data: HMAC-SHA512 연산에 사용되는 입력 데이터로, Hardened 파생 시 개인키를, Normal 파생 시 공개키를 포함합니다.
6. child_tweak: HMAC 결과(h)의 앞 32바이트를 정수(Integer)로 변환한 값으로, 자식 개인키를 계산할 때 부모 개인키에 더해지는 조정 값입니다.
7. n: 비트코인 등이 사용하는 SECP256k1 타원 곡선의 순서(Order)를 나타내는 상수값으로, 개인키 계산 시 모듈러 연산의 기준이 됩니다.
8. child_private_key: 부모 개인키와 child_tweak을 합한 후 곡선 순서로 나눈 나머지인, 최종 자식 개인키입니다.

def derive_from_path(
master_key: bytes,
chain_code: bytes,
path: str
) -> tuple[bytes, bytes, bytes]:
"""
Derive a key from a given derivation path.
:param master_key: The master private key.
:param chain_code: The master chain code.
:param path: The derivation path (e.g., "m/44'/60'/0'/0/0").
:return: The derived private key, chain code, and public key.
"""
levels = path.split("/")[1:] # Skip "m"
key, code = master_key, chain_code
for level in levels:
if "'" in level: # Hardened key
index = 0x80000000 | int(level[:-1])
else: # Normal key
index = int(level)
print(f"Processing index: {index} (Hardened: {'Yes' if index & 0x80000000 else 'No'})")
key, code, pub_key = derive_child_key(key, code, index)
# Log the current state
print(f"Derived private key: {key.hex()}")
print(f"Derived chain code: {code.hex()}")
print(f"Derived public key: {pub_key.hex()}")
return key, code, pub_key
# Example usage
path = "m/44'/60'/0'/0/0" # Compare with Metamask
derived_key, derived_chain_code, derived_pub_key = derive_from_path(master_key, chain_code, path)
print(f"Derived Private Key: {derived_key.hex() if derived_key else None}")
print(f"Derived Chain Code: {derived_chain_code.hex()}")
print(f"Derived Public Key: {derived_pub_key.hex()}")HD Wallet의 마스터키($m$)에서 BIP-44 경로($m / purpose' / coin\_type' / account' / change / address\_index$)를 따라 최종 주소의 키를 파생하는 코드입니다.
1. derive_from_path: BIP-44 표준 파생 경로 문자열을 해석하여 마스터키에서 최종 주소의 키까지 계층적으로 순차 파생하는 함수입니다.
2. levels: 입력된 경로 문자열(path)에서 최상위 m을 제외하고 /로 구분된 파생 인덱스 문자열 리스트입니다.
3. level: levels 리스트를 순회하며 처리하는 각 계층의 인덱스 문자열입니다.
4. if "'" in level: 현재 level 문자열에 프라임 기호(')가 포함되어 있는지 확인하여 Hardened Derivation 대상인지 판단하는 조건문입니다.
5. index = 0x80000000 | int(level[:-1]): Hardened 인덱스(44')의 프라임 기호를 제거한 후, $0x80000000$을 비트 OR 연산하여 최상위 비트를 1로 설정해 Hardened 파생을 명시합니다.
6. index = int(level): 프라임 기호가 없는 Normal 인덱스(0)를 단순히 정수형으로 변환합니다.
7. key, code, pub_key = derive_child_key(...): 현재 파생된 확장 키(key, code)를 다음 루프의 부모 키로 사용하여, 경로의 다음 단계로 순차적으로 진행합니다.
2. ECC 🎯
Elliptic Curve Cryptography는 타원 곡선 암호입니다. ECC를 통해 공개키 암호화 과정을 살펴보겠습니다.
2-1. 개인키, 공개키 이론 ✅
개인키 d는 랜덤하게 생성된 바이트일 뿐입니다. d는 1 이상 N 미만의 범위를 갖는데요, 여기서 N은 타원 곡선 군의 위수를 의미합니다. 즉, 개인키는 특별한 구조가 없는 단순한 난수입니다.
공개키 Q는 개인키 d에 타원 곡선의 생성점 G를 곱한 결과입니다. 즉, Q = dG이죠. Q는 (x, y)의 형태로 도출됩니다. 공개키라는 것은 타원 곡선 위의 점이라고 이해할 수 있겠습니다.
Q가 15이고 G가 3이라면, d는 5라는 것을 쉽게 알 수 있습니다. 이거 뭐 개인키 오픈하는 거 아니냐?라는 의심이 들 수 있는데요, 타원 곡선 위에서의 곱셈 연산은 일반적인 곱셈 연산이 아니라 Scalar Multiplication이라는 점 덧셈입니다. 역산이 매우 어렵기 때문에 타원 곡선 암호의 보안성을 보장할 수 있습니다.
2-2. 개인키, 공개키 코드 ✅

func TestPrivateKey(t *testing.T) {
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
// Parameters of the secp256k1 curve which is used in Ethereum
curveParams := privateKey.Params()
t.Logf("P: %x\n", curveParams.P)
t.Logf("N: %x\n", curveParams.N)
t.Logf("B: %x\n", curveParams.B)
t.Logf("Gx: %x\n", curveParams.Gx)
t.Logf("Gy: %x\n", curveParams.Gy)
t.Logf("BitSize: %v\n", curveParams.BitSize)
t.Logf("Name: %v\n", curveParams.Name)
// The private key is just a random number
t.Logf("D: %x\n", privateKey.D)
// The public key is a point on the curve calculated by (Gx, Gy) * D
t.Logf("X: %x\n", privateKey.PublicKey.X)
t.Logf("Y: %x\n", privateKey.PublicKey.Y)
// Uncomprseed public key = 04 + X + Y
t.Logf("Uncompressed Public Key: %x\n", crypto.FromECDSAPub(&privateKey.PublicKey))
// Compressed public key = 02 or 03 + X
t.Logf("Compressed Public Key: %x\n", crypto.CompressPubkey(&privateKey.PublicKey))
// Sign a message
message := []byte("Hello, world!")
messageHash := crypto.Keccak256Hash(message)
signature, err := crypto.Sign(messageHash[:], privateKey)
require.NoError(t, err)
t.Logf("Signature: %x\n", signature)
// Verify the signature
crypto.VerifySignature(crypto.FromECDSAPub(&privateKey.PublicKey), messageHash[:], signature)
}위 함수는 이더리움에서 사용되는 secp256k1 곡선을 기반으로 키 쌍을 생성하고, 서명과 검증을 수행하는 과정을 보여줍니다.
1. crypto.GenerateKey(): 이더리움의 crypto 패키지를 사용하여 secp256k1 곡선에 유효한 새로운 개인키를 무작위로 생성합니다.
2. curveParams.P: 타원 곡선의 정의 방정식인 $y^2 = x^3 + ax + b \pmod{p}$에서 사용되는 소수 필드의 크기(p)입니다.
3. 4. 5. 6. 7. 8. 9. 10. 11. 안전하게 관리되는 니모닉(BIP-39)은 PBKDF2-HMAC-SHA512 알고리즘을 통해 64바이트의 마스터 시드(Seed)로 변환되며, 이 시드가 곧 HD Wallet (Hierarchical Deterministic Wallet) 구조의 출발점이 됩니다. HD Wallet은 초기 Loose-Key 방식의 비효율적인 다중 백업 문제를 해결하고, 트랜잭션마다 새로운 주소를 생성하여 프라이버시를 향상시키기 위해 등장했습니다. HD Wallet은 BIP-32 표준에 따라 하나의 마스터 시드에서 무한한 키 쌍을 계층적(부모-자식 관계)이고 결정론적(항상 동일하게 재현 가능)으로 파생시킵니다. 키를 파생하는 과정은 HMAC-SHA512를 기반으로 하며, 보안 수준에 따라 두 가지 방식으로 나뉩니다. Hardened Derivation (강화 파생): 부모 개인키를 사용하여 보안성이 높으며, 경로의 상위 계층(예: 목적, 코인 타입, 계정)에 사용됩니다. Normal Derivation (일반 파생): 부모 공개키만으로 자식 공개키를 파생시킬 수 있어 편리하며, 경로의 하위 계층(예: 주소 인덱스)에 사용됩니다. 이 모든 키 파생의 근간에는 타원 곡선 암호(ECC, Elliptic Curve Cryptography)가 있습니다. ECC는 개인키($d$)와 생성점($G$)의 스칼라 곱셈 연산($Q = d \cdot G$)을 통해 공개키($Q$)를 생성하며, 이 과정의 역산인 타원 곡선 이산 로그 문제(ECDLP)의 어려움을 이용하여 지갑의 보안성을 최종적으로 보장합니다. 추가적으로 Metamask 지갑을 설치하고 니모닉을 생성하는 등의 실습을 진행해 봤습니다. 단순 UI 조작이기에, 이 부분은 직접 해보시면 좋을 것 같습니다.curveParams.N: 타원 곡선 군의 위수(Order)로, 개인키(d)가 가질 수 있는 최대 범위(1≤dcurveParams.Gx, curveParams.Gy: 타원 곡선 군의 생성점(G)의 X 좌표와 Y 좌표를 나타냅니다.privateKey.D: 생성된 개인키(d)의 실제 값으로, 타원 곡선 위수(N) 미만의 무작위 정수입니다.privateKey.PublicKey.X, privateKey.PublicKey.Y: 개인키(d)와 생성점(G)의 스칼라 곱(Q=d⋅G)으로 계산된 공개키(Q)의 X 좌표와 Y 좌표입니다.crypto.FromECDSAPub(...): 공개키 $Q(x, y)$를 접두사 04를 포함하는 비압축 형식(04∥X∥Y)의 바이트로 직렬화합니다.crypto.CompressPubkey(...): 공개키 $Q(x, y)$를 $02$ 또는 $03$ 접두사를 포함하는 압축 형식($[02/03] \| X$)의 바이트로 직렬화합니다.crypto.Keccak256Hash(message): 서명 전에 메시지 원본을 이더리움 표준 해시 함수인 Keccak-256으로 해시하여 메시지 다이제스트를 생성합니다.crypto.Sign(...): 해시된 메시지와 개인키를 사용하여 ECDSA(타원 곡선 디지털 서명 알고리즘) 기반의 서명(r,s,v)을 생성합니다.crypto.VerifySignature(...): 공개키, 메시지 해시, 그리고 서명을 사용하여 해당 서명이 이 개인키에 의해 생성되었는지 검증하는 과정입니다.3. 정리 🎯
More to read
Amazon VPC Architecture 이해하기
새로운 프로젝트를 기획하며, 개발에서 무엇을 가장 먼저 고민해야 하는지 다시 돌아보게 되었습니다.한때는 프론트엔드가 모든 설계의 출발점이라고 믿었습니다. 유저가 무엇을 보고, 어떤 흐름에서 머무르고 이탈하는지에 대한 이해 없이 서비스를 만든다는 건 불가능하다고 생각했기
'원사이트'프론트엔드 관점으로 알고리즘 이해하기
오랜만에 방법론에 관한 글을 쓰게 되었습니다. 최근 상황은 이렇습니다. SSAFY에서는 하루에 엄청난 양의 알고리즘 문제들을 과제로 수행하게 됩니다. 그 과정에서, '구현력'이 매우 떨어진다는 생각이 들었습니다. 완전히 어려운 문제라면 '아쉬움'이라는 감정조차 느끼지
SubnetVPC 설계의 시작: IP와 Subnet
반복되는 루틴 속에서 얻은 안정감을 발판 삼아, 이제는 기술적 스펙트럼을 넓히기 위한 개인 프로젝트에 착수하고자 합니다.이번 프로젝트의 목표는 단순한 포트폴리오 구축을 넘어, 실제 서비스 수준의 블로그 시스템 구현과 다국어 처리 적용 등 실무에 가까운 역량을 한 단계