즐코

지갑, 트랜잭션 구현 (6) - 트랜잭션 풀 업데이트와 브로드캐스트 본문

BlockChain

지갑, 트랜잭션 구현 (6) - 트랜잭션 풀 업데이트와 브로드캐스트

YJLEE_KR 2022. 6. 25. 20:31

해당 포스팅에선 블록 채굴 후 트랜잭션 풀의 업데이트 및 블록체인 네트워크 상의 다른 노드들에게 트랜잭션 풀을 공유 즉, 브로드캐스트 해주는 것에 대해서 정리한다.

 

1/ 트랜잭션 풀 업데이트

우선은 tx를 데이터로 넣고 채굴한 블록을 체인에 추가하면서 txPool도 같이 업데이트해줘야하므로 chain에 txPool 속성을 추가해준다. (txPool 관련 이전 포스팅 https://yjleekr.tistory.com/81?category=1284408)

 

- 트랜잭션 발생 시 tx pool에 새로운 tx가 추가되게끔 appendTxPool 메소드도 만들어준다.

- 블록 채굴 후 블록체인에 추가 시 block으로 만들어진 data는 이제 필요가 없으므로 pool에서 빼줘야한다. updateTxPool()

// src/core/blockchain/chain.ts

export class Chain {
  private blockchain: Block[];
  private utxo: IUtxo[];
  private txPool: ITransaction[];
  
  constructor() {
    this.blockchain = [Block.createGENESIS(GENESIS)];
    this.utxo = [];
    this.txPool = [];
  }
  
  public getTxPool(): ITransaction[] {
  	return this.txPool;
  }
  
  public appendTxPool(_transacion: ITransaction): void {
  	this.txPool.push(_transaction);
  }
  // 새로운 블럭 받아서 txPool 업데이트
  public updateTxPool(_newBlock: IBlock): void {
    let curTxPool: ITransaction[] = this.getTxPool();
    _newBlock.data.forEach((tx: ITransaction) => {
    	curTxPool = curTxPool.filter((eachTx) => {
          eachTx.hash !== tx.hash;
        });
    })
    this.txPool = curTxPool;
  }
}

트랜잭션 발생 시 새로운 트랜잭션을 추가해주고 체인 내 utxo도 업데이트해준다.

// index.ts

app.post("/sendTransaction", (req, res) => {
   try{
    	const rcvdTx: ReceivedTx = req.body;
        const tx = Wallet.sendTransaction(rcvdTx, ws.getUTXO());
        ws.appendTxPool(tx); // 트랜잭션 풀에 트랜잭션 추가
        ws.updateUTXO(tx); // 새로운 트랜잭션으로 인한 utxo 업데이트
    }catch(e){
    	if (e instanceof Error) console.error(e.message);
    }
    res.json([]);
})

새로운 블럭 추가 시 chain클래스 메소드에 두 가지 수정사항이 있다.

1/ 새로운 블럭 채굴 시 새로운 tx뿐만 아니라 기존 txPool에 그동안 쌓여있던 tx들도 가져와줘야하므로 miningBlock 메소드 내에서 addBlock 실행 시 인자를 [transaction] 이 아닌 전체 기존 txpool의 tx도 가져와서 넣어준다.

return this.addBlock([transaction, ...this.getTxPool()]);

  // src/core/blockchain/chain.ts
  
  public miningBlock(_account: string): Failable<Block, string> {
    // 1. 채굴보상을 넣어준다
    const txin: ITxIn = new TxIn("", this.getLatestBlock().height + 1);
    const txout: ITxOut = new TxOut(_account, 50);
    const transaction: Transaction = new Transaction([txin], [txout]);
    
    // 2. 채굴보상 내역에 대해 utxo를 생성하고 이를 체인 내 utxo 배열 내에 추가해준다.
    const utxo = transaction.createUTXO();
    this.appendUTXO(utxo);
    
    // 3. addBlock 실행 - 새로운 tx와 기존 txPool의 tx들을 불러와서 블럭의 데이터로 넣어준다.
    return this.addBlock([transaction, ...this.getTxPool()]);
  }

2/ 블럭 검증 후 새로운 블럭의 데이터에 들어있는 tx의 txIns를 utxo 배열에서 없애줌과 동시에 tx 자체도 txPool에서 빼줘야한다.  이걸 addBlock 메서드에서 해결해준다.

  // src/core/blockchain/chain.ts
  
  public addBlock(data: ITransaction[]): Failable<Block, string> {
    const previousBlock = this.getLatestBlock();
    previousBlock.height + 1;
    const adjustmentBlock: Block = this.getAdjustmentBlock();
    const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
    const isValid = Block.isValidNewBlock(newBlock, previousBlock);

    if (isValid.isError) return { isError: true, error: isValid.error };
    
    this.blockchain.push(newBlock);
    
    newBlock.data.forEach((_tx: ITransaction) => {
      this.updateUTXO(_tx);
    });
    
    this.updateTxPool(newBlock);
    
    return { isError: false, value: newBlock };
  }

updateTxPool() 에선 기존 txPool을 가져와서, 새로운 블럭의 tx들의 hash값과 txPool에 모여있는 tx들의 hash 값을 비교해서 다른 거만 curTxPool 에 담아주고 그걸 현재 txPool로 대체해준다.

// src/core/blockchain/chain.ts
  
public updateTxPool(_newBlock: IBlock): void {
    // 새로운 블록을 가져온 뒤 그 block의 tx hash랑 txPool의 tx들 해쉬랑 비교해서 같은 거는 없애준다.
    let curTxPool: ITransaction[] = this.getTxPool();
    _newBlock.data.forEach((tx: ITransaction) => {
      curTxPool = curTxPool.filter((eachTx) => {
        eachTx.hash !== tx.hash;
      });
    });

    this.txPool = curTxPool;
  }

 

2/ 트랜잭션 풀 Broadcast

이후 이 업데이트한 txPool을 같은 블록체인 네트워크 상의 노드들에게 공유해줘야하기때문에 이전에 만들어둔 블록체인 네트워크의 p2p.ts 수정도 해준다. 

 

웹소켓을 통한 p2p 통신이므로, 이전에 블럭과 체인을 broadcast 했던 방식대로 Message를 만들어서 tx를 공유해준다.

 

1/ 우선 tx에 대한 메시지이므로 MessageType을 추가해준다. (rcvdTx) 

2/ 현재 받은 rcvdTx가 내 트랜잭션 풀에 없다면 추가해주고, payload에 해당 tx를 넣어서 다른 노드들에게 broadcast 해준다.

// src/serve/p2p.ts

enum MessageType {
  latest_block = 0,
  all_block = 1,
  rcvdChain = 2,
  rcvdTx = 3,
}

messageHandler(socket: WebSocket) {
    const cb = (data: string) => {
      const result: Message = P2PServer.dataParse<Message>(data);
      const send = this.send(socket);

      switch (result.type) {
        // 이전에 블럭과 체인에 관련된 메시지 케이스들은 생략한다.
        case MessageType.rcvdTx: {
          const rcvdTx: ITransaction = result.payload;
          if (rcvdTx === null) break;
          const sameTx = this.getTxPool().find((_tx: ITransaction) => {
            return _tx.hash === rcvdTx.hash;
          });
          if (!sameTx) {
            // 받은 트랜잭션 내용이 내 트랜잭션 풀에 없다면 내 풀에 추가해준다.
            this.appendTxPool(rcvdTx);
            const msg: Message = {
              type: MessageType.rcvdTx,
              payload: rcvdTx,
            };
            // 다른 노드에게도 업데이트된 트랜잭션 풀을 넘겨준다
            this.broadcast(msg);
          }
          break;
        }
      }
    };
    socket.on("message", cb);
  }

3/ 트랜잭션 요청이 들어오면 트랜잭션을 생성하고, 이 트랜잭션을 다른 노드에게 공유해야하므로, p2p.ts에서 설정해둔대로 Message를 만들어서 payload로 해당 트랜잭션을 전달하면 된다.

// index.ts

app.post("/sendTransaction", (req, res) => {
  console.log("reqbody", req.body);
  try {
    const rcvdTx: ReceivedTx = req.body;
    const tx = Wallet.sendTransaction(rcvdTx, ws.getUTXO());
    ws.appendTxPool(tx);
    ws.updateUTXO(tx);
     // 다른 노드에게 트랜잭션을 공유하기위해 메시지를 만든다. 
    const msg: Message = {
      type: MessageType.rcvdTx,
      payload: tx,
    };
    // 다른 노드에게 트랜잭션이 담긴 메시지를 브로드캐스트!
    ws.broadcast(msg);
  } catch (e) {
    if (e instanceof Error) console.error(e.message);
  }
  res.json([]);
});

 

또한, 블럭과 체인을 다른 노드에게 broadcast 받았을 때도, 블럭이나 전체 체인 내 블럭에 담긴 tx를 나의 txPool에서 빼는 작업을 해줘야하므로, 받은 블럭을 체인에 추가해주는 addToChain() 메서드와 받은 체인을 통째로 교체하는 replaceChain() 메서드 상에도 updateTxPool() 메서드를 써서 txPool을 업데이트해줘야한다.

// src/core/blockchain/chain.ts

public addToChain(_rcvdBlock: Block): Failable<undefined, string> {
    const isValid = Block.isValidNewBlock(_rcvdBlock, this.getLatestBlock());
    if (isValid.isError) return { isError: true, error: isValid.error };
    this.blockchain.push(_rcvdBlock);
    _rcvdBlock.data.forEach((tx) => this.updateUTXO(tx));

    this.updateTxPool(_rcvdBlock);
    return { isError: false, value: undefined };
  }

 

replaceChain 시엔, txPool 업데이트와 동시에 utxo 업데이트도 필요하다.

// src/core/blockchain/chain.ts

replaceChain(rcvdChain: Block[]): Failable<undefined, string> {
    const latestRcvdBlock: Block = rcvdChain[rcvdChain.length - 1];
    const latestBlock: Block = this.getLatestBlock();

    if (latestRcvdBlock.height === 0) {
      return { isError: true, error: "받은 최신블록이 제네시스 블록입니다." };
    }
    if (latestRcvdBlock.height <= latestBlock.height) {
      return { isError: true, error: "내 블체가 더 길거다 같다" };
    }
    if (latestRcvdBlock.previousHash === latestBlock.hash) {
      return { isError: true, error: "블록이 하나 모자르다" };
    }
    this.blockchain = rcvdChain;

    // tx pool 업뎃 + utxo 업뎃
    this.blockchain.forEach((block) => {
      this.updateTxPool(block);
      block.data.forEach((tx) => {
        this.updateUTXO(tx);
      });
    });
    return { isError: false, value: undefined };
  }
Comments