이번 글에서는 실제로 코인을 전송하는 것을 구현해보자.
1. set up
1.1 axios set up
앞으로는 나 혼자서 블록체인 서버와 지갑 서버 두 개의 서버를 구동하고, 이 두 서버들 사이의 상호작용을 활용해야 한다.
이 때 생길 수 있는 CORS 에러 등의 문제를 미리 방지하고 두 서버의 상호작용을 조금 더 편리하게 할 수 있도록
Basic과 axios의 메소드 하나를 활용할 것이다.
Basic이란 원래 uri에서 통신 프로토콜과 호스트 사이에 접속하고자 하는 사용자의 개인 정보를 넣어줌으로써
uri에서 로그인과 유사한 기능을 사용할 수 있게 만드는 장치이다.
클라이언트가 서버에 접속할 때, 아이디와 비밀번호를 입력해 host에 함께 요청하면
서버는 uri의 basic을 읽어 허용된 클라이언트인지 확인 후, 클라이언트의 접속 허용 여부에 따라 다른 응답을 줄 수 있다.
우선 서버의 역할을 하게 될 루트 디렉토리의 index.ts (이하 blockchain 서버)에 다음과 같이 클라이언트 정보를 추가한다.
/* index.ts */
app.use((req,res,next) => {
const baseAuth : string = (req.headers.authorization || '').split(' ')[1]
if(baseAuth === '') {
return res.status(401).send()
// Basic 정보가 없다면 에러를 응답으로 전송한다.
}
const [userid, userpw] = Buffer.from(baseAuth, 'base64').toString().split(':')
if( userid !== 'lsj' || userpw !== '1234' ) {
return res.status(401).send()
}
// 사용자 정보가 틀리다면 에러를 응답으로 전송
console.log(userid, userpw)
next()
// 사용자 정보가 맞다면 다음 미들웨어 실행
})
사용자의 userid가 lsj, userpw가 1234인 요청에만 적절한 응답을 주고, 그렇지 않은 경우 에러를 응답으로 줄 수 있다.
클라이언트 역할을 할 지갑 서버에는 다음과 같이 적어준다. (이하 지갑 서버)
/* wallet/server.ts */
// ...중략
const userid = process.env.USERID || 'lsj'
const userpw = process.env.USERPW || '1234'
// id, pw를 설정
const baseURL = process.env.BASEURL || 'http://localhost:3000'
// 디폴트로 접속할 호스트를 정해준다
const baseAuth = Buffer.from( userid + ':' + userpw ).toString('base64')
const request = axios.create({
baseURL,
headers : {
Authorization : 'Basic ' + baseAuth, // Basic 쓰고 한 칸 띄워야 한다
'Content-type' : 'application/json'
}
})
변수 request엔 axios의 메소드 중 하나인 create를 사용한 결과물이 담긴다.
우리가 뭔가 요청을 보낼때마다 호스트나 포트를 입력하지 않아도 알아서 미리 지정해둔 값으로 요청을 보내고,
이 때 요청을 보내는 사용자의 정보를 함께 전송하면 블록체인 서버에서 사용자 정보를 읽은 후 허용된 사용자라면
응답을 준다.
1.2 tx 요청 객체, 데이터 전송 루트 생성
우선 tx에 관한 여러 데이터를 객체로 만들어 프론트에서 지갑 서버를 거쳐 블록체인 서버까지 보내고,
다시 블록체인 서버에서 응답을 돌려줄 수 있게 통신의 루트를 만들어줘야 한다.
다음과 같이 html 문서에 데이터를 채워 submit하면 요청을 보내주게끔 element와 script를 추가해준다.
/* views/index.html */
<!-- 지갑 생성, 지갑 리스트 사이에 넣어준다 -->
<h1> transaction <h1/>
<form id='transaction_form'>
<ul>
<li> receiver : <input id='receiver' placeholder='받을 사람'/> <li/>
<li> amount : <input id='amount' placeholder='보낼 금액'/> <li/>
<ul/>
<input type='submit' value='전송'/>
<form/>
// ...중략
<script type='text/javascript'>
// ...중략
const transactionForm = document.querySelector('#transaction_form')
const submitHandler = async (e) => {
e.preventDefault()
const publicKey = document.querySelector('.publicKey').innerHTML
const account = document.querySelector('.account').innerHTML
// 보내는 사람의 공개키, 지갑 주소를 전송
const data = {
sender : {
publicKey,
account
},
// 발신자 정보
receiver : e.target.receiver.value,
amount : parseInt(e.target.amount.value)
// 수신자 주소, 액수 정보
}
const response = await axios.post('/sendTransaction', data)
}
transactionForm.addEventListener('submit', submitHandler)
<script/>
요청을 보낸 uri에서 받은 data 객체를 적절히 처리해줄 미들웨어를 만들어준다.
/* wallet/server.ts */
app.post('/sendTransaction', async (req, res) => {
console.log(req.body)
const { sender : { account, publicKey }, receiver, amount }} = req.body
// 여기서는 받은 데이터를 기반으로 서명을 만들고, 그 서명까지 포함해
// 블록체인 서버에 보내고 싶다.
// 우선 첫 번째로 할 일은 서명을 생성할 함수를 만들어주는 것이다.
})
첫 번째로 서명을 만들 함수 createSign을 wallet class 객체 안에 만들어보자.
/* wallet/wallet.ts */
static createSign ( _obj:any ) {
const { sender : { account, publicKey }, receiver, amount } = _obj
// 받은 데이터를 구조 분해할당
const hash : string = SHA256([publicKey, receiver, amount].join('')).toString()
// 받은 데이터를 해시화
const privateKey : string = Wallet.getWalletPrivateKey(account)
const KeyPair : elliptic.ec.KeyPair = ec.keyFromPrivate(privateKey)
return keyPair.sign(hash, 'hex')
// 만든 해시값과 개인키를 이용해 서명을 생성해 리턴
}
이제 이 함수를 /sendTransaction 라우터의 미들웨어 안에서 실행해 거기서 서명을 만들고 그걸 포함한
객체를 새로 만들어 블록체인 서버로 보낸다.
/* wallet/server.ts */
app.post('/sendTransaction', async (req, res) => {
// ...중략
const signature = Wallet.createSign(req.body)
const txObject = {
sender : publicKey,
receiver,
amount,
signature
}
const response = await request.post('/sendTransaction', txObject)
// axios 대신 아까 setup때 만든 request를 사용하면 미리 우리가 정한
// 호스트, 포트로 요청을 전송할 수 있다. path만 써주면 된다.
})
이제 블록체인 서버에서 받은 트렌젝션 객체를 처리해줘야 한다.
이를 위해서는 블록체인 서버 쪽에서도 지갑 class 객체를 생성해줄 필요가 있다.
블록 체인 서버의 Wallet class와 지갑 서버의 Wallet class는 전혀 다른 class라는 것을 기억해두어야 한다.
속성들을 비교해보면 완전히 다른 속성을 가지고 있음을 확인할 수 있는데,
지갑 서버에는 내 지갑의 현재 상태가 중점이 되지만, 블록체인 서버에서는 지갑 간의 거래가 중점이 된다.
가장 큰 차이점으로 지갑 서버는 지갑의 사용자가 사용하는 서버이기 때문에 개인키가 있어도 되지만,
블록 체인서버에는 개인키가 있어서는 안되고, 대신 서명이 비슷한 역할을 수행한다.
/* src/core/wallet/wallet.ts */
import { SHA256 } from 'crypto-js'
import elliptic from 'elliptic'
const ec = new elliptic.ec('secp256k1')
export type Signature = elliptic.ec.Signature
// Signature class를 생성
export interface ReceivedTx {
sender : string
receiver : string
amount : number
signature: Signature
}
// ReceivedTx class 생성
export class Wallet {
public publicKey : string
public account : string
public balance : number
public signature : Signature
// 지갑 서버의 Wallet class의 속성과 한 번 비교해보자.
// 블록체인 서버에는 지갑의 개인키가 오지 않는다.
constructor( _sender : string, _signature : Signature) {
this.publicKey = _sender
this.account = this.getAccount(_sender)
this.balance = 0
// 잔고에 대한 부분은 이후 추가 구현
this.signature = _signature
}
public getAccount() : string {
return Buffer.from(this.publickey).slice(26).toString()
}
// 발신인의 공개키로 지갑 주소 탐색하는 함수
public sendTransaction ( _receivedTx : ReceivedTx ) {
const verify = Wallet.getVerify(_receivedTx)
// 서명을 검증하는 getVerify 함수를 우선 호출해 서명을 검증
if ( verify.isError ) throw new Error (verify.error)
// 에러가 있다면 에러 출력
console.log(verify.isError)
// 에러가 없다면 false가 출력될 것이다.
}
static getVerify ( _receivedTx : ReceivedTx ) : Failable < undefined, string > {
const { sender, receiver, amount, signature } = _receivedTx
const data : any[] = [ sender, receiver, amount ]
const hash : string = SHA256(data.join('')).toString()
const keyPair = ec.keyFromPublic(sender, 'hex')
const isVerify = keyPair.verify(hash, signature)
if ( isVerify == false ) {
return { isError : true, error : '서명이 올바르지 않습니다.' }
}
return { isError : false, value : undefined }
}
}
우선 전달받은 객체를 블록체인 서버의 Wallet class에서 인식할 수 있도록
지갑 서버에서 보낸 txObject와 동일한 형태를 갖는 ReceivedTx class를 선언한다.
이 tx 데이터를 가지고 우선 서명이 유효한지 확인하고 결과를 출력해보자.
(실제로는 서명 검증 후 UTXO, tx pool 등 다양한 곳을 거쳐 tx를 블록체인에 기록하게 될텐데
지금은 우선 서명을 검증하는 기능부터 구현해보고 나머지는 나중에 다른 기능을 추가하면서 추가하자.)
index.ts에서 sendTransaction 함수를 실행하면 받은 데이터를 가지고 내부에서 getVerify 함수를 실행해
서명을 검증한다.
이 과정에서 공개키 (sender라는 변수로 받은) 나 액수 등이 틀렸다면 블록체인 네트워크에서
서명을 직접 새로 만들었을 때 받은 signature값과 다른 값이 나올 것이다. 그러면 에러를 출력한다.
그렇지 않다면 isError가 false값이므로 이를 리턴 받은 sendTransaction 함수가 false를 출력해줄 것이다.
이 함수를 index의 sendTransaction 라우터에서 호출한다.
/* index.ts */
//...중략
import { Wallet, ReceivedTx } from '@core/wallet/wallet'
// ...중략
app.post('/sendTransaction', (req, res) => {
try {
const receivedTx : ReceivedTx = req.body
console.log(receivedTx)
Wallet.sendTransaction(receivedTx)
}
catch (e) {
if ( e instanceof Error ) console.error(e.message)
}
res.json([])
// tx에 대한 모든 기능이 아직 구현되지 않았으므로 일단은 빈 배열만 돌려준다
})
이렇게 추가하면 된다. 여기까지 하고 지갑을 하나 선택하고 보낼 지갑 주소와 수량을 입력후 전송하면
다음과 같은 결과물이 출력될 것이다.
지갑 서버에서 보낸 tx가 블록체인 서버에서도 나왔다면 일단 사용자와 지갑 서버, 블록체인 서버간의
데이터 전송 통로까지는 확보가 된 것이다.
다음 글에서는 본격적으로 출금과 입금 메커니즘을 구현한다.
'Blockchain' 카테고리의 다른 글
#14 transaction ch4. - transaction (0) | 2022.06.24 |
---|---|
#13 tx ch3. 채굴자 보상 (0) | 2022.06.23 |
#11 Transaction ch1.개념 (0) | 2022.06.22 |
#10 지갑, 개인키 (0) | 2022.06.20 |
#9 체인 최신화 (0) | 2022.06.17 |