본문 바로가기
프로젝트/자바로 만드는 블록체인

[블록체인] 자바로 내 첫 번째 블록체인을 만들어보자 - Part 3. /Java Block Chain / 자바 블록체인

by 으노으뇨 2022. 12. 8.
728x90
반응형
SMALL

안녕하세요~~~!!! 

이번에 포스팅을 할 주제는! 자바로 블록체인을 구현해보고 공부해보는 시간 제 3탄입니다!!!

본 포스팅은 블록체인을 만들어보는 포스팅의 마지막 (파트 3) 이며 지난 

블록체인에 대한 기초적인 생성 과정 - 파트1 https://uno-kim.tistory.com/252
 

[블록체인] 자바로 내 첫 번째 블록체인을 만들어보자 - Part 1. / Java BlockChain / 자바 블록체인

안녕하세요~!!! 이번 포스팅으로는 자바로 블록체인을 만들어보고 학습내용을 정리해 보겠습니다! 지금이 튜토리얼 시리즈는 블록체인 기술이라던가 블록체인 개발에 대해서 조금 도움과 이해

uno-kim.tistory.com

지갑과 트랜잭션에 대한 기초 설명 - 파트2 https://uno-kim.tistory.com/259
 

[블록체인] 자바로 내 첫 번째 블록체인을 만들어보자 - Part 2. /Java Block Chain / 자바 블록체인

안녕하세요~~~!!! 이번에 포스팅을 할 주제는! 자바로 블록체인을 구현해보고 공부해보는 시간 제 2탄입니다!! 본 포스팅은 블록체인 관련된 두번째 이므로 첫 번째 포스팅을 참고해주세요! https:/

uno-kim.tistory.com

우선 다른 포스팅을 먼저 접그하시고 보시면 될것같습니다. 

이번 포스팅에 대해서 조금 더 설명을 드리자면

주된 기능은

  • 어떻게 암호화폐의 소유권이 결정되는가?
  • 트랜잭션에 대해서 블록들은 어떻게 처리되는가?
  • 지갑에 암호화폐를 넣고 거래하고 확인하자

입니다!!! 자바로 블록체인을 만들어보는 마지막 포스팅 시작하겠습니다!!


1. 어떻게 암호화폐의 소유권이 결정되는가?

우리가 비트코인 하나를 소유하기 위해서 , 우리는 비트코인 하나를 누군가로부터 받아야합니다. 사거나?ㅎㅎ

이 장부는 내 코인이 하나가 추가가 되면서 다른사람의 코인이 1개를 감소시키는 그런형태의 거래가 아니라, 주는 사람도 누군가에게 받은 코인이 있을텐데 그런 기록들을 저에게 넘기는 형태로 제가 1비트코인이 생겼다 이렇게 소유하게되는것 같습니다. 누군가로부터 전달받은 코인을 결국 또 내가 전달 받는다. 이런개념인거죠

우리의 지갑 잔액은 결국 내 지갑주소에서 누군가에게 '보내지않은' 합계가 출력되는 것이다.

지금부터 우리는 코인의 행적을 추적하고, 사용되지 않은 트랜잭션 output 들을 UTXO라고 하겠습니다.

따라서 지난 포스팅에서 만든 TransactionInput 클래스는 아직 사용되지 않은 것들을 기록하는 클래스입니다. 
TransactionInputId 라는 변수에는 그에 대응하는 TransactionInput을찾는데이용될것이며,시스템이 소유권을 체크하는데 이용될 것이다. 
package Java.BlockChain;

import java.security.PublicKey;

public class TransactionOutput
{
	private String id;
	private PublicKey reciepient; // 이 코인의 새 주인
	private float value; // 잔액
	private String parentTransactionId; // 생성된 트랜잭션의 ID
	// Constructor
	public TransactionOutput(PublicKey reciepient, float value, String parentTransactionId)
	{
		this.reciepient = reciepient;
		this.value = value;
		this.parentTransactionId = parentTransactionId;
		this.id = BlockUtils.applySha256(BlockUtils.getStringFromKey(reciepient) + Float.toString(value) + parentTransactionId);
	}
	// 동전이 내껀지 확인
	public boolean isMine(PublicKey publicKey)
	{
		return (publicKey == reciepient);
	}
	public String getId()
	{
		return id;
	}
	public void setId(String id)
	{
		this.id = id;
	}
	public PublicKey getReciepient()
	{
		return reciepient;
	}
	public void setReciepient(PublicKey reciepient)
	{
		this.reciepient = reciepient;
	}
	public float getValue()
	{
		return value;
	}
	public void setValue(float value)
	{
		this.value = value;
	}
	public String getParentTransactionId()
	{
		return parentTransactionId;
	}
	public void setParentTransactionId(String parentTransactionId)
	{
		this.parentTransactionId = parentTransactionId;
	}
}

트랜잭션 출력은 트랜잭션으로 부터 각 당사자들에게 최종 금액을 보여주게 됩니다. 이 정보들은 새로운 거래에서 참조될때 보낼수 있는 잔액이 남아있다느 것을 확인 하는 용도로 사용됩니다.

거의다 끝나간다..!?

 

2. 트랜잭션 처리

블록체인의 블록들은 많은 트랜잭션을 수신할 수 있고, 블록체인은 매우 길 수 있으며, 입력된 것들을 찾ㄱ조 확인해야 하기에 새로운 트랜잭션을 처리하는데 더 오랜시간이 걸립니다. 

이 문제를 해결하기 위해 사용되지 않은 모든 트랜잭션의 추가 컬렉션을 두어 입력값으로 사용되게끔 할 것입니다.

UnoChain 클래스에는 모든 UTXO 에 대한 컬렉션을 추가하겠습니다.

HashMap<String, TransactionOutputs> UTXOs = new HashMap<String, TransactionOutputs>();

한줄을 추가해줍니다.

이제 트랜잭션 클래스 안에 있는 트랜잭션처리하는 메소드 processTransaction 메소드를 boolean 타입으로 만들어보겠습니다.

	
	public boolean processTransaction(UnoChain unoChain, float minimumTransaction)
	{
		if (verifiySignature() == false)
		{
			System.out.println("#Transaction Signature failed to verify");
			return false;
		}
		// 트랜잭션 입력을 수집 (사용되지 않았는지 확인)
		for (TransactionInput i : this.inputs)
		{
			i.setUTXO(unoChain.UTXOs.get(i.getTransactionOutputId()));
		}
		// 트랜잭션이 유효한지 확인
		if (getInputsValue() < minimumTransaction)
		{
			System.out.println("#Transaction Inputs to small: " + getInputsValue());
			return false;
		}
		// 트랜잭션 출력 생성
		float leftOver = getInputsValue() - this.value;
		// 입력값을 받은 다음 변화
		this.transactionId = calulateHash();
		// 수신자에게 값을 보내다
		this.outputs.add(new TransactionOutput(this.reciepient, this.value, this.transactionId));
		// 잔돈을 보낸 사람에게 돌려주다
		this.outputs.add(new TransactionOutput(this.sender, leftOver, this.transactionId)); //
		// 사용 안 함 목록에 출력
		for (TransactionOutput o : this.outputs)
		{
			unoChain.UTXOs.put(o.getId(), o);
		}
		// UTXO 목록에서 사용된 트랜잭션 입력을 제거
		for (TransactionInput i : this.inputs)
		{
			// 트랜잭션을 찾을 수 없는 경우 건너뜀
			if (i.getUTXO() == null)
			{
				continue;
			}
			unoChain.UTXOs.remove(i.getUTXO().getId());
		}
		return true;
	}
소스만보면 조~~~금 어지러울수 있습니다. 우선 차근차근 설명드리겠습니다.
소스는 우선 트랙잭션 처리를 하는 과정입니다.
매개변수로 거래를 할 대상 (UnoChain) 그리고 최소 트랜잭션 float로 받아옵니다.
우선 거래전 유효성 검사를 먼저합니다.
그리고 TransactionInput 값들을 리스트형태로 생성하는데 UnoChain에서 셋팅된 UTXOs의 트랜잭션아웃풋아이디를 통하여 생성합니다. 그렇게 생성된 리스트는 inputs입니다.
그리고 그 트랜잭션이 유효한지 확인합니다. 입력합계 inputs를 루프를 돌려 확인합니다.
그리고 트랜잭션 출력을 생성하고 해시를 만들어서 그것을 거래 아이디로 지정합니다.
그리고 수신자와 에게 값을 보내고, 보낸사람에겐 잔돈과 결과를 보내줍니다.

이 방법을 사용하여 트랜잭션이 유효한지 확인하기 위해 검사를 수행합니다. 

중요한 것은 마지막으로 UTXO 목록에서 인풋을 삭제하는 것입니다. 즉, 트랜잭션은

한번 입력으로 사용할 수 있습니다. 따라서 입력의 전체 값을 사용 해야하므로 발신자는 다시 자신에게 보냅니다.

빨간색 화살표는 출력입니다. 녹색 입력은 이전 출력에 대한 참조

마지막으로 지갑클래스를 다음과 같이 수정합니다.

  • 잔액을 수집하는 것을 추가
  • 트랜잭션이 생성되도록 추가
	// 잔액을 반환하고 이 지갑이 소유한 UTXO를 저장
	public float getBalance(UnoChain uno)
	{
		float total = 0;
		for (Map.Entry<String, TransactionOutput> item : uno.UTXOs.entrySet())
		{
			TransactionOutput UTXO = item.getValue();
			if (UTXO.isMine(publicKey))
			{
				// 코인이 내 것인 경우 사용되지 않은 거래 목록에 추가
				UTXOs.put(UTXO.getId(), UTXO);
				total += UTXO.getValue();
			}
		}
		return total;
	}
	// 이 지갑에서 새 트랜잭션을 생성하고 반환
	public Transaction sendFunds(UnoChain uno, PublicKey _recipient, float value)
	{
		if (getBalance(uno) < value)
		{ // 잔금을 모음
			System.out.println("#Not Enough funds to send transaction. Transaction Discarded.");
			return null;
		}
		// 입력 배열 목록 생성
		ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
		float total = 0;
		for (Map.Entry<String, TransactionOutput> item : UTXOs.entrySet())
		{
			TransactionOutput UTXO = item.getValue();
			total += UTXO.getValue();
			inputs.add(new TransactionInput(UTXO.getId()));
			if (total > value) break;
		}
		Transaction newTransaction = new Transaction(publicKey, _recipient, value, inputs);
		newTransaction.generateSignature(privateKey);
		for (TransactionInput input : inputs)
		{
			UTXOs.remove(input.getTransactionOutputId());
		}
		return newTransaction;
	}

거래 내역을 기록하는 것과 같은 다른 기능을 지갑에 자유롭게 추가해도됩니다. 나중에 추가해보곘습니다!!ㅎㅎㅎ

3. 블럭에 트랜잭션 집어넣기

이제 작동하는 트랜잭션 시스템을 만들었으니, 이 시스템을 어디 한번 실행해볼까요?

실행을 위해서 트랜잭션들이 머클루트 트리로 사용되어야 합니다. 따라서 BlockUtils에 새로운 매서드를 만들어보겠습니다.

바로 트랜잭션리스트들을 머클루트로 변환하는 작업입니다.

	public static String getMerkleRoot(ArrayList<Transaction> transactions)
	{
		int count = transactions.size();
		ArrayList<String> previousTreeLayer = new ArrayList<String>();
		for (Transaction transaction : transactions)
		{
			previousTreeLayer.add(transaction.getTransactionId());
		}
		ArrayList<String> treeLayer = previousTreeLayer;
		while (count > 1)
		{
			treeLayer = new ArrayList<String>();
			for (int i = 1; i < previousTreeLayer.size(); i++)
			{
				treeLayer.add(applySha256(previousTreeLayer.get(i - 1) + previousTreeLayer.get(i)));
			}
			count = treeLayer.size();
			previousTreeLayer = treeLayer;
		}
		String merkleRoot = (treeLayer.size() == 1) ? treeLayer.get(0) : "";
		return merkleRoot;
	}

이제 오랜만에 우리  Block 블록 클래스를 조금 수정해 줍니다.

//추가된 변수
	private String merkleRoot;
	private ArrayList<Transaction> transactions = new ArrayList<Transaction>();

//바뀐 생성자
	public Block(String data, String previousHash)
	{
		this.previousHash = previousHash;
		this.timeStamp = new Date().getTime();
		this.hash = calculateHash();
	}

	//채굴관련 메서드도 이제 머클루트를 이용해서 바뀌는 것으로 수정
    public void mineBlock(int difficulty) {
		merkleRoot = StringUtil.getMerkleRoot(transactions);
		String target = StringUtil.getDificultyString(difficulty); //Create a string with difficulty * "0" 
		while(!hash.substring( 0, difficulty).equals(target)) {
			nonce ++;
			hash = calculateHash();
		}
		System.out.println("Block Mined!!! : " + hash);
	}

	// 블록에 트랜잭션 추가
	public boolean addTransaction(UnoChain uno, float minimumTransaction, Transaction transaction)
	{
		// 거래를 처리하고 유효한지 확인하고, 블록이 생성 블록이 아니라면 무시
		if (transaction == null) return false;
		if ((this.previousHash != "0"))
		{
			if ((transaction.processTransaction(uno, minimumTransaction) != true))
			{
				System.out.println("Transaction failed to process. Discarded.");
				return false;
			}
		}
		this.transactions.add(transaction);
		System.out.println("Transaction Successfully added to Block");
		return true;
	}

그래서 밑의 접은글을 펼치면 전체 소스가 나옵니다.

더보기
package Java.BlockChain;

import java.util.ArrayList;
import java.util.Date;

class Block
{
	private String hash; // 해시값
	private String previousHash; // 이전 해시값
	private long timeStamp;
	private int nonce;
	private String merkleRoot;
	private ArrayList<Transaction> transactions = new ArrayList<Transaction>();
	
	public Block(String data, String previousHash)
	{
		this.previousHash = previousHash;
		this.timeStamp = new Date().getTime();
		this.hash = calculateHash();
	}
	
	public String calculateHash()
	{
		return BlockUtils.applySha256(this.previousHash + Long.toString(this.timeStamp) + Integer.toString(this.nonce) + this.merkleRoot);
	}
	
	public void mineBlock(int difficulty)
	{
		this.merkleRoot = BlockUtils.getMerkleRoot(this.transactions);
		String tmpHash = this.hash;
		while (!tmpHash.substring(0, difficulty).equals(BlockUtils.getDificultyString(difficulty)))
		{
			this.nonce++;
			tmpHash = this.calculateHash();
		}
		this.hash = tmpHash;
		System.out.println("Block Mined!!! : " + this.hash);
	}
	// 블록에 트랜잭션 추가
	public boolean addTransaction(UnoChain uno, float minimumTransaction, Transaction transaction)
	{
		// 거래를 처리하고 유효한지 확인하고, 블록이 생성 블록이 아니라면 무시
		if (transaction == null) return false;
		if ((this.previousHash != "0"))
		{
			if ((transaction.processTransaction(uno, minimumTransaction) != true))
			{
				System.out.println("Transaction failed to process. Discarded.");
				return false;
			}
		}
		this.transactions.add(transaction);
		System.out.println("Transaction Successfully added to Block");
		return true;
	}
	public String getHash()
	{
		return this.hash;
	}
	
	public String getPreviousHash()
	{
		return this.previousHash;
	}
}

여기서 확인해야할것은 우리의 BLock 생성자가 업데이트 되었습니다. 기존의 문자열 데이터로 더이상 보내지 않아도 되기때문입니다. 그리고 머클루트로 계산된 해시코드도 보셔야할것같습니다.

이제 addTransaction 메소드를 통해 트랜잭션들을 추가할 것이고 트랜잭션이 성공적으로 추가가가 되면 true 를 리턴해 성공하게 됩니다.

이제 블록체인의 트랜잭션을 만들기 위한 모든 구성요소들은 완성되었습니다!!

7. 진짜 찐찐 마지막

코인을 지갑으로 쌍방향으로 보낼 수 있는지 테스트해보겠습니다!!!

우선 새로운 코인을 생성해야하니 이 방법에 대해 설명드리겠습니다.

여기서 새로운 코인 생성방법
비트코인의 블럭체인의 예시를 들자면 채굴자나 그에 상응하는 주체는 그들 스스로 한개의 트랜잭션을 각 블럭들이 생성될 때마다 보상을 얻습니다. 하지만 여기서는 비트코인을 끌어들일 수 없으니, 비트코인이라는 것을 하나 하드코딩해서 있다고 치고, 가지고 싶은 만큼의 코인의 수를 첫번째 블럭에 발행 할 수 있도록 해보겠습니다. 
  • 지감A 에 100개의 으노코인을 발행하는 한개의 비트코인 블록하나
  • 트랜잭션을 고려한 유혀성체크
  • 테스트

이제 거의 페이스오프가 된 제 UnoChain이 동작할 소스입니다.

package Java.BlockChain;

import java.security.Security;
import java.util.ArrayList;
import java.util.HashMap;

public class UnoChain
{
	public static HashMap<String, TransactionOutput> UTXOs = new HashMap<String, TransactionOutput>();
	static int difficulty = BlockUtils.DIFFICULTY_NOMAL;
	public static void main(String[] args)
	{
		UnoChain uc = new UnoChain();
		uc.start();
	}
	
	public void start()
	{
		// 사용하지 않은 모든 트랜잭션 목록
		ArrayList<Block> blockchain = new ArrayList<Block>();
		Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
		Wallet walletA = new Wallet();
		Wallet walletB = new Wallet();
		Wallet coinbase = new Wallet();
		// 100 으노코인을 지갑으로 보내는 비트코인 트랜잭션을 만듭니다
		Transaction bitCoinTransaction = new Transaction(coinbase.getPublicKey(), walletA.getPublicKey(), 100f, null);
		// 생성 트랜잭션에 수동으로 서명
		bitCoinTransaction.generateSignature(coinbase.getPrivateKey());
		// 트랜잭션 ID 수동 설정
		bitCoinTransaction.setTransactionId("0");
		// 트랜잭션 출력 수동 추가
		bitCoinTransaction.getOutputs().add(new TransactionOutput(bitCoinTransaction.getReciepient(), bitCoinTransaction.getValue(), bitCoinTransaction.getTransactionId()));
		// 첫 번째 트랜잭션을 UTXO 목록에 저장하는 것이 중요
		UTXOs.put(bitCoinTransaction.getOutputs().get(0).getId(), bitCoinTransaction.getOutputs().get(0));
		System.out.println("Creating and Mining Genesis block... ");
		Block bitCoin = new Block("0");
		bitCoin.addTransaction(this, bitCoinTransaction, difficulty);
		BlockUtils.addBlock(blockchain, difficulty, bitCoin);
		// testing
		Block block1 = new Block(bitCoin.getHash());
		System.out.println("\nWalletA's balance is: " + walletA.getBalance(this));
		System.out.println("\nWalletA is Attempting to send funds (40) to WalletB...");
		block1.addTransaction(this, walletA.sendFunds(this, walletB.getPublicKey(), 40f), 0.1f);
		BlockUtils.addBlock(blockchain, difficulty, block1);
		System.out.println("\nWalletA's balance is: " + walletA.getBalance(this));
		System.out.println("WalletB's balance is: " + walletB.getBalance(this));
		
		Block block2 = new Block(block1.getHash());
		System.out.println("\nWalletA Attempting to send more funds (1000) than it has...");
		block2.addTransaction(this, walletA.sendFunds(this, walletB.getPublicKey(), 1000f), 0.1f);
		BlockUtils.addBlock(blockchain, difficulty, block2);
		System.out.println("\nWalletA's balance is: " + walletA.getBalance(this));
		System.out.println("WalletB's balance is: " + walletB.getBalance(this));
		
		Block block3 = new Block(block2.getHash());
		System.out.println("\nWalletB is Attempting to send funds (20) to WalletA...");
		block3.addTransaction(this, walletB.sendFunds(this, walletA.getPublicKey(), 20), 0.1f);
		System.out.println("\nWalletA's balance is: " + walletA.getBalance(this));
		System.out.println("WalletB's balance is: " + walletB.getBalance(this));
	}
}
  1. 비트코인으로 부터 알트코인인 으노코인을 100개를 생성 그걸 지갑 A에 담느다.
  2. 으노코인 40개를 지갑 B에 전송
  3. 지갑 A는  100 -> 60 지갑 B는 0 -> 40
  4. 지갑 A에서 1000을 보내려 하자 전송 오류 잔액부족.
  5. 이번엔 지갑 B에서 A로 으노코인 20개 전송
  6.  지갑 A는  60 -> 80 지갑 B는 40 -> 20

성공

지갑들은 우리의 블럭체인에서 안전하게 코인들을 보냈다 말았다 할 수 있었습니다. ! 단!! 돈이 있을때만 가능하겠죠??

이말은 드디어 우리가 암호화폐를 가지게 되었습니다 ㅎㅎㅎㅎ

이제 나만의 블럭체인을 만드는데 성공했습니다 ㅎㅎㅎ

  • new Wallet() 클래스를 통해서 새로운 wallet들을 만들 수 있다.
  • Elliptic-Curve를 이용해서 public / private 키를 지갑에게 던져준다.
  • 우리가 가지고 있는 자산을 디지털 시그쳐 알고리즘을 활용해서 이동시켜준다.
  • 사용자들이 우리의 블럭체인에서 거래와 거래장부를 만들수 있게한다.
  • 바로 소스 Block.addTransaction(walletA.sendFunds( walletB.publicKey, 20)); 를 이용해서 겠지요 ㅎㅎ

긴글 읽어주셔서 감사합니다.!!!

우선 영어원문 보느라 소스치느라 그리고 소스 제가 이해하고 넘어가느라 많은 시간이 걸렸네요 ㅠㅠ

소스들은 뭔가 다 간단한데 영어 원문이 너무 어려웠고?? 단어들은 쉬웠는데 뭐랄까 CS지식적으로 풀어가려니까 머리아프더군요 ㅎㅎㅎ

그래서 파파고와 

https://medium.com/programmers-blockchain/creating-your-first-blockchain-with-java-part-2-transactions-2cdac335e0ce

 

Creating Your First Blockchain with Java. Part 2 — Transactions.

The aim of this tutorial series, is to help you build a picture of how one could develop blockchain technology. You can find part 1 here.

medium.com

해당 포스팅을 참고했습니다. 

다음 포스팅에는 최종적으로 제가 제스스로 소스리팩토링과 용어 편집, 그리고 좀더 단순화 직렬화 하여서 소스를 공개하겠습니다.!!!

해당 깃허의 소스와 제가 지금 작성한 소스와 많이 다릅니다. ㅠㅠㅠ

그래도 동작은 동일하게 됩니다. 저만의 소스스타일이 확고해서용...

우선 제 소스주소입니다.

https://github.com/uno-km/Study_java/tree/main/src/Java/BlockChain

 

GitHub - uno-km/Study_java

Contribute to uno-km/Study_java development by creating an account on GitHub.

github.com

 

나중에 추후 조금 재미있는 소스들을 가져와서 포스팅해드리겠습니다.

 

긴글 읽어주셔서

감사합니다.!!!

728x90
반응형
LIST

댓글