개인키로 지갑 주소까지 만들어보자 - 2(feat. Node.js, Solana)
March 05, 2025
배경
최근 블로그 포스트로 EVM 계열의 주소를 개인키로부터 이끌어내는 과정을 다뤘었다. 요즘에는 솔라나 체인에서 활동을 하고 있는데 EVM과는 다른 형식의 주소 형식을 사용한다. 호기심이 동해서 똑같은 과정으로 솔라나의 지갑 주소를 깊게 파헤쳐 보려고 한다.
지갑 주소 만드는 과정
- 임의의 32 bytes 데이터를 만든다.
- Public Key(지갑주소, 16진수 형태, 이하 공개키)를 생성한다.
- 공개키를 base58 인코딩한다.
먼저 임의의 32 bytes 데이터를 만든다. EVM에서 지갑 주소를 만들 때와 똑같은 시작이다.
ed25519를 이용해서 임의로 만들어두었던 32bytes 데이터로 공개키를 만들어 낼 수 있다. 이 공개키를 base58 형식으로 인코딩을 하면 솔라나에서 사용하는 지갑 주소를 이끌어낼 수 있다.
여기서 더 나아가 솔라나 지갑 상에서 사용하는 개인키도 설명하도록 하겠다. 솔라나 개인키는 이더리움과 다르게 값을 십진수 형태로 raw한 형식을 가지고 있다. 예를 들면 'abef'라는 16진수 문자열을 '[171, 239]'의 십진수 배열의 형태로 저장한다. 개인키는 지갑 주소를 만들 때 사용했던, Random 값과 이 값으로 이끌어냈던 지갑 주소 값을 이어 붙이기만 하면 된다.
base58은 base64 표기 문자 중, 화면 상으로 분류하기 어려운 알파벳과 표기에 사용하던 모든 부호 문자를 삭제하고 남은 문자들로 데이터를 표현하는 방식이다.
ECDSA(secp256k1) vs EdDSA(ed25519)
구현에 앞서, 이번에 솔라나 지갑 주소를 만들면서 사용하게 된 ed25519 에 대해서 알아보자.
이더리움에서는 secp256k1를 이용했었다. secp256k1는 ECDSA(Elliptic Curve Digital Signature Algorithm)에서 사용하는 알고리즘이다. 유한체에서 정의된 타원 곡선을 이용한다. 타원 곡선의 방정식은 다음과 같다.
y^2 = x^3 + 0 * x + 7 => y^2 = x^3 + 7
이 암호화 알고리즘의 작동방식을 간단히 정리해보자. 위 식으로 그려지는 그래프 위의 점들, 이 점들 간에 이루어지는 덧셈 연산을 정의한다. 이 덧셈 연산은 기존 우리가 아는 수들의 덧셈과는 다르게 역산하기가 어렵다는 특징이 있다. 타원 곡선 상에서 생성자(Generator)로 사용할 임의의 점 G을 정해놓고, 랜덤값을 더해서 만든 결과값이 만들어진다. 결과값과 G를 알더라도 이용한 랜덤값을 구하기가 어렵다는 점을 이용한 것이다. 기존 우리가 알던 것처럼 결과값에서 G값을 빼서 랜덤값을 알아내는 방식은 여기선 통하지 않는다.
ECDSA는 비교적 짧은 키 값으로 연산이 가능해 빠르다는 특징을 가지고 있다. 이전에 사용하던 RSA 암호화 방식은 보안을 위해서 키 값을 늘려야했던 것에 비해 대조적이다.
ed25519는 EdDSA(Edwards-curve Digital Signature Algorithm)에서 사용하는 알고리즘이다. Github에서 ssh를 등록할 때 선택지 중 하나로 본 경험이 있을 수 있다. 뒤틀린 에드워즈 곡선(Edwards25519)에 기반해서 암호화를 하는 기법인데, 보안성은 유지하며 더 빠르게 작동한다. 이 곡선의 방정식은 다음과 같다.
a*x^2 + y^2 = 1 + d*x^2*y^2
ECDSA는 서명값과 메시지로부터 Public Key를 복원하는 방법을 제공하는 반면, EdDSA는 Public Key를 복원하는 방법이 없다는 것이 큰 차이점이라고 한다.
구현
import { randomBytes } from "crypto";
import { getPublicKeyAsync } from "@noble/ed25519";
import bs58 from "bs58"
(async ()=>{
// 1. 임의의 32 bytes 데이터를 만든다.
const randomKey = randomBytes(32);
console.log(randomKey)
console.log(randomKey, 'hex');
console.log(JSON.stringify(randomKey.toJSON().data));
const pkBuffer = randomKey;
// 2. Public Key(지갑주소, 16진수 형태, 이하 공개키)를 생성한다.
const publicKeyRaw = await getPublicKeyAsync( pkBuffer );
// 3. 공개키를 base58 인코딩한다.
const publicKey = bs58.encode(publicKey);
// (option). 솔라나 개인키 만들기
const keypair = Buffer.concat([pkBuffer, publicKey])
console.log(`Public Key: ${bs58.encode(publicKey)}`);
console.log(JSON.stringify(keypair.toJSON().data));
console.log(keypair.length);
})()
외부 라이브러리를 2 개 이용했다.
하나는 ed25519
암호화함수를 제공하는 @noble/ed25519
이고,
다른 하나는 base58
인코딩 함수를 제공하는 bs58
이다.
위에서 설명한 지갑 주소 만드는 과정을 순차적으로 구현한 코드다.
[결과 로그]
만들어진 지갑 주소를 검증하기 위해 Solflare
에 솔라나 개인키를 넣은 결과값을 비교해봤다.
[지갑에서 개인키로 가져오기]
Solflare
의 지갑 주소 값과 터미널 로그 상의 지갑 주소 값이 일치하고 있음을 확인 가능하다.