블록체인: 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
AI&ML 기초
Reference: https://bettermesol.github.io/ml/2019/09/16/ai-ml-dl/AI: 기계가 사람처럼 생각하고 판단하게 만드는 가장 넓은 범주의 기술입니다.ML: 데이터를 학습하여 스스로 규칙을 찾아내는 AI의 한 분야로,
'AI Agent Economy'Novitas : AI Agent가 지갑을 가지는 세상
얼마 전, 미래에셋증권 리서치 리포트(올해는 이더리움이다: 에이전트 시대의 Near Automata)를 접하게 되었습니다. AI Agent를 인간과 함께할 경제 주체로 바라보는 시각에 적잖이 충격을 받았더랬죠.한 가지 짚고 넘어갈 부분이 있습니다. 우리가 흔히 'AI'
'ERC-8004'Novitas: AI 에이전트 경제 주체
Web 4.0을 한 문장으로 정의하면 Sovereign Transact입니다.AI가 인간의 허락 없이 지갑을 소유하고, 결제를 수행하며, 인프라를 통제하는 주권적 경제 주체가 되는 세계입니다. Web 3.0이 블록체인 기반의 탈중앙화를 실현했다면, Web 4.0은 그