만족

[Blockchain] js로 이해하는 블록체인의 트랜잭션과 채굴 보상 (Transaction & Mining Reward) 본문

[Blockchain] js로 이해하는 블록체인의 트랜잭션과 채굴 보상 (Transaction & Mining Reward)

BlockChain/이론 Satisfaction 2021. 9. 14. 20:08

이전 포스트에서 계속되는 내용이다.

 

아래 링크를 참조하여 작성된 포스트이다.

https://www.youtube.com/watch?v=kWQ84S13-hw&ab_channel=SimplyExplained 

 

트랜잭션

트랜잭션은 말 그대로 거래 내역을 의미한다.

 

코인을 생각해보면,

코인을 보내는 사람+ 코인을 받는 사람+ 전송되는 코인의 양

이라는 데이터가 트랜잭션이 된다.

 

//Transaction은 보내는 지갑주소, 받을 지갑주소, 보낸 코인의 양을 포함하는 객체이다
const Transaction = function (fromAddr, toAddr, amount) {
  this.fromAddr = fromAddr;
  this.toAddr = toAddr;
  this.amount = amount;
};

따라서 우리는 Transcation이라는 새로운 객체를 사용할 것이다.

 

//Block
//더 이상 index값은 필요하지 않다
//왜냐하면 어차피 블록들은 prevHash<->hash로 각각의 블록 위치가 결정되기 때문이다
const Block = function (timestamp, transactions, prevHash = "") {
  this.timestamp = timestamp;
  this.transactions = transactions;
  this.prevHash = prevHash;
  this.hash = this.calcHash();
  this.nonce = 0;
};

이제 블록은 data대신 transcation을 포함한다.

 

또한 전에 쓰던 index라는 값은 더 이상 필요하지 않다.

 

왜냐하면 블록체인들은 따로 순서를 명시해주지 않더라도 prevHash를 통해 서로 블록이 연결되어 있기 때문에 손쉽게 블록의 순서를 알아낼 수 있기 때문이다.

 

대기 중인 트랜잭션

트랜잭션을 생성한다고 해서 바로 반영되는 것이 아니다.

 

만약 그렇다면 거래 하나에 블록 하나가 생성되는데,

이전 포스트에서 보았듯 블록 하나를 생성하는 데는 매우 많은 컴퓨팅 파워가 요구되기 때문에

그렇게 무차별적으로 블록을 생성하는 것이 아니라

트랜잭션을 모아 두고 일정 시간마다 그 트랜잭션들을 모아 하나의 블록으로 만들어낸다.

 

//Blockchain
const Blockchain = function () {
  this.chain = [this.createGenesisBlock()];
  this.difficulty = 2;
  //새로운 block이 mining될 때 까지 트랜잭션들은 이곳에 보관된다.
  //새로운 block이 채굴되면 거래 내역들이 블록에 포함된다.
  this.pendingTransactions = [];
};
Blockchain.prototype.createTransaction = function (transcation) {
  this.pendingTransactions.push(transcation);
};

 'A가 B에게 100코인을 전송한다'는 트랜잭션은 다음과 같이 작성될 수 있다.

const testCoin= new Blockchain();
testCoin.createTransaction(new Transaction('A','B', 100));

이제 pendingTransaction을 사용해 트랜잭션을 대기시키는 작업을 구현했다.

 

채굴 보상

우리가 입력한 pendingTransaction이 실제 유효한 트랜잭션이 되기 위해서는

pendingTranscation을 블록에 넣고 조건에 맞을 때 까지 해싱하여 유효한 블록을 만들어내야 한다.

 

이를 채굴이라 하는데, 이전 포스트에서 말했듯이

이 채굴의 한 방법인 POW(Proof Of Work)는 많은 컴퓨팅 파워를 요구한다.

 

따라서 채굴자가 공짜로 채굴을 해 줄 리는 없으니,

채굴자가 채굴에 성공했을 때 보상을 주어서 '채굴을 할 이유'를 만들어주어야 한다.

 

//Blockchain
const Blockchain = function () {
  this.chain = [this.createGenesisBlock()];
  this.difficulty = 2;
  //새로운 block이 mining될 때 까지 트랜잭션들은 이곳에 보관된다.
  //새로운 block이 채굴되면 거래 내역들이 블록에 포함된다.
  this.pendingTransactions = [];
  //채굴에 성공했을 때, 채굴자에게 수여되는 코인의 양
  //채굴자가 이 값을 임의로 바꾸는 것은 가능하지만
  //매우 많은 수의 사용자들이 P2P로 연결되어 있기 때문에 값을 조작할 경우 그 값은 무시될 것이다
  this.miningRewrad = 100;
};


Blockchain.prototype.createTransaction = function (transcation) {
  this.pendingTransactions.push(transcation);
};

//채굴 (pendingTransaction을 포함하는 블록 추가)
Blockchain.prototype.minePendingTransactions = function (miningRewardAddress) {
  //예를 들어 비트코인에서는 현재 대기중인 모든 트랜잭션을 블록에 포함시키지는 않는다
  //비트코인에서 하나의 블록 사이즈는 1MB를 넘길 수 없으므로, 채굴자가 어떤 트랜잭션을 포함시킬지를 선택한다
  const block = new Block(Date.now(), this.pendingTransactions, this.getLatestBlock().hash);
  block.mining(this.difficulty);
  this.chain.push(block);

  this.pendingTransactions = [
    new Transaction(null, miningRewardAddress, this.miningRewrad),
  ];
};

// 더 이상 임의의 데이터를 블록에 추가시키는 동작을 하지 않는다
// Blockchain.prototype.addBlock = function (newBlock) {
//   //새로운 블록이 생성되면 가장 최근 블록의 해시값을 새로운 블록의 prevHash에 복사한다
//   newBlock.prevHash = this.getLatestBlock().hash;
//   newBlock.mining(this.difficulty);

//   //해시 계산이 완료되면 블록체인에 연결시킨다
//   this.chain.push(newBlock);
// };

Blockchain에 miningReward값이 새로 생겼다.

 

이 값은 채굴에 성공했을 때 채굴자에게 수여되는 코인의 양이다.

 

또한 minePendingTranscations는 기존의 addBlock을 대체하여

pendingTranscations을 포함시키는 블록을 생성하는 '채굴'을 의미한다.

 

여기서 주의할 점은, 우리는 블록 생성 시 현재 존재하는 모든 pendingTransactions를 포함시켰지만

거래가 활발한 가상화폐의 경우 단시간에 매우 많은 pendingTranscation이 생성되기 때문에

비트코인에서는 블록의 최대 크기를 1MB로 제한하고, 어떤 트랜잭션을 블록에 포함시킬지 채굴자가 선택하게 한다.

 

어떤 지갑 주소에 대한 잔액 조회

위 코드에서 보았듯이 우리는 각각의 지갑에 대한 잔액을 보관하는 데이터를 가지고 있지 않다.

 

이것은 어떤 지갑 주소에 대해 잔액을 조회하려면

모든 블록의 트랜잭션에 대해 순회하면서 잔액을 계산해야 한다는 것을 의미한다.

 

//어떤 지갑 주소에 대해 잔액을 알고 싶을 떄 이 함수를 사용한다.
//각각의 주소에 대해 잔액을 저장하지 않기 때문에 모든 트랜잭션에 대해 순회하며 잔액을 계산해야 한다
Blockchain.prototype.getBalanceOfAddress = function (addr) {
  let balance = 0;
  this.chain.map((block, idx) => {
    if (idx === 0) {
      //genesis block은 생략한다
      return;
    }
    block.transactions.map((transcation) => {
      if (transcation.toAddr === addr) {
        balance += transcation.amount;
      }
      if (transcation.fromAddr === addr) {
        balance -= transcation.amount;
      }
    });
  });
  return balance;
};

코인을 보내는 사람은 amount만큼 잔액이 줄어들고,

받는 사람은 amount만큼 잔액이 늘어나므로 개념 자체는 어렵지 않다.

 

테스트

//test
const testCoin = new Blockchain();
testCoin.createTransaction(new Transaction("addr1", "addr2", 100));
testCoin.createTransaction(new Transaction("addr2", "addr1", 50));

console.log("\nstarting mining...");
testCoin.minePendingTransactions("Jane");

console.log("\nbalance of Jane is ", testCoin.getBalanceOfAddress("Jane"));

console.log("\nstarting mining again....");
testCoin.minePendingTransactions("Jane");
console.log(testCoin.chain);

시나리오는 다음과 같다.

 

1. addr1이 addr2에게 100만큼 코인을 보낸다.

2. addr2가 addr1에게 50만큼 코인을 보낸다.

3. Jane이 채굴한다 (pendingTranscation을 포함하는 블록을 생성한다)

4. Jane의 잔액을 조회한다

5. Jane이 채굴한다 (pendingTranscation을 포함하는 블록을 생성한다)

 

3번 작업이 완료되었을 때, Jane의 잔액을 조회해보니 0이 나왔다.

 

이것이 의아할 수 있는데, 다시 한 번 minePendingTransactions를 보자.

 

//채굴 (pendingTransaction을 포함하는 블록 추가)
Blockchain.prototype.minePendingTransactions = function (miningRewardAddress) {
  //예를 들어 비트코인에서는 현재 대기중인 모든 트랜잭션을 블록에 포함시키지는 않는다
  //비트코인에서 하나의 블록 사이즈는 1MB를 넘길 수 없으므로, 채굴자가 어떤 트랜잭션을 포함시킬지를 선택한다
  const block = new Block(Date.now(), this.pendingTransactions);
  block.mining(this.difficulty);
  this.chain.push(block);

  this.pendingTransactions = [
    new Transaction(null, miningRewardAddress, this.miningRewrad),
  ];
};

위에서 보았듯, 채굴 후 바로 채굴자에게 보상을 수여하는 것이 아니라

채굴자에게 보상을 수여하는 트랜잭션은 다시 pendingTranscations에 들어가게 되어

실제 보상 수여 시점은 '다음 블록이 만들어질 때 (5번 작업이 완료되었을 때)'가 된다.

 

전체 코드

 

const SHA256 = require("crypto-js/sha256");

//Transaction은 보내는 지갑주소, 받을 지갑주소, 보낸 코인의 양을 포함하는 객체이다
const Transaction = function (fromAddr, toAddr, amount) {
  this.fromAddr = fromAddr;
  this.toAddr = toAddr;
  this.amount = amount;
};
//Block
//더 이상 index값은 필요하지 않다
//왜냐하면 어차피 블록들은 prevHash<->hash로 각각의 블록 위치가 결정되기 때문이다
const Block = function (timestamp, transactions, prevHash = "") {
  this.timestamp = timestamp;
  this.transactions = transactions;
  this.prevHash = prevHash;
  this.hash = this.calcHash();
  this.nonce = 0;
};
Block.prototype.calcHash = function () {
  //index, prevHash, timestamp, data를 입력으로 해시값을 계산한다
  return SHA256(
    this.prevHash +
      this.timestamp +
      JSON.stringify(this.transactions) +
      this.nonce
  ).toString();
};
//블록 생성
Block.prototype.mining = function (difficulty) {
  const start = new Date();
  //difficulty개의 0으로 시작하는 hash가 발생될 때 까지 해시를 반복한다
  while (
    this.hash.substring(0, difficulty) !== Array(difficulty).fill("0").join("")
  ) {
    this.nonce++;
    this.hash = this.calcHash();
  }
  const end = new Date();

  //조건을 만족했을 때 nonce 값 출력
  //이 때 nonce는 해시를 한 횟수와 동일하다
  console.log("block is mined", this.nonce);
  //걸린 시간 출력
  console.log("ellipsed time is ", end.getTime() - start.getTime(), "ms");
};

//Blockchain
const Blockchain = function () {
  this.chain = [this.createGenesisBlock()];
  this.difficulty = 2;
  //새로운 block이 mining될 때 까지 트랜잭션들은 이곳에 보관된다.
  //새로운 block이 채굴되면 거래 내역들이 블록에 포함된다.
  this.pendingTransactions = [];
  //채굴에 성공했을 때, 채굴자에게 수여되는 코인의 양
  //채굴자가 이 값을 임의로 바꾸는 것은 가능하지만
  //매우 많은 수의 사용자들이 P2P로 연결되어 있기 때문에 값을 조작할 경우 그 값은 무시될 것이다
  this.miningRewrad = 100;
};
Blockchain.prototype.minePendingTransactions = function (miningRewardAddress) {
  //예를 들어 비트코인에서는 현재 대기중인 모든 트랜잭션을 블록에 포함시키지는 않는다
  //비트코인에서 하나의 블록 사이즈는 1MB를 넘길 수 없으므로, 채굴자가 어떤 트랜잭션을 포함시킬지를 선택한다
  const block = new Block(Date.now(), this.pendingTransactions);
  block.mining(this.difficulty);
  this.chain.push(block);

  this.pendingTransactions = [
    new Transaction(null, miningRewardAddress, this.miningRewrad),
  ];
};
Blockchain.prototype.createTransaction = function (transcation) {
  this.pendingTransactions.push(transcation);
};
//어떤 지갑 주소에 대해 잔액을 알고 싶을 떄 이 함수를 사용한다.
//각각의 주소에 대해 잔액을 저장하지 않기 때문에 모든 트랜잭션에 대해 순회하며 잔액을 계산해야 한다
Blockchain.prototype.getBalanceOfAddress = function (addr) {
  let balance = 0;
  this.chain.map((block, idx) => {
    if (idx === 0) {
      //genesis block은 생략한다
      return;
    }
    block.transactions.map((transcation) => {
      if (transcation.toAddr === addr) {
        balance += transcation.amount;
      }
      if (transcation.fromAddr === addr) {
        balance -= transcation.amount;
      }
    });
  });
  return balance;
};
// 더 이상 임의의 데이터를 블록에 추가시키는 동작을 하지 않는다
// Blockchain.prototype.addBlock = function (newBlock) {
//   //새로운 블록이 생성되면 가장 최근 블록의 해시값을 새로운 블록의 prevHash에 복사한다
//   newBlock.prevHash = this.getLatestBlock().hash;
//   newBlock.mining(this.difficulty);

//   //해시 계산이 완료되면 블록체인에 연결시킨다
//   this.chain.push(newBlock);
// };
Blockchain.prototype.createGenesisBlock = function () {
  //번호 0번, 이전 해시 "0", data를 "GenesisBlock"으로 임의로 지정
  return new Block("2021/09/13", "GenesisBlock", "0");
};
Blockchain.prototype.getLatestBlock = function () {
  return this.chain[this.chain.length - 1];
};
Blockchain.prototype.isValid = function () {
  //제네시스 블록은 이전 블록이 없어 검사를 건너뛰기 위해 1부터 시작한다.
  for (let i = 1; i < this.chain.length; i++) {
    const currentBlock = this.chain[i];
    const prevHash = this.chain[i - 1].hash;

    if (currentBlock.prevHash !== prevHash) {
      //현재 블록의 이전 해시값이 일치하지 않음
      return false;
    } else if (currentBlock.calcHash() !== currentBlock.hash) {
      //현재 블록에 저장된 해시값과 다시 계산한 해시값이 일치하지 않음
      return false;
    }
  }
  return true;
};

//test
const testCoin = new Blockchain();
testCoin.createTransaction(new Transaction("addr1", "addr2", 100));
testCoin.createTransaction(new Transaction("addr2", "addr1", 50));

console.log("\nstarting mining...");
testCoin.minePendingTransactions("Jane");

console.log("\nbalance of Jane is ", testCoin.getBalanceOfAddress("Jane"));

console.log("\nstarting mining again....");
testCoin.minePendingTransactions("Jane");
console.log(testCoin.chain);

 

생각해 볼 문제

각각의 트랜잭션을 펜딩시키고, 채굴 시점에 그 트랜잭션들을 유효하게 만드는 방법에 대해 알았다.

 

그러나 A지갑의 소유자가 소유권이 없는 B지갑의 소유자의 잔액을 마음대로 사용할 수 있다는 문제가 있다.

 

단순히 createTranscation(new Transaction('B', 'A', 100000)) 처럼 사용해서 B지갑의 잔액을 A지갑으로 원하는 만큼 보낼 수 있다는 의미이다.

 

이런 문제는 어떻게 해결할 수 있을까?

 

다음 포스트에서 알아보자.

 



Comments