본문 바로가기
경일/Block-chain

[Blockchain/블록체인] Wallet(지갑)/PrivateKey(개인키)/PublicKey(공개키)/account(계정) 톺아보기

by dev_kong 2022. 6. 20.
728x90
728x90

0. 목차

1. 개요

2. 지갑

3. 개인키와 공개키 서명

4. 기능구현

5. 계정

1. 개요

나는 수업을 통해 블록체인에 대해 공부하기 전에는 블록체인과 코인, NFT 그 이외의 기타등등에 대해 완전한 문외한이었다.


그래도 블록체인 지갑(Wallet)이란 말은 몇번 들어본적이 있었고,
그냥 현실세계의 지갑 처럼 코인을 담고있는 무언가라고만 생각을 했었는데,
수업을 통해 내가 유추하던 지갑과는 전혀 다른 것이란 걸 알게되었는데 그 내용을 정리해보려고 한다.

2. 지갑(Wallet)

블록체인에서 말하는 지갑이란,
너무나 당연한 소리지만, 하나의 어플리케이션이다.

 

그리고 이 어플리케이션을 만드는 방법은 매우 다양하다.


가장 많이 쓰인다고 하는 metamask라는 지갑 어플리케이션은
크롬 익스텐션(Chrome extension)으로 만들어져 있다.

 

그럼 이 지갑이라 불리는 어플리케이션은 무엇을 관리하는 것 일까.
결론부터 말하자면 지갑이란, 개인키(Private Key)와 공개키(Public Key)를 관리하는 어플리케이션 그 이상, 그 이하도 아니다.

 

개인키와 공개키 그리고 계정까지.
하나씩 살펴보면서 직접 구현도 해보려고 한다.

3. 개인키와 공개키, 서명

개인키와 공개키, 서명 이 세가지에 대해 알아보기 전에 한가지 이해하고 넘어가야 될 부분이 있다.
이런 것들이 왜 필요한가 이다.

 

예를들어서,
A라는 사람이 B라는 사람에게 1BTC을 보낸다고 생각해보자.

 

기존의 은행거래라면
A는 은행어플을 켜서 원하는 금액을 입력하고, 소유한 계좌(a)에서 송금할 금액을 입력하고 B가 소유한 계좌(b)도 입력하고,
a의 비밀번호를 입력하고 송금버튼을 누르면된다.

 

그리고 이 거래에 대한 검증과 보증은 해당 은행에서 해준다.

 

하지만 블록체인에서는 거래에대한 검증과 보증을 해줄 중앙 서버라는 개념 자체가 존재하질 않는다.

 

그렇기 때문에 사용하는 개념이

공개키 암호화 방식과 해시함수이다.

 

개인키와 공개키는 1:1 매칭 관계에 있다.

 

이게 무슨 말이냐면,

개인키와 Hash(transaction의 내용을 해시화 한 것)를 합쳐서 만들어진 암호(signature=서명)는

개인키로 만들어진 공개키를 이용하면 Hash를 다시 가져올 수 있다.

 

그림으로 보면 이렇다.

👆개인키 => 공개키

👆 개인키 + Hash => signature

👆 signature + 공개키 => 개인키

 

 

이거에 대한 자세한 내용은
굉장히 수학적..인 내용이 들어간다.


무슨 타원 알고리즘 인가 뭔가.

 

타원알고리즘과 블록체인 이 블로그에 자세히 정리 되어있다.
근데 난 읽어도 잘 모르겠더라..

 

무튼 이걸 이용해서 검증을 할 수 있다.

다시 A와 B의 송금내용으로 돌아와서

A가 B에게 1BTC를 보내고 싶다.

 

이때 transactions의 내용에는
sender(A의 공개키), recevier(B의 계정), 보낼 금액(1BTC), signature가 포함되는데
signature는 A의 개인키와 시그니쳐를 제외한 트랜잭션의 내용을 해시화 한것을 합쳐서 만들어진 암호이다.

 

검증을 할 때는
transaction에 들어있는 signature를 sender를 통해 복호화를 했을 때
복호화가 성공하면 코인을 보낸사람이 A라는 것을 알 수 있다.


그리고 복호화를 통해 나온 hash(hash-A)가 나온다.


그리고 transaction 에서 signature를 제외한 데이터를 hash화 한다(hash-B)
hash-A와 hash-B를 비교했을 때 일치한다면,
transaction의 거래내용 까지 검증 할 수 있다.

 

그림으로 보면 이해가 쉽다.


👆 transaction 검증
출처: bitkunst 블로그

4. 기능 구현

이제 직접 구현을 해보자.

 

위에서 말했듯 지갑은 개인키를 관리하는 별도의 프로그램이다.


즉, 완전히 새로운 프로젝트 디렉토리에서 작성을 해도 전혀 상관이 없다.

새로운 디렉토리에 새마음 새뜻으로 작성을 해보자.

 

account, privateKey, publicKey, balance를 가지고 있는 인스턴스 객체를 만들 수 있는 class 함수를 작성해보자.

 

export class Wallet {
  account: string;
  privateKey: string;
  publicKey: string;
  balance: number;

  constructor(_privateKey: string = '') {
    this.privateKey =''
    this.publicKey = ''
    this.account = ''
    this.balance = 0
  }
}

 

우선은 이렇게만 작성을 해놓고, privateKey부터 하나하나 만들어보자.

privateKey를 만드는 방법은 node의 내장라이브러리 중 하나인 crypto를 이용해서 만들거다.

 

import { randomBytes } from 'crypto';

export class Wallet {
  account: string;
  privateKey: string;
  publicKey: string;
  balance: number;

  constructor(_privateKey: string = '') {
    this.privateKey = _privateKey || this.generatePrivateKey()
    this.publicKey = ''
    this.account = ''
    this.balance = 0
  }

 generatePrivateKey(): string {
    return randomBytes(32).toString('hex');
  }
}

 

이렇게 작성해주면 된다.

이번엔 publicKey를 만들어보자.


위에서 얘기했듯, 무슨 타원곡선.. 어쩌고 알고리즘 어쩌고.. 참 어렵다.

 

그리고 이런 어려운건 어떤 똑똑한 사람이 라이브러리를 만들어줬다.
라이브러리 이름은 elliptic이다.

 

npm i elliptic

 

라이브러리 설치를 해주자.

 

import { randomBytes } from 'crypto';
import elliptic from 'elliptic';

const ec = new elliptic.ec('secp256k1');

export class Wallet {
  account: string;
  privateKey: string;
  publicKey: string;
  balance: number;

  constructor(_privateKey: string = '') {
    this.privateKey = _privateKey || this.generatePrivateKey();
    this.publicKey = this.generatePublicKey();
    this.account = ''
    this.balance = 0
  }

 generatePrivateKey(): string {
    return randomBytes(32).toString('hex');
  }

  generatePublicKey(): string {
    const keyPair = ec.keyFromPrivate(this.privateKey);
    return keyPair.getPublic().encode('hex', true);
  }
}

 

elliptic 라이브러리를 이용해서, ec라는걸 만들었는데 이걸 만들때 사용하는 알고리즘이 secp256k1이다.
뭔지 모른다. 그냥 이거 쓰면 된다.

 

그렇게 만들어진 ec라는 인스턴스를 생성하고,
인스턴스안의 메서드(keyFromPrivate)를 이용해서 keyPair를 만들어준다.
이걸 이용해서 publicKey를 만들어 줄 수 있다.

 

마지막으로 account를 만들어보자.
account 같은 경우에는 코인의 종류마다 구현하는 방법이 제각각이다.

 

비트코인 같은 경우에는 publicKey를 hash화를 두번 진행해서 만드는 걸로 알고있고,
이더리움 같은 경우에는 publicKey에서 앞에서 24자를 삭제한 나머지 40글자를 account로 사용한다.

 

비트코인 방식은 구현하기가 어려우니까..
이더리움 방식으로 구현 해보겠음

 

import { randomBytes } from 'crypto';
import elliptic from 'elliptic';

const ec = new elliptic.ec('secp256k1');

export class Wallet {
  account: string;
  privateKey: string;
  publicKey: string;
  balance: number;

  constructor(_privateKey: string = '') {
    this.privateKey = _privateKey || this.generatePrivateKey();
    this.publicKey = this.generatePublicKey();
    this.account = this.generateAccount();
    this.balance = 0
  }

 generatePrivateKey(): string {
    return randomBytes(32).toString('hex');
  }

  generatePublicKey(): string {
    const keyPair = ec.keyFromPrivate(this.privateKey);
    return keyPair.getPublic().encode('hex', true);
  }

  genrateAccount(): string {
    return Buffer.from(this.publicKey).slice(26).toString();
  }
}

 

24자 자른다고 했는데 자른건 26자다.

왜냐하면 PublicKey를 만들때 앞에 02 또는 03 두글자가 추가되기 때문이다.

 

그래서 26글자 잘라주면 40글자가 남는데 이걸 account로 사용할거다.

그리고 지갑을 생성하면 해당 정보를 어딘가에 저장을 해야한다.


지갑의 내용에는 개인키가 들어있기 때문에 절대로 유출 되어선 안된다.

가장 좋은 방법이라곤 할 순 없지만,
우선은 지갑을 생성한 컴퓨터에 저장을 하는 방식으로 만들어 보려한다.

 

// ... 
static createWallet(newWallet: Wallet) {
    const filename = path.join(dir, newWallet.account);
    const filecontent = newWallet.privateKey;
    fs.writeFileSync(filename, filecontent);
  }
//...

 

함수를 하나 추가해주었다.
node.js 내장객체인 fspath를 이용해서 파일을 생성해준다.


파일의 이름은 account가 되고,
파일의 내용은 privateKey가 된다.

 

이 함수는 인스턴스(지갑)이 생성 될때 마다 실행 되어서,
코드를 실행한 컴퓨터의 로컬에 저장을 해주어야 하기 때문에,

constructor 함수 내부에서 마지막에 실행해주면 된다.

 

import { randomBytes } from 'crypto';
import elliptic from 'elliptic';

const ec = new elliptic.ec('secp256k1');

export class Wallet {
  account: string;
  privateKey: string;
  publicKey: string;
  balance: number;

  constructor(_privateKey: string = '') {
    this.privateKey = _privateKey || this.generatePrivateKey();
    this.publicKey = this.generatePublicKey();
    this.account = this.generateAccount();
    this.balance = 0
  }

 generatePrivateKey(): string {
    return randomBytes(32).toString('hex');
  }

  generatePublicKey(): string {
    const keyPair = ec.keyFromPrivate(this.privateKey);
    return keyPair.getPublic().encode('hex', true);
  }

  genrateAccount(): string {
    return Buffer.from(this.publicKey).slice(26).toString();
  }
}

 

 

 

  static getWalletList(): string[] {
    const files: string[] = fs.readdirSync(dir);
    return files;
  }

  static getWalletPrivateKey(_account: string) {
    const filepath = path.join(dir, _account);
    return fs.readFileSync(filepath).toString();
  }

두가지 함수를 추가했다.

 

getWalletList는 지갑정보가 저장되어있는 디렉토리에서 모든 파일을 읽어서 읽은 파일의 이름 전부를 배열에 담아서 리턴해준다.

 

getWalletPrivateKey 함수는 인자값으로 account 를 받는데,
account와 일치하는 이름을 가진 파일의 내용 즉, privateKey를 가져온다.

 

이제 테스트를 위한 서버와 서버에서 보여줄 HTML을 작성해야 되는데,
서버는 express로 만들었다.

 

전체 코드 올릴거임

import express, { application, Request, Response } from 'express';
import nunjucks from 'nunjucks';
import axios from 'axios';
import { Wallet } from './wallet';

const userid = 'kong';
const userpw = '1234';

const baseURL = 'http://localhost:3000';

const basicAuth = Buffer.from(userid + ':' + userpw).toString('base64');

const request = axios.create({
  baseURL,
  headers: {
    Authorization: 'basic ' + basicAuth,
    'Content-type': 'application/json',
  },
});

const app = express();

app.use(express.json());

app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});

app.get('/', (req: Request, res: Response) => {
  res.render('index');
});

app.post('/newWallet', (req: Request, res: Response) => {
  const newWallet = new Wallet();
  res.json(newWallet);
});

app.post('/walletList', (req: Request, res: Response) => {
  const list: string[] = Wallet.getWalletList();
  res.json(list);
});

app.get('/wallet/:account', (req: Request, res: Response) => {
  const { account } = req.params;
  const privatKey = Wallet.getWalletPrivateKey(account);
  res.json(new Wallet(privatKey));
});

app.post('/sendTx', async (req: Request, res: Response) => {
  const {
    sender: { publicKey },
    received,
    amount,
  } = req.body;

  const signature = Wallet.createSignature(req.body);

  // 보낼사람: 공개키, 받는사람:계정, 보낼양, 서명
  const txObject = {
    sender: publicKey,
    received,
    amount,
    signature,
  };

  console.log(txObject);

  const response = await request.post('/sendTx', txObject);
  console.log(response.data);
});

app.listen(3005, () => {
  console.log('3005 \n 서버시작');
});

👆 Wallet Server

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <title>Document</title>
  </head>
  <body>
    <h1>안녕!</h1>
    <button id="wallet_btn">지갑생성</button>
    <ul id="wallet_list">
      <li>Coin : koin</li>
      <li>Account : <span class="account">kong</span></li>
      <li>Private Key : <span class="private_key">asdfasdfasd</span></li>
      <li>Publick Key : <span class="public_key">kong</span></li>
      <li>Balance : <span class="balance">kong</span></li>
    </ul>

    <h1>Transaction</h1>
    <form action="" id="transaction">
      <ul>
        <li class="to">To: <input name="to" placeholder="보낼 계정" /></li>
        <li class="to">
          Value: <input name="value" placeholder="보낼 금액" />
        </li>
      </ul>
      <button type="submit">전송!</button>
    </form>

    <h1>지갑목록</h1>
    <div>
      <button id="wallet_list_btn">지갑목록 가져오셈</button>
      <ul id="wallet_list2"></ul>
    </div>
    <script type="text/javascript">
      const walletBtn = document.querySelector('#wallet_btn');
      const walletListBtn = document.querySelector('#wallet_list_btn');
      const walletList = document.querySelector('#wallet_list2');
      const accountSpan = document.querySelector('.account');
      const privateKeySpan = document.querySelector('.private_key');
      const publicKeySpan = document.querySelector('.public_key');
      const balanceSpan = document.querySelector('.balance');

      const trnasaction = document.querySelector('#transaction');

      const view = (wallet) => {
        const { account, privateKey, publicKey, balance } = wallet;
        accountSpan.innerHTML = account;
        privateKeySpan.innerHTML = privateKey;
        publicKeySpan.innerHTML = publicKey;
        balanceSpan.innerHTML = balance;
      };

      const createWallet = async () => {
        const response = await axios.post('/newWallet', null);
        view(response.data);
      };

      const submitHandler = async (e) => {
        e.preventDefault();

        const publicKey = document.querySelector('.public_key').innerHTML;
        const account = document.querySelector('.account').innerHTML;

        const data = {
          sender: {
            publicKey,
            account,
          },
          received: e.target.to.value,
          amount: Number(e.target.value.value),
        };

        const response = await axios.post('/sendTx', data);
      };

      const getPrivateKey = async (e) => {
        const account = e.target.innerHTML;
        const response = await axios.get(`/wallet/${account}`);
        view(response.data);
      };

      const getWalletList = async () => {
        const response = await axios.post('walletList', null);
        const list = response.data.forEach((v) => {
          const liEliment = document.createElement('li');
          liEliment.innerHTML = v;
          liEliment.onclick = getPrivateKey;
          walletList.append(liEliment);
        });
      };

      trnasaction.addEventListener('submit', submitHandler);
      walletBtn.addEventListener('click', createWallet);
      walletListBtn.addEventListener('click', getWalletList);
    </script>
  </body>
</html>

👆 Wallet index.html

간단한 서버 구축과 간단한 HTML,JS 라 따로 설명은 안적을 거임

이렇게 생긴 지갑프로그램을 만들었다.

 

지갑목록 가져오셈 버튼을 누르면 파일로 저장된 지갑주소가 나오고,
지갑주소(account)를 클릭하면,
클릭된 account를 이용해서 인스턴스를 생성하고 생성된 인스턴스를 화면에 보여주는 코드다.

 

이제 만들거는 Transaction 부분이다.
1번 지갑에서 2번 지갑으로 5BTC를 보내려고 한다면

위의 화면에서
To 에는 2번지갑주소를
Value 에는 10을 입력해준다


그리고 전송 버튼을 누르면
submitHandler 함수가 동작하는데,
이 함수는 입력받은 값으로

 

{
  sender: {
    publicKey,
      account,
  },
    received: e.target.to.value,
      amount: Number(e.target.value.value),
}

 

위 모양의 객체를 만들어서 서버에 요청을 보낸다.

 

app.post('/sendTx', async (req: Request, res: Response) => {
  const {
    sender: { publicKey },
    received,
    amount,
  } = req.body;

  const signature = Wallet.createSignature(req.body);

  // 보낼사람: 공개키, 받는사람:계정, 보낼양, 서명
  const txObject = {
    sender: publicKey,
    received,
    amount,
    signature,
  };

  console.log(txObject);

  const response = await request.post('/sendTx', txObject);
  console.log(response.data);
});

 

위 라우터에서 요청을 받으면,
시그니쳐를 만들어서 txObject 객체를 만들어준다.

 

그런데 현재 Wallet class 에는 createSignature 메서드가 없다.
만들어주자.

 

  static createSignature(_obj: any): elliptic.ec.Signature {
    const {
      sender: { account, publicKey },
      received,
      amount,
    } = _obj;

    const hash: string = SHA256(
      [publicKey, received, amount].join(''),
    ).toString();

    const privateKey: string = Wallet.getWalletPrivateKey(account);

    const keyPair: elliptic.ec.KeyPair = ec.keyFromPrivate(privateKey);
    const signature: elliptic.ec.Signature = keyPair.sign(hash, 'hex');

    return signature;
  }

 


위 그림대로 만든거다.

 

만들어진 signature를 이용해 txObject를 만든뒤에,
이걸 블록체인 네트워크로 전송을 해줘야 한다.

 

블록체인 네트워크에는 아직 /sendTx 라는 라우터가 없으므로
만들어주자..

 

app.post('/sendTx', (req: Request, res: Response) => {
  try {
    const receivedTx: ReceviedTx = req.body;
    Wallet.sendTransaction(receivedTx);
    res.json([]);
  } catch (error) {}
});

👆 index.ts

 

위 라우터에 Wallet이 있는데,
이 Wallet은 아까 그 Wallet과는 다른 녀석이다.


core 디렉토리 내에 새로 하나 만들어 줘야 한다.

 

import { SHA256 } from 'crypto-js';
import elliptic from 'elliptic';

const ec = new elliptic.ec('secp256k1');

export type Signature = elliptic.ec.Signature;

export interface ReceviedTx {
  sender: string;
  received: string;
  amount: number;
  signature: Signature;
}

export class Wallet {
  publicKey: string;
  account: string;
  balance: number;
  signature: Signature;

  constructor(_sender: string, _signature: Signature) {
    this.publicKey = _sender;
    this.account = this.getAccount();
    this.balance = 0;
    this.signature = _signature;
  }

    getAccount(): string {
    return Buffer.from(this.publicKey).slice(26).toString();
  }
}

 

우선 이렇게 간단하게 만든뒤에,
위에 서버에서 사용하는 메서드 sendTransaction를 작성하자.

 

import { SHA256 } from 'crypto-js';
import elliptic from 'elliptic';

const ec = new elliptic.ec('secp256k1');

export type Signature = elliptic.ec.Signature;

export interface ReceviedTx {
  sender: string;
  received: string;
  amount: number;
  signature: Signature;
}

export class Wallet {
  publicKey: string;
  account: string;
  balance: number;
  signature: Signature;

  constructor(_sender: string, _signature: Signature) {
    this.publicKey = _sender;
    this.account = this.getAccount();
    this.balance = 0;
    this.signature = _signature;
  }

    static sendTransaction(_receviedTx: ReceviedTx) {
    /*
      TODO: 서명 검증
      TODO: 보내는 사람의 지갑정보 최신회
      TODO: Balance 확인
      TODO: Tx 생성
    */

    /**
     * 서명(Signature) 검증
     */
    const verify = Wallet.verifySignature(_receviedTx);

    if (verify.isError) {
      throw new Error(verify.error);
    }

    console.log(verify.isError);
    /**
     * 보내는 사람의 지갑 최신정보
     */
    const myWallet = new this(_receviedTx.sender, _receviedTx.signature);
  }

    getAccount(): string {
    return Buffer.from(this.publicKey).slice(26).toString();
  }
}

방금 추가된 따끈따끈한 함수 sendTransaction은 총 4가지의 작업을 수행해야 하는데,

우선 오늘은 서명을 검증하고, 보내는 사람의 지갑정보를 최신화 하는 것 까지만 할거다.

 

트랜잭션에 관한 자세한 내용은 다음 포스팅에 작성할 예정이다.

 

우선 해야할 것이 서명검증이다.
서명검증을 위해 이 class에 verifySignature 라는 메서드를 추가해주자

 

static verifySignature(_receviedTx: ReceviedTx): Failable<undefined, string> {
    /*
      서명검증
      1. hash만들기
      2. keyPair 만들기
      3. 검증하기 
      4. 검증 결과에 따라 리턴해주기
     */
    const { sender, received, amount, signature } = _receviedTx;

    // 1.
    const hash: string = SHA256([sender, received, amount].join('')).toString();

    // 2.
    const keyPair = ec.keyFromPublic(sender, 'hex');

    // 3.
    const isVerify = keyPair.verify(hash, signature);

    // 4.
    if (!isVerify) {
      return { isError: true, error: '서명이 올바르지 않음' };
    }

    return { isError: false, value: undefined };
  }

 

서명을 검증하는 로직은 위 함수에 주석에 적어놓았다.

 

우선 hash와 kepair를 만들고 이 두개와 elliptic 라이브러리를 이용해서 손쉽게 검증을 할 수 있다.

 

isVerify 는 boolean 값을 갖게 되는데,
검증에 성공하면 true 를 실패하면 false를 리턴한다.

 

그리고 이 검증 결과에 따라 리턴값을 정해준다.
리턴은 이전에 만들었던 Failable을 사용했다.

 

다시 sendTransaction 메서드로 돌아가서,
const verify = Wallet.verifySignature(_receviedTx);
위 코드의 verify 는 Failable type을 갖게 되므로
verify.isError를 통해 검증의 성공 여부를 확인한뒤


검증에 성공 했다면,
const myWallet = new this(_receviedTx.sender, _receviedTx.signature);
위 코드를 통해 지갑정보를 최신화 한다.

 

이제 트랜잭션을 만들고 트랜잭션을 블록에 저장하는 부분을 해야되는데

그 부분은 다음 포스팅에 작성할 예정이다.

728x90
728x90

댓글