EthereumNode.js

개인키로 지갑 주소까지 만들어보자(feat. Node.js, ethereum)

February 25, 2025

배경

블록체인 생태계에 참여하려면 본인 소유의 지갑은 필수다. 현재 각 체인에서 사용되는 지갑 서비스는 여러 개가 존재한다. 덕분에 복잡한 연산 필요없이 버튼 몇 번 만 누르면 지갑을 생성할 수 있다.

개발자로서는 여기에 만족할 수가 없었다. 지갑 서비스의 뒷면에 어떤 메커니즘이 작동하는 지 호기심이 생겼다.

비대칭키

암호화 종류를 크게 두 가지로 나눌 수 있는데, 하나는 대칭키 암호화고, 다른 하나는 비대칭키 암호화다. 대부분의 블록체인 시스템에서는 비대칭키를 사용한다.

비대칭키를 간단히 설명하자면, 암호화와 복호화에 사용하는 키가 서로 다른 암호화 방식을 일컫는다. 보통 공개키(Public Key)가 사람들에게 공개되고, 개인키(Private Key)가 숨겨져서 프로토콜에서 사용된다.

블록체인에서는 개인키를 통해서 거래 내역을 서명한다. 그리고 각자 소유한 개인키를 통해 만드는 공개키는 지갑의 주소가 된다. 보통 자산을 전송받는 대상은 지갑 주소로 명시된다.

EVM 계열 지갑 주소

지갑 서비스를 이용해 본 사람들은 개인키만 있으면 지갑이 복구 가능하다는 것을 알고 있을 것이다. 그래서 개인키는 함부로 타인에게 공유되어서는 안된다는 경고문구도 본 경험이 있을 것이다. 개인키는 사실 32 bytes 의 랜덤한 데이터를 뽑아서 사용하기만 하면 된다. (1 byte 는 8 bits 다. 1 byte 는 16진수 표기법으로는 두 글자로 표현된다.) 256 bits 로 표현되는 데이터면 뭐든 가능하다.

공개키는 개인키를 통해 이끌어낼 수 있다. 보통 다른 지갑 주소 관련한 글들을 보면 이즈음해서 타원곡선함수를 설명한다. 타원곡선함수에 대해서는 하단에 정리해놓은 레퍼런스를 참고해서 더 자세히 알아보길 바란다. 여기서는 타원곡선암호화에 이용하는 알고리즘 secp256k1에 개인키를 인수로 넣어 공개키를 만들어낸다는 것만 설명하도록 하겠다.

이렇게 이끌어낸 공개키를 이더리움에서는 다시 한 번 keccak256이라는 해시함수의 인수로 넣는다. 결과적으로 32 bytes의 데이터를 얻을 수 있는데, 뒤쪽에서부터 20 bytes에 해당하는 값만 이용해서 지갑주소로 사용한다.

정리하자면 다음과 같다.

  1. 개인키 32 bytes를 랜덤하게 뽑아낸다.
  2. 타원곡선암호화 알고리즘 secp256k1에 개인키를 넣어 공개키를 만든다.
  3. 공개키를 keccak256 해시함수에 넣은 결과값 중, 뒤에서부터 20 bytes의 값을 이용해 주소로 이용한다.

앞에서 본인이 공개키가 지갑주소인 것처럼 언급했는데, 엄밀하게는 바로 위에 설명된 것 같이 구분될 수 있다.

구현

구현된 코드로 만든 지갑주소의 검증은 메타마스크를 통해 하려고 한다. 메타마스크에 동일한 개인키를 넣었을 시, 똑같은 지갑 주소가 생성되면 검증이 완료된다.

import { randomBytes, createECDH } from "crypto";
import { keccak256 } from "@ethersproject/keccak256";

// Generate Random Private Key
const privateKey = randomBytes(32);
console.log(`Private Key: ${privateKey.toString('hex')}`)

// Generate Public Key
const ecdh = createECDH('secp256k1');
ecdh.setPrivateKey(privateKey);
const publicKey = ecdh.getPublicKey().subarray(1); // remove prefix '0x'
console.log(`public length: ${publicKey.byteLength}`); // 64
console.log(publicKey);

// Generate Ethereum Address
const addressLong = keccak256(publicKey);
console.log(addressLong);
const address = "0x" + addressLong.slice(-40);
console.log(`Address: ${address}`)

실제 지갑주소를 이끌어내는 과정을 Node.js 로 구현하면 위와 같이 된다. 사실 이렇게 고생할 필요 없이 Ethers.js 라이브러리를 이용하면 쉽게 지갑 주소를 생성할 수 있다.

randomBytes 함수를 통해서 개인키를 만든다. 이후 ECDH 개체를 통해서 공개키를 이끌어낸다. Node.js 에서는 이렇게 만든 공개키 앞에 0x라는 16진수 표기가 포함되어 만들어진다. 이를 잘라내기 위한 작업을 subarray 함수로 해놓았다. keccak256 함수는 아쉽게도 외부 라이브러리를 이용하게 되었다. 결과값의 뒤에서부터 40자리(20 bytes)까지 뽑아내면 지갑 주소가 된다.

위 코드를 실행해본 결과를 하나 살펴보자.

CLI wallet

지갑 주소는 0xe46d8ed175a3206377784db66d711437d49f8314로 확인된다.

메타마스크에 개인키를 넣어보자.

Import Private Key

Metamask Wallet Address

CLI에 찍힌 지갑 주소와 메타마스크에 찍힌 지갑 주소가 같다. 성공적으로 구현된 것을 확인할 수 있다.

(추가) EIP-55 Address 대소문자 Checksum

EIP-55는 기존 주소 체계를 벗어나지 않고 주소가 유효한지 검증하기 위한 방법이다.

방식도 간단명료하다. 기존 주소는 keccak256으로 해싱이 된 이후 뒤에서 40 개의 16진수 문자열로 표현된다. 16진수를 표현하기 위해 사용하는 알파벳은 모두 소문자로 나타난다.('a', 'b', 'c', 'd', 'e', 'f') 기존 주소를 다시 keccak256을 하면 다시 32 bytes 의 문자열이 만들어진다.

만들어진 주소를 compared라고 하자 compared 접두어 '0x'를 제외한 나머지 문자열들이 0x8(8)보다 큰지 확인한다. 0x8보다 큰 자리가 있을 경우, 기존의 주소에서 똑같은 자리의 문자를 대문자로 변경한다. 만약 해당 자리의 문자가 숫자이면 변형없이 숫자 그대로 자리를 지킨다.

위에서 설명한 방식으로 Checksum 주소를 만드는 과정을 코드로 구현하면 아래와 같다. 참고로 아래의 코드는 EIP-55에 있는 코드를 거의 따라서 작성했다.

// Convert to checksum address
let addressChecksumed = "0x";
const addressOrigin = address.replace("0x", ""); // remove prefix '0x'
const addressCompared = keccak256(Buffer.from(addressOrigin)).slice(2);

console.log("Address:");
console.log(addressOrigin);
console.log("Address Compared:");
console.log(addressCompared);

for (let i = 0 ; i < addressOrigin.length ; i++) {
    if ( parseInt(addressCompared[i], 16) >= 8 ) {
        addressChecksumed += addressOrigin[i].toUpperCase();
    } else {
        addressChecksumed += addressOrigin[i];
    }
}

console.log("Checksum Address:");
console.log(addressChecksumed)

result checksum

보통은 지갑이나 디앱에서 주소를 복사하고 붙여넣기 할 수 있게 기능을 제공해 줘서, 무의미해 보일 수 있다. 그러나 손으로 주소를 기입하게 되면, 이 Checksum 방식이 잘못된 주소를 거를 수 있는 수단이 될 것이다.

verify checksum address Checksum 주소를 "https://ethsum.netlify.app" 에서 확인해보면 유효하다는 것을 확인할 수 있다.

레퍼런스

개인키로 지갑 주소까지 만들어보자(feat. Node.js, ethereum) - ALROCK Blog