Blockchain

#10 지갑, 개인키

Sila 2022. 6. 20. 21:27

이제부턴 암호화폐 지갑에서 다른 지갑으로 트랜잭션을 일으키고,

 

이를 블록체인에 기록으로 남기는 것에 대해 공부를 할 예정이다.

 

우선 지갑을 생성하는 방법에 대해 알아보고, 그 지갑은 어떻게 지키는지 상대방에게 내 지갑이 어떤 것인지를

 

어떤 방식으로 알려주는지 등에 대해 알아보자.

 

1. set up

npm i crypto

npm i elliptic
npm i --save-dev elliptic

 

우선 터미널에서 npm으로 crypto, elliptic 라이브러리를 설치한 후, 다음과 같이 core 폴더에 wallet 폴더를 만들고,

 

그 안에 wallet.test.ts 파일을 생성한다.

 

/*  core/wallet/wallet.test.ts  */

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

const new elliptic.ec('secp256k1')

 

crypto 라이브러리는 원하는 길이의 난수를 원하는 형식으로 (여기서는 16진수를 사용) 생성해주는데 사용할 라이브러리,

 

elliptic은 타원 곡선을 이용해 개인키로 공개키를 생성해주는데 사용하는 라이브러리이다.

 

이는 수학적인 지식이 필요하니 그냥 개인키를 이용해 특정한 알고리즘을 거쳐 공개키를 생성해주는 라이브러리라고만

 

이해해도 괜찮을 것 같다.

 

2. 지갑 

지갑 다른 말로 개인키는 hash값과 마찬가지로 64자리의 16진수이다.

 

암호화폐 지갑이라는 것은 우리들의 주민번호처럼

 

개인이 다른 사람이 모르게 보관해야하는 일련의 16진수 문자열에 불과하다.

 

그렇기 때문에 만드는 것도 난수 생성을 통해 간단히 만들 수 있다.

 

이 문자열이 같을 가능성은 2의 256제곱이므로 사실상 중복될 가능성은 없기 때문에

 

심지어 인터넷 연결 없이도 혼자서 만들 수도 있다.

 

개인키를 이용해 공개키를 만들고, 공개키의 일부를 잘라 지갑 주소로 사용한다.

 

이 값들을 설치한 라이브러리들을 이용해 직접 생성해보자.

 

/*  src/core/wallet/wallet.test.ts  */

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

const ec = new elliptic.ec('secp256k1')
// 개인키의 암호화 메커니즘 재료로 사용될 타원을 가져온다.
// secp256k1은 타원 곡선의 한 종류이다.

describe('wallet study', () => {
    let privKey : string
    let pubKey : string
    let signature : elliptic.ec.Signature
    // 라이브러리 내장함수 Signature은 공개키로 서명을 만드는데 쓰인다.
    
    // 개인키 생성 코드
    if('privateKey', () => {
        privKey = randomBytes(32).toString('hex')
        // 길이 32의 난수를 16진수 형식으로 생성해 변수에 넣는다 
    })
    
    // 개인키 > 공개키 생성 코드
    it('create publicKey', () => {
        const keyPair = ec.keyFromPrivate(privKey)
        pubKey = keyPair.getPublic().encode('hex',true)
        // privateKey를 이용해 공개키를 만드는 코드
        // 사용된 메소드 정도만 알아두고 넘어가면 된다.
    })
    
    // 개인키 > signature 생성 함수
    it('digital sign', () => {
        const keyPair = ec.keyFromPrivate(privKey)
        const hash = SHA256('txhash').toString()
        // txhash는 여기서는 임의의 문자열을 사용했다.
        
        signature = keyPair.sign(hash, 'hex')
        //sign 메소드를 사용해 원하는 문자열과 형식으로 서명 생성
    })
    
    // 서명이 위조되지 않았는지 (지갑 주인이 서명한게 맞는지) 확인하는 코드
    it('sign verify', () => {
        const hash = SHA256('txhash').toString()
        const verify = ec.verify(hash, signature, ec.keyFromPublic(pubKey, 'hex')
        console.log(verify)
        // 서명 검증 결과 문제가 없다면 true, 아니면 false가 리턴된다.
    })
    
    // 공개키 > 지갑주소 생성 코드
    it('create account', () => {
        const buffer = Buffer.from(pubKey)
        const address = buffer.slice(26).toString()
        // 공개키의 끝 26자리를 잘라 계좌번호로 사용한다.
        console.log(address)
    })
}

 

내가 만든 hash값과 keyPair를 조합해 서명을 만든다

 

hash, keyPair, 서명을 상대방에게 주면 상대방은 이 중 hash, keyPair로

 

나와 동일한 연산을 수행해 내가 준 서명과 동일한지 확인한다.

 

동일하다면 서명자가 키소유자와 동일한 인물임이 증명된다.

 

3. 네트워크와 상호작용

지갑은 블록체인 네트워크와 상호작용해 트렌젝션을 블럭에 기록해야한다.

 

nodeJS에서 프론트앤드 서버와 백엔드서버로 비유를 들어 생각을 해보자.

 

어떤 사이트에 회원가입을 할 때, 프론트에 정보를 입력해 제출하면

 

이는 백앤드 서버에 전달된다. 백앤드에서는 받은 데이터를 미리 만들어둔 쿼리문에 매개변수로 넣어

 

DB에 삽입하는 등의 코드를 실행한다.

 

비슷하게, 지갑에서 데이터를 블록체인에 전달하면 (여기서 데이터는 보통 트랜젝션에 대한 데이터)

 

블록체인은 검증을 거쳐 그 트랜젝션 데이터를 블록에 추가한다.

 

기본적인 지갑 생성에 대한 것은 해봤으니 이제부턴 네트워크 상에서 지갑을 생성하거나 불러오고,

 

이를 블록체인 네트워크와 상호작용 시키는 기능을 구현해볼 것이다.

 

3.1 지갑 생성 

루트 디렉토리에 wallet 폴더를 만들고 server.ts 파일을 생성한다.

 

npm i nunjucks
npm i --save-dev @types/nunjucks

 

/*  wallet/server.ts  */

import express from 'express'
import nunjucks from 'nunjucks'

const app = express()

app.use(express.json())
app.set('view engine', 'html')

nunjucks.configure('views', { express :app })

app.get('/', (req, res) => {
    res.send('hello wallet')
})

app.listen(3005, () => {
    console.log('server run', 3005)
})

// 이 server.ts 파일을 실행하려면 npm run dev:ts wallet/server 를 입력한다.

 

문제없이 서버가 구동된다면 이제 wallet.ts 파일을 생성해 좀 전에 키를 생성한 테스트 코드를 비슷한 방식으로

 

작성해주면 된다.

 

/*  wallet/wallet.ts  */

// 여기서는 wallet class 객체를 생성해준다.

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

import fs from 'fs'
import path from 'path'
// 생성된 키와 지갑 주소를 파일로 보관하기 위해 fs, path 라이브러리 사용

const ec = new elliptic.ec('secp256k1')
const dir = path.join(__dirname, '../data')
// 루트 디렉토리에 data 폴더를 생성해 여기 지갑 파일을 저장한다.

export class Wallet {
    public account : string
    public privateKey : string
    public account : string
    public balance : number
    
    constructor () {
        this.privateKey = this.getPrivateKey()
        this.publicKey = this.getPublicKey()
        this.account = this.getAccount()
        this.balance = 0
        
        Wallet.createWallet(this)
    }
    
    // 키, 지갑주소 생성 코드 (순서대로 개인키, 공개키, 지갑 주소)
    public getPrivateKey(): string {
        return randomBytes(32).toString('hex')
    }
    
    public getPublicKey():string {
        const keyPair = ec.keyFromPrivate(this.privateKey)
        return keyPair.getPublic().encode('hex', true)
    }
    
    public getAccount() : string {
        return Buffer.from(this.publicKey).slice(26).toString()
    }
    
    // 여기에 더해 만든 지갑에 대한 정보를 파일로 저장하는 함수를 추가한다.
    static createWallet(myWallet : Wallet ) {
        const filename = path.join(dir, mywallet.account)
        // 지갑주소를 파일 이름으로 지정
        const filecontent = mywallet.privateKey
        // 파일 내용(텍스트)에는 개인키를 저장한다
        fs.writeFileSync(filename, filecontent)
        // 파일이름, 파일내용을 지정해 writFileSync로 텍스트 파일 생성
    }
}

 

이 코드를 프론트에서 실행할 수 있도록 html 파일을 만들어주자.

 

/*  views/index.html  */

<!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>
    // axios를 head의 script에 추가

    <title>Document</title>
</head>
<body>
    <h1>Wallet Practice</h1>
    
    <button id='Wallet_btn'>지갑 생성</button>
    
    <script type='text/javascript'>
        const walletBtn = document.querySelector('#Wallet_btn')
        
        const createWallet = async () => {
            const repsonse = await axios.post('/newWallet', null)
            console.log(response.data)
        }
        walletBtn.addEventListener('click', createWallet)
    </script>
</body>

 

버튼을 누르면 지정한 URI로 요청이 가고, 이 라우터에서 지갑 생성 함수를 실행해 그 결과를 콘솔에 출력해줄 것이다.

 

server.ts 파일로 가서 요청을 보낸 라우터에 대한 내용을 작성해준다.

 

/*  wallet/server.ts  */

app.post('/newWallet', (req, res) => {
    res.json(new Wallet())
    // 새로운 Wallet class 객체 생성
})

 

여기까지 하고 버튼을 클릭했을 때, 새로운 지갑이 생성되어 그 정보가 data 폴더 안에 파일로 저장되면 된다.

 

3.1 지갑 불러오기

생성된 지갑을 불러와보자.

 

wallet class 객체에 지갑들을 가져오는 함수 getWalletList 함수를 추가해보자.

 

/*  wallet/wallet.ts  */

export class Wallet {
    //...중략
    
    static getWalletList() : string[] {
        const files : string[] = fs.readdirSync(dir)
        // dir 내의 파일을 가져와 배열에 넣는다
        return files
    }
}

 

지정해준 uri에 요청을 보내면 이 getWalletList 함수가 실행되어 지갑 정보를 전부 가져온다.

 

/*  wallet/server.ts  */

app.post('./walletList', (req, res) => {
    const list = Wallet.getWalletList()
    res.json(list)
})

 

html에서 이 uri에 요청을 보낼 element를 만들어준다.

 

/*  views/index.html  */

// ...중략

<h1> Wallet list </h1>
<button id='wallet_list_btn'> 지갑 목록 버튼 </button>
<div class = 'wallet_lists'>
    <ul>
    
    </ul>
</div>

<script type='text/javascript'>
// ...중략
    const walletListBtn = document.querySelector('#wallet_list_btn')

    const getWalletList = async() => {
        const walletList = document.querySelector('.wallet_lists > ul')
        const reponse = await axios.post('/walletList', null)
        
        const list = response.data.map((wallet) => {
            return `<li>${wallet}</li>`
        })
        
        walletList.innerHTML = list
    }
    
    walletListBtn.addEventListener('click', getWalletList)
</script>

 

지갑 생성도 그렇고, 파일로 저장된 정보를 불러오는 것도 그렇고

 

html에서 해당 정보를 요청할 element와 비동기통신 관련 함수를 만들고,

 

라우터로 요청을 보내고,

 

라우터에서 요청을 받아 class 객체 내의 함수를

 

실행하는 단계를 순차적으로 만들어주고 있다.

 

3.2 지갑 정보 가져오기

이제 지갑을 불러왔을 때, 지갑 리스트에서 하나를 선택해 클릭하면 지갑에 대한 정보 (개인키, 공개키, 주소)를 

 

가져오는 기능을 만들어보자.

 

굳이 모든 정보를 가지고 올 필요 없이 개인키만을 가져와서 공개키와 주소를 즉석에서 만들어줄 수 있다.

 

내가 가진 지갑 주소 정보와 동일한 파일명을 가진 파일의 내용을 읽어

 

개인키를 가져오는 함수를 getWalletPrivateKey 함수를 wallet class 객체에 추가한다.

 

/*  wallet/wallet.ts  */

static getWalletPrivateKey ( _wallet : string) {
    const filepath = path.join(dir, _wallet)
    // 파일 이름을 읽어 일치하는 파일명을 가진 파일을 찾는다
    const filecontent = fs.readFileSync(filepath)
    // 해당 파일의 내용이 지갑의 개인키이므로 이를 읽어온다
    return filecontent.toString()
    // 개인키를 string으로 바꿔 리턴
}

 

지갑을 불러와 <li> element에 그 지갑 주소를 가져왔으므로 이를 클릭하면 함수가 발동하게끔 수정한다.

 

/*  views/index.html  */

`<li onClick="getView('${wallet}')">${wallet}</li>`
// 지갑 주소에 해당하는 파일을 찾아 키를 읽어오게끔 지갑 주소를 매개 변수로 투입한다

 

요청을 보낼 함수를 만든다.

 

/*  views/index.html  */

const getView = async(wallet) => {
    const response = await axios.get(`/wallet/${wallet}`)
    console.log(response.data)
    view(response.data)
}

const view = (wallet) => {
    const account = document.querySelector('.account')
    const publicKey = document.querySelector('.publicKey')
    const privateKey = document.querySelector('.privateKey')
    const balance = document.querySelector('.balance')

    account.innerHTML = wallet.account
    publicKey.innerHTML = wallet.publicKey
    privateKey.innerHTML = wallet.privateKey
    balance.innerHTML = wallet.balance
}

 

요청을 받아 함수를 실행 라우터를 만든다.

 

/*  wallet/server.ts  */

app.get('wallet/:wallet', (req, res) => {
    const { wallet } = req.params
    console.log(wallet)
    
    const privateKey = Wallet.getWalletPrivateKey(wallet)
    // 지갑 주소를 찾아 그 파일에 담긴 개인 키를 가져온다
    res.json(new Wallet(privateKey))
    // 개인키를 가지고 새로운 Wallet class 객체를 생성한다.
}

 

주의할 점은 지갑의 모든 정보를 통째로 가져오는게 아니라, 개인키만을 가져와서 이 개인키로

 

아예 새로운 Wallet class 객체를 생성해준다는 점이다.

 

개인키가 같다면 이로부터 생성되는 공개키와 지갑 주소가 전부 일정하므로 이렇게 하는게 효율적이다.

 

그런데 이렇게 하면 Wallet class 객체를 생성할 때, 매개변수로 개인키를 줄수도, 안 줄수도 있는데

 

주지 않는다면 난수 생성을 통해 만들면 되지만 줄 경우 이를 우선적으로 사용해 Wallet class 객체를 생성하게끔

 

케이스를 분리해주어야 한다.

 

constructor 함수를 다음과 같이 수정해주면 된다.

 

/*  wallet/wallet.ts  */

constructor ( _privateKey : string = '') {
// 매개변수 개인키의 디폴트 값으로는 공백을 준다.
    this.privateKey = _privateKey || this.getPrivateKey()
    // 매개변수로 개인키가 있다면 이를 사용, 없다면 난수 생성함수로 만들어준다.
    this.publicKey = this.getPublicKey()
    this.account = this.getAccount()
    this.balance = 0

    Wallet.createWallet(this)
}

 

지갑에 관한 정보를 생성하고 불러오는 것은 이 정도로 하고, 다음 글에서는 트랜젝션에 대해 알아보자.

'Blockchain' 카테고리의 다른 글

#12 Transaction ch2.코인 전송  (0) 2022.06.22
#11 Transaction ch1.개념  (0) 2022.06.22
#9 체인 최신화  (0) 2022.06.17
#8 mining ch.2 - 활용  (0) 2022.06.17
#7 mining ch.1 - 개념  (0) 2022.06.17