1. Transaction
트랜잭션이란 블록체인의 거래내역이라고 생각하면된다.
블록이 생성될때 데이터 부분에 몇번째 블록인지 문자로 담았는데 그 부부분에 트랜잭션을 담아준다.
첫번째 블록은 하드코딩 했던것처럼 트랜잭션도 첫번째 트랜잭션은 코인베이스 트랜잭션이라고 하고
마이닝 했을때 얻는 보상의 내용이 들어간다.
블록을 채굴하고 채굴한 사람이 얼마나 코인을 보상받았는지가 담기는 것이다.
트랜잭션은 객체로 이루어져있다.
UnspentTransactionOutput(UTXO) : 이 공간에 잔액을 객체로 저장
암호화폐에서 아직 사용되지 않은 거래 출력 -> 사용할수 있게 승인된 일정량의 디지털 화폐
2. 트랜잭션 구조
지갑프로그램에 계정 A와 B가 있다.
A가 B한테 10BTC의 전송을 보내면 지갑서버에서 요청을 받고 서명과 트랜잭션 데이터를 만든 후
블록체인 서버쪽에 해당 데이터을 전송하여 블록체인 서버에서 트랜잭션 객체를 만든다.
트랜잭션 객체가 생겼을때 UTXO배열 안에서 unspentTxOut 객체를 제거하고
트랜잭션을 통해 새로 생긴 unspentTxOut 객체들을 UTXO에 추가 시켜주는 작업 후 UTXO 내용을 업데이트 한다.
트랜잭션 풀이라는 공간을 만들고 트랜잭션 객체가 생성되었을때 트랜잭션을 트랜잭션 풀에 담아준다.
그리고 블록체인 네트워크 상에 연결된 노드들은 트랜잭션 풀의 내용을 공유한다.
블록 마이닝 할때 트랜잭션 풀 안에 있는 트랜잭션 객체를 사용해 새로 생성되는 블록의 data 속성값으로 넣어준다.
그래서 블록이 마이닝 될때 트랜잭션 풀을 업데이트 해주는 부분이 필요하다.
노드들이 풀의 내용을 공유하는 부분은 트랜잭션 객체가 생겼을때 트랜잭션 객체를 브로드캐스트 하는 방식이다.
3. 트랜잭션 예시
첫번째 트랜잭션(코인베이스 트랜잭션)
Tx 0000 = {
input : {},
output : {주소 : 타인,얻은코인 : 50}
}
UTXO = [{주소 : 타인, 얻은코인 : 50, 참조 트랜잭션 : Tx0000}]
UTXO 안에 있는 객체를 수정하는것이 아니라 삭제하고 새로운 객체를 넣어주는것이다.
두번째 트랜잭션
타인이 나에게 1코인을 보낸 트랜잭션
Tx 0001 = {
input : {주소 : 타인, 코인 : 50, 참조 트랜잭션 : Tx0000},
output : [{주소 : 나, 코인 : 1},{주소 : 타인, 코인 : 49}]
}
기존 UTXO 제거 후
UTXO = [{주소 : 나, 코인 : 1 , 참조트랜잭션 : Tx00001},{주소 : 타인, 코인 : 49, 참조트잭 : Tx0001}]
위와 같이 새로운 객체를 넣어준다.
세번째 트랜잭션
타인이 나에게 20코인을 보냈다.
Tx 0002 = {
input : {주소 : 타인, 코인 :49, 참조 트랜잭션 : Tx0001},
output : [{주소 : 나, 코인 : 20},{주소: 타인, 코인 : 29}]
}
UTXO = [{주소 : 나, 코인 : 1}, {주소 : 나, 코인 : 20}, {주소 : 타인, 코인 29}]
UTXO라는 공간은 위처럼 트랜잭션의 내용을 구성해서 거래 내용을 기록한다.
네번째 트랜잭션
Tx 0003 = {
input : [{주소 : 나, 코인 :1, 참조 트랜잭션 : Tx0001},{주소 : 나, 코인 : 20, 참조 트랜잭션 : Tx0002}],
output : [{주소 : 나, 코인 : 21}]
UTXO = [{주소 : 타인, 코인 : 29, 참조 트랜잭션 : Tx 0002},{주소 : 타인, 코인 : 21, 참조 트랜잭션 : Tx0003}]
4. /@types/transaction.d.ts
트랜잭션을 만들어주기 위해 타입을 먼저 선언한다.
declare interface ITxOut {
account: string; // 해당 사람의 주소
amount: number; // 잔액
}
// ITxIn은 IUnsepentTxOut[]을 참조해서 만들고
// ITxIn을 만들때 IUspentTxOut[]에서 삭제
// input
declare interface ITxIn {
txOutId: string; // ITransaction 객체의 hash값
txOutIndex: number; // ITransaction에 있는 txOuts 배열의 인덱스
// signature? : signature 속성이 없어도되고 있어도 된다.
signature?: string;
}
// 트랜잭션
declare interface ITransaction {
hash: string; // txIns, txOuts를 사용해서 만든 hash 값
txOuts: ITxOut[];
txIns: ITxIn[];
}
// TxOut을 만들때 IUnspentTxOut[]에 생성
// UTXO
declare interface IUnspentTxOut {
txOutId: string;
txOutIndex: number;
account: string;
amount: number;
}
5. /src/core/transaction/txout.ts
// txOut 객체를 만들 클래스
import { Wallet } from "@core/wallet/wallet";
export class TxOut implements ITxOut {
public account: string; // 주소
public amount: number; // 코인
constructor(account: string, amount: number) {
this.account = account;
this.amount = amount;
}
// 인자값 : 보내는 계정, 받는 계정, 합, 보낼 코인갯수
// txOuts 배열을 추가할 함수
static createTxOuts(sum: number, receivedTx: any): TxOut[] {
// receivedTx.amount = 보낼 금액
// receivedTx.sender = 보내는 사람의 공개키
// receivedTx.received = 받는 사람 계정
const { amount, sender, received } = receivedTx;
const senderAccount: string = Wallet.getAccount(sender);
// 받는사람 txOut
const receivedTxOut = new TxOut(received, amount);
// 보내는 사람 txOut
// sum 보내는 사람의 코인 합
const senderTxOut = new TxOut(senderAccount, sum - amount);
// 보내는 사람의 코인이 0보다 작거나 같으면
if (senderTxOut.amount <= 0) return [receivedTxOut];
return [receivedTxOut, senderTxOut];
}
}
6. /src/core/transaction/txin.ts
// txIn 객체를 생성해줄 클래스
export class TxIn implements ITxIn {
public txOutId: string;
public txOutIndex: number; // 배열의 인덱스값(첫 트잭의 경우 블록의 높이)
public signature?: string; //첫 트랜잭션은(코인베이스 트랜잭션) 서명이 없으니
constructor(txOutId: string, txOutIndex: number, signature?: string) {
this.txOutId = txOutId;
this.txOutIndex = txOutIndex;
this.signature = signature;
}
static createTxIns(receivedTx: any, myUTXO: IUnspentTxOut[]) {
let sum = 0;
let txins: TxIn[] = [];
for (let i = 0; i < myUTXO.length; i++) {
const { txOutId, txOutIndex, amount } = myUTXO[i];
const item: TxIn = new TxIn(txOutId, txOutIndex, receivedTx.signature);
txins.push(item);
sum += amount;
5;
if (sum >= receivedTx.amount) return { sum, txins };
}
return { sum, txins };
}
}
7. /src/core/transaction/unspentTxOut.ts
// unspentTxOut 생성 클래스
export class UnspentTxOut implements IUnspentTxOut {
public txOutId: string; // transaction 객체의 hash 값
public txOutIndex: number; // transaction객체에서 txOuts 배열의 인덱스 값
public account: string;
public amount: number;
constructor(
txOutId: string,
txOutIndex: number,
account: string,
amount: number
) {
this.txOutId = txOutId;
this.txOutIndex = txOutIndex;
this.account = account;
this.amount = amount;
}
// UTXO를 가져오는 함수
// 인자값 전체 UTXO, 내계정
static getMyUnspentTxOuts(
account: string,
unspentTxOut: UnspentTxOut[]
): UnspentTxOut[] {
return unspentTxOut.filter((utxo: UnspentTxOut) => {
return utxo.account === account;
});
}
}
// UnspentTxOut 객체를 만들때 txOut객체 안에 있는 내용으로 만든다.
// UnspentTxOut 객체 안에 있는 account 속성과 amount 속성은 txOut 객체의 내용으로 구성
8. /src/core/transaction/transaction.ts
// transaction 객체 생성 클래스
import { TxIn } from "./txin";
import { TxOut } from "./txout";
import { UnspentTxOut } from "./unspentTxOut";
import { SHA256 } from "crypto-js";
export class Transaction implements ITransaction {
public hash: string;
public txIns: ITxIn[];
public txOuts: ITxOut[];
constructor(txIns: TxIn[], txOut: TxOut[]) {
this.txIns = txIns;
this.txOuts = txOut;
this.hash = this.createTransactionHash();
}
// 인스턴스 생성하고 만들어 진다.
createTransactionHash(): string {
// 배열의 값들을 뽑아서 하나의 문자열로 바꿔줌
const txOutContent: string = this.txOuts
.map((v) => Object.values(v))
.join("");
const txInContent: string = this.txIns
.map((v) => Object.values(v))
.join("");
// 트랜잭션의 해시값을 만들어주고
return SHA256(txOutContent + txInContent).toString();
}
createUTXO(): UnspentTxOut[] {
const utxo: UnspentTxOut[] = this.txOuts.map(
(txout: TxOut, index: number) => {
return new UnspentTxOut(this.hash, index, txout.account, txout.amount);
}
);
return utxo;
}
// 트랜잭션을 만들어 주는 함수
// 트랜잭션 만들때 내 계정과 일치하는 UTXO 필요
static createTransaction(
receivedTx: any,
myUTXO: UnspentTxOut[]
): Transaction {
const { sum, txins } = TxIn.createTxIns(receivedTx, myUTXO);
const txouts: TxOut[] = TxOut.createTxOuts(sum, receivedTx);
const tx = new Transaction(txins, txouts);
return tx;
}
}
9. 데이터 타입 수정 및 함수 추가
transaction 을 블록의 데이터로 넣기 때문에 기존 string[] 로 정해놨던 타입을 ITransaction[] 로 다 바꾸었다.
(1) /@types/Block.d.ts
declare interface IBlock extends IBlockHeader {
merkleRoot: string;
hash: string;
nonce: number;
difficulty: number;
data: string[];
}
declare interface IBlock extends IBlockHeader {
merkleRoot: string;
hash: string;
nonce: number;
difficulty: number;
data: ITransaction[];
}
(2) /src/core/config.ts
export const GENESIS: IBlock = {
version: "1.0.0",
height: 0,
timeStamp: new Date().getTime(),
hash: "0".repeat(64),
previousHash: "0".repeat(64),
merkleRoot: "0".repeat(64),
difficulty: 0,
nonce: 0,
data: [
"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks",
],
};
export const GENESIS: IBlock = {
version: "1.0.0",
height: 0,
timeStamp: new Date().getTime(),
hash: "0".repeat(64),
previousHash: "0".repeat(64),
merkleRoot: "0".repeat(64),
difficulty: 0,
nonce: 0,
data: [],
}
(3) /src/core/blockChain/block.ts
export class Block extends BlockHeader implements IBlock {
public hash: string;
public merkleRoot: string;
public nonce: number;
public difficulty: number;
public data: string[];
// 생성 단계에 Block클래스 타입의 이전 블록을 매개변수로 받고
// 블록의 내용 _data
// 10번째 전 블록 : Block타입의 _adjustmentBlock
constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block)
export class Block extends BlockHeader implements IBlock {
public hash: string;
public merkleRoot: string;
public nonce: number;
public difficulty: number;
public data: ITransaction[];
// 생성 단계에 Block클래스 타입의 이전 블록을 매개변수로 받고
// 블록의 내용 _data
// 10번째 전 블록 : Block타입의 _adjustmentBlock
constructor(
_previousBlock: Block,
_data: ITransaction[],
_adjustmentBlock: Block
)
public static generateBlock(
_previousBlock: Block,
_data: string[],
_adjustmentBlock: Block
)
public static generateBlock(
_previousBlock: Block,
_data: ITransaction[],
_adjustmentBlock: Block
)
(4) /src/core/wallet/wallet.ts
this.account = this.getAccount();
this.account = Wallet.getAccount(this.publicKey);
// 지갑의 주소
public getAccount(): string {
// Buffer에 있는 동안 바이너리 데이터를 조작할수 있기 때문
return Buffer.from(this.publicKey).slice(26).toString();
// 지갑의 주소
static getAccount(publicKey: string): string {
// Buffer에 있는 동안 바이너리 데이터를 조작할수 있기 때문
return Buffer.from(publicKey).slice(26).toString();
}
// 코인 보내는 사람의 잔액을 확인하기 위한 함수
static getBalance(account: string, unspentTxOuts: IUnspentTxOut[]): number {
return unspentTxOuts
.filter((v) => v.account === account)
.reduce((acc, utxo) => {
return acc + utxo.amount;
}, 0);
// 남아있는 잔액을 확인하고 확인한 잔액으로 보낼수 있는지 확인 하기 위해서
}
기존 public 을 static으로 바꿔주고 getBalance함수를 추가했다.
(5) /src/core/blockChain/chain.ts
import { Block } from "@core/blockChain/block";
import { DIFFICULTY_ADJUSTMENT_INTERVAL } from "@core/config";
import { Transaction } from "@core/transaction/transaction";
import { TxIn } from "@core/transaction/txin";
import { TxOut } from "@core/transaction/txout";
import { UnspentTxOut } from "@core/transaction/unspentTxOut";
import { BlockList } from "net";
// 블록의 체인
export class Chain {
// blockchain Block배열의 타입을 가진 변수
private blockChain: Block[];
private unspentTxOuts: IUnspentTxOut[];
private transactionPool: ITransaction[];
// 블록체인에 최초 블럭 넣어두기
// 처음에 생성될때 constructor로 클래스를 동적할당으로 생성 했을때
constructor() {
// 최초의 블록은 거의 하드 코딩으로 넣어준다.
// 생성 될때 최초 블록 배열에 추가(블록 체인에 최초 블록추가)
this.blockChain = [Block.getGENESIS()];
// UTXO라는 배열을 만들어줌
this.unspentTxOuts = [];
this.transactionPool = [];
}
// 트랜잭션 풀을 반환
public getTransactionPool(): ITransaction[] {
return this.transactionPool;
}
// 트랜잭션 풀에 트랜잭션 추가 함수
public appendTransactionPool(transaction: ITransaction) {
this.transactionPool.push(transaction);
}
// 트랜잭션 풀 업데이트
public updateTransactionPool(newBlock: IBlock) {
let txPool: ITransaction[] = this.getTransactionPool();
newBlock.data.forEach((tx: ITransaction) => {
txPool = txPool.filter((txp) => txp.hash !== tx.hash);
});
this.transactionPool = txPool;
}
// UTXO get 함수(UTXO 조회 함수)
public getUnspentTxOuts(): IUnspentTxOut[] {
return this.unspentTxOuts;
}
// UTXO 추가함수
public appendUTXO(utxo: IUnspentTxOut[]) {
// unspentTxOuts 배열에 utxo 값을 복사해서 배열에 추가
this.unspentTxOuts.push(...utxo);
}
// 마이닝 블록
public miningBlock(account: string): Failable<Block, string> {
// 코인베이스 트랜잭션의 내용을 임의로 만든것
const txIn: ITxIn = new TxIn("", this.getLatestBlock().height + 1);
const txOut: ITxOut = new TxOut(account, 50);
const coinbaseTransaction: Transaction = new Transaction([txIn], [txOut]);
// createUTXO 함수로 UTXO에 담을 객체를 만들어 준것
const utxo = coinbaseTransaction.createUTXO();
// UTXO에 appendUTXO함수로 만든 객체를 추가
this.appendUTXO(utxo);
return this.addBlock([coinbaseTransaction]);
}
// 현재 연결된 블록들 리스트를 확인하기 위해 getChain함수로
// 연결된 노드들을 확인할수 있는 함수
public getChain(): Block[] {
return this.blockChain;
}
// 현재 연결된 블록들의 갯수 길이
// 연결된 노드의 갯수
public getLength(): number {
return this.blockChain.length;
}
// 맨 마지막 블록을 확인하는 함수
// 맨 마지막 노드 확인
public getLatestBlock(): Block {
return this.blockChain[this.blockChain.length - 1];
}
// 블록 체인에 블록을 추가하는 함수 매개변수로는 블록의 내용을 받는다.
// 반환값으로는 Failable<Block, string> Block 타입이랑 string타입을 반환 할수 있는 함수
// 반환 값은 객체이고 {isError: true, value : 여기 우리가 설정한 타입}
// value라는 키값에 Block 타입이나 string 타입을 허용한다.
public addBlock(data: ITransaction[]): Failable<Block, string> {
// 마지막 블럭 가져오기
const previousBlock = this.getLatestBlock();
// 10번째 전 블록 가져오기
const adjustmentBlock: Block = this.getAdjustmentBlock();
// 마이닝이 끝난 추가할 블록 가져오기
const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
// 새로운 블록을 이전 블록과 검증을 하는데 isValidNewBlock 이전블록(마지막 블록)과 새로운 블록을
// 매개변수로 전달해서 블록의 높이, 해시 검사
const isValid = Block.isValidNewBlock(newBlock, previousBlock);
// 블록검증 에러시 에러 반환
if (isValid.isError) return { isError: true, value: "블록 추가 에러" };
// 다 통과 후 블록체인에 추가
this.blockChain.push(newBlock);
// 에러가 없다고 알려주고 value로 Block타입의 newBlock반환
return { isError: false, value: newBlock };
}
// 체인 검증 코드
public isValidChain(chain: Block[]): Failable<undefined, string> {
// 최초 블록 검사 하는 코드
// const genesis = chain[0];
//체인의 유효성 검사
for (let i = 0; i < chain.length; i++) {
// 현재 인덱스의 블록
const newBlock = chain[i];
// 그 인덱스의 이전블록
const previousBlock = chain[i - 1];
// 블록 검증 시도
const isValid = Block.isValidNewBlock(newBlock, previousBlock);
// 검사 오류시
if (isValid.isError) return { isError: true, value: isValid.value };
}
// 다 통과하면 패스
return { isError: false, value: undefined };
}
// replaceChain 본인과 상대방의 체인을 검사하는 함수
public replaceChain(receivedChain: Block[]): Failable<undefined, string> {
// 본인 체인과 상대방 체인을 검사하는 함수
// 상대방
const latestReceivedBlock: Block = receivedChain[receivedChain.length - 1];
// 본인
const latestBlock: Block = this.getLatestBlock();
// 높이가 0 받은블록이 최초라서 검사할 필요가 없고
if (latestReceivedBlock.height === 0)
return { isError: true, value: "받은 블럭이 최초 블럭" };
// 본인의 블록이 더 길거나 같은 블록이면 검사 필요가 없다.
if (latestReceivedBlock.height <= latestBlock.height)
return { isError: true, value: "본인의 블럭보다 길거나 같은 블럭" };
// 여러명이 빠르게 추가하다보면 검증단계에서 블록의 길이 차이가 있다.
// 해시 비교를 해서 이전 블록의 해시가 같다면 블록의 갯수 차이가 있는것으로
// 검사 에러 이 문제는 사람들이 빠르게 블록을 추가하다가 여러명이 비슷하게 블록
// 생성이 되었을때 일어남
if (latestReceivedBlock.previousHash === latestBlock.hash)
return { isError: true, value: "블럭이 하나 모자라다." };
// 본인의 체인이 더 짧다하면 체인 갱신 해줌
// 체인을 갱신해줌
this.blockChain = receivedChain;
return { isError: false, value: undefined };
}
// 11.03
// 생성 시점으로 블록 높이 -10인 블록 구하기
// 현재 높이값 < DIFFICULTY_ADJUSTMENT_INTERVAL : 최초 블록 반환
// 현재 높이값 > DIFFICULTY_ADJUSTMENT_INTERVAL : -10번째 블록 반환
public getAdjustmentBlock() {
// 자신의 길이를 getLength함수로 가져오고
// 자신의 길이가 난이도조절(10개)보다 작으면 최초의 블럭 반환
// 길이가 10보다 크면 -10번째 블럭을 반환해준다.
const currentLength = this.getLength();
const adjustmentBlock: Block =
this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
? Block.getGENESIS()
: this.blockChain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
return adjustmentBlock; // 최초블록 or -10번째 블록 반환
}
updateUTXO(tx: ITransaction) {
// txOutId, txOutIndex, account, amount
// UTXO 배열을 가져오고 getUnspnetTxOuts 함수를 사용해서
const unspentTxOuts: UnspentTxOut[] = this.getUnspentTxOuts();
// UTXO에 추가할 unspentTxOuts 객체를 생성
// 트랜잭션 객체의 배열안에 있는 txOut객체를 사용해서 새로 생성될
// UnspentTxOut 객체를 만들어준다.
const newUnspentTxOuts = tx.txOuts.map((txout, index) => {
return new UnspentTxOut(tx.hash, index, txout.account, txout.amount);
});
// filter로 unspentTxOuts 배열 안에서 txOut객체들은 제거하고 생성된 newUnspentTxOuts배열을 붙여준다.
const tmp = unspentTxOuts
.filter((v: UnspentTxOut) => {
const bool = tx.txIns.find((value: TxIn) => {
return (
value.txOutId === v.txOutId && v.txOutIndex === value.txOutIndex
);
});
// !undefined == true
// !{} == false
return !bool;
})
.concat(newUnspentTxOuts);
// UTXO에 사용한 unspentTxOut 객체 제거와 생성된 unspnetTxOuts 객체를 UTXO를 추가
// tmp배열을 reduce함수로 acc 배열 안에서 해당 조건에 맞는 값을 내보내주고
// acc 배열에 push해서 배열에 넣어주고 acc배열을 반환해서
// unspentTmp에 담고 this.unspentTxOuts에 바인딩
let unspentTmp: UnspentTxOut[] = [];
const result = tmp.reduce((acc, uxto) => {
const find = acc.find(({ txOutId, txOutIndex }) => {
return txOutId === uxto.txOutId && txOutIndex === uxto.txOutIndex;
});
if (!find) acc.push(uxto);
return acc;
}, unspentTmp);
this.unspentTxOuts = result;
}
}
chain.ts 안에 트랜잭션을 넣어주기 위한 함수들과 타입들을 추가했다.
10. test
.test.ts 파일을 만들고 테스트 해본다.
describe("UTXO 테스트", () => {
let node: Chain = new Chain();
it("miningBlock() 함수", () => {
for (let i = 0; i < 10; i++) {
node.miningBlock(
"7C91020020680EFBE45B775942839479739294C686E15D70AD824E16822D7424"
);
console.log(node.getLatestBlock().data);
console.log(node.getUnspentTxOuts());
}
});
});
'개발 > BlockChain' 카테고리의 다른 글
[BlockChain] RPC web3 테스트 (0) | 2022.11.21 |
---|---|
[BlockChain] ubuntu 환경 설정 (0) | 2022.11.21 |
[BlockChain] TypeScript로 지갑 만들기 (0) | 2022.11.11 |
[BlockChain] TypeScript로 P2P 구현 (0) | 2022.11.07 |
[BlockChain] TypeScript로 체인 만들기 (0) | 2022.11.03 |