본문 바로가기
경일/Block-chain

[Blockchain/블록체인] P2P 서버로 받은 블록 검증 및 업데이트

by dev_kong 2022. 6. 15.
728x90
728x90

0. 목차

1. 개요

2. 기능구현

1. 개요

저번 포스팅에서 P2P 네트워크를 통해 각각의 노드가 가지고 있는 블록과 체인을 주고 받아보았다.
상대방에게 전달 받은 블록체인으로 업데이트가 될 필요가 있다면, 해당 블록체인으로 내 노드의 블록체인을 업데이트 해줘야 한다.
그렇다면 우선 전달 받은 블록체인으로 업데이트가 될 필요가 있는지 없는지 부터 확인을 해야 한다.
그리고 업데이트가 될 필요가 있다면,
하지만 업데이트 하기 전에, 상대방에게서 받은 블록체인에 대한 검증이 이뤄져야한다.
검증이 통과 됐다면, 상대방의 블록체인으로 내 블록체인을 업데이트 해주면 된다.

2. 기능구현

위에서 업데이트가 될 필요라고 했는데,

이 필요성을 확인하는 조건이 있다.
전달받은 최신 블록의 height+1이 내가 가진 최신블록의 height와 같은지 이다.
height를 비교했을 때 위 조건이 성립한다면
당연히 hash값 역시 비교를 해줘야 한다.
전달받은 최신 블록의 previousHash 와 내가 가진 최신 블록의 hash가 일치 한다면,

블록을 전달한 노드의 블록체인의 length는 내가 가진 블록체인의 length 보다 1이 크다는 의미가 된다.

그렇게 된다면, 굳이 블록 전체를 바꿀 필요없이 전달받은 블록하나만 내 블록체인에 추가해주면 된다.

그런데 위 조건이 성립하지 않는다면,
블록체인 전체를 전달 받고, 전달 받은 블록체인의 length 와 내가 블록체인의 length를 비교한 뒤,
전달받은 블록체인의 length가 더 길다면, 내 블록체인을 전달받은 걸로 바꿔 끼운다.
하지만 바꿔 끼우기 전에 전달 받은 블록체인에 대한 검증이 이뤄져야한다.

length가 1 차이날 때를 2-1, length가 2 이상 차이날 때를 2-2에서 작성할거다.

2-1. length 차이가 1 일때

저번 포스팅에서 작성한
p2p.ts 파일의 messageHandler method 부분에 코드를 추가해주면 된다.

  messageHandler(socket: WebSocket) {
    const send = this.send(socket);

    const messageCallback = (data: string) => {
      const result: Failable<Message, string> =
        P2PServer.dataParser<Message>(data);

      if (result.isError) {
        console.log(result.error);
        return;
      }

      if (!result.isError) {
        switch (result.value.type) {
          case MessageType.latest_block: {
            const message: Message = {
              type: MessageType.all_block,
              payload: [this.getLatestBlock()],
            };

            send(message);
            break;
          }

          case MessageType.all_block: {
            const message: Message = {
              type: MessageType.all_block,
              payload: this.getChain(),
            };

            send(message);
            break;
          }
          //추가된 부분
          case MessageType.receiveChain: {
            const receivedChain: IBlock[] = result.value.payload;

            console.log(receivedChain);
            break;
          }
        }
      }
    };

    socket.on('message', messageCallback);
  }

위의 코드는 저번 포스팅에서 작성된 내용이다.
상대방 노드의 최신 블록을 받아와서 처리하는 부분은
case MessageType.all_block 의 부분이다.

 case MessageType.all_block: {
   /**
             * TODO:
             * 내 체인의 최신 블록의 hash와 상대방 최신블록의 previousHash 가 일치하는 지 확인
             */
   const [receivedBlock] = result.value.payload;
   const isValid = this.addToChain(receivedBlock);

   if (!isValid.isError) {
     console.log('chain updated!');
     break;
   }

   const message: Message = {
     type: MessageType.receiveChain,
     payload: this.getChain(),
   };

   send(message);
   break;
 }

이렇게 만들어 주면 된다!
그리고 addToChain 함수는 이제부터 작성해주면 된다.
addToChain은 chain.ts에 작성해줄거다.

  addToChain(_receivedBlock: Block): Failable<undefined, string> {
    const isValid: Failable<Block, string> = Block.isValidNewBlock(
      _receivedBlock,
      this.getLatestBlock(),
    );

    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    this.blockchain.push(_receivedBlock);
    return { isError: false, value: undefined };
  }

addToChain 함수는 receivedBlock(전달받은 최신블록) 을 인자값으로 받아서,
isValidNewBlock 함수에 내가 가진 최신블록과 함께 인자값으로 넣어준다

그리고 isValidNewBlock 함수는 저번에 만들었던 함수인데,
설명이 안적혀있는 것 같다.

  static isValidNewBlock(
    _newBlock: Block,
    _previousBlock: Block,
  ): Failable<Block, string> {
    /* 
    1. 이전 블록 높이 +1 === 새로생긴 블록 높이 check
    2. 이전블록.해시 === 새로생긴블록.이전해시 check
    3. 새로생긴 블록의 해시를 새로 만듬 === 새로생긴블록.해시 check
     */

    if (_previousBlock.height + 1 !== _newBlock.height) {
      return { isError: true, error: '블록 높이가 맞지 않습니다.' };
    }

    if (_previousBlock.hash !== _newBlock.previousHash) {
      return { isError: true, error: '이전 해시값이 맞지않습니다.' };
    }

    if (Block.createHash(_newBlock) !== _newBlock.hash) {
      return { isError: true, error: '블록해시가 올바르지 않습니다.' };
    }

    return { isError: false, value: _newBlock };
  }

블럭을 새로 만들 때 사용하기 위해 만들었는데,
재사용이 가능하다.

위 세가지 조건을 확인하고 세가지 조건이 모두 통과되면
그때 블록을 추가해준다.

블록이 추가됐다면 블록이 추가된 노드와 연결된 다른 모든 노드들에게 업데이트 내용을 전달해줘야한다.

 case MessageType.all_block: {
   /**
             * TODO:
             * 내 체인의 최신 블록의 hash와 상대방 최신블록의 previousHash 가 일치하는 지 확인
             */
   const [receivedBlock] = result.value.payload;
   const isValid = this.addToChain(receivedBlock);

   if (!isValid.isError) {
     console.log('chain updated!');
    // 추가된 부분
     const message: Message = {
       type: MessageType.latest_block,
       payload: {},
     };

     this.broadcast(message);
     break;
   }

   const message: Message = {
     type: MessageType.receiveChain,
     payload: this.getChain(),
   };

   send(message);
   break;
 }

이렇게 추가하면 된다.
이러면 length가 1만 차이날때의 코드는 완성이다.
근데 broadcast 함수를 만들어줘야 한다.

broadcast는 매우 간단하다.
handshake가 일어날때 마다,
handshake가 이뤄진 모든 소켓에대한 정보를
sockets라는 배열에 담아 두었었다.

이 포스팅에 들어있다.

sockets 배열에 for문을 돌려서 나와 연결된 모든 노드에게 send를 해주는 매우 간단한 함수다.

  broadcast(message: Message): void {
    this.sockets.forEach((socket) => this.send(socket)(message));
  }

진짜 별거 없다.

여기까지 작성을 하고 다시 확인을 해보니 전달받은 최신블록과 내가 가진 최신블록이 같은 경우의 예외처리를 안해줬는데,
이 정도는 딱히 적지 않아도 충분히 작성 가능할 거라고 생각한다.

2-2. length가 2 이상 차이날 때

이 부분에 대한 코드는 case MessageType.receiveChain에 추가하면 된다.

   case MessageType.receiveChain: {
     const receivedChain: IBlock[] = result.value.payload;
     this.handleChainResponse(receivedChain);
     break;
   }

코드가 길어서 함수로 따로 묶어줬다.

  handleChainResponse(_receivedChain: IBlock[]): Failable<undefined, string> {
    const isValid = this.replaceChain(_receivedChain);
    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    return { isError: false, value: undefined };
  }

예외 처리를 깔끔하기 위해 함수를 두개로 나누었다.. ㅎㅎㅎ

replaceChain 역시 method로 추가해주면 된다.

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

    /**
     * TODO:
     * 0. genesisBlock 비교해서 서로 다르면 리턴
     * 1. 받은체인의 최신블록,height과 내 체인 최신블록.height 비교
     * 내 체인의 최신블록의 height 가 더 높거나 같으면 return
     * 2. 받은체인의 최신블록.previousHash === 내 체인의 최신블록.hash -> return
     * 3. 받은체인의 길이가 === 1 받은체인에 제네시스밖에 없음 return
     * 4. 내 체인이 더 짧아서 바꾸긴 해야될거 같은데 일단 받은 체인 검증한번 때리자.
     * 5. 검증 통과하면 다바꾸자!
     */
    if (
      JSON.stringify(_receivedChain[0]) !== JSON.stringify(this.blockchain[0])
    ) {
      return { isError: true, error: 'GENESIS 블록이 다름' };
    }

    if (receivedLatestBlock.height === 0) {
      return { isError: true, error: '받은 체인의 최신블록이 GENESIS임' };
    }

    if (receivedLatestBlock.height <= latestBlock.height) {
      return { isError: true, error: '자신의 체인이 길거나 같습니다.' };
    }

    if (receivedLatestBlock.previousHash === latestBlock.hash) {
      return { isError: true, error: '블록갯수 하나 차이남' };
    }

    const isValid = this.isValidChain(_receivedChain);

    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    this.blockchain = _receivedChain;
    return { isError: false, value: undefined };
  }

함수가 조금 길다. 예외처리가 많아서 그런거지 로직자체는 복잡한 편이 아니다.

  1. GENESIS 블록이 다르면 비교다른걸 할필요도 없다.

  2. 내가 가진 블록체인의 length가 더 길거나 같다면, 내 chain이 최신이니 업데이트 할 필요가 없다.

  3. 받은 체인의 최신블록의 previousHash 와 내가 가진 최신블록의 hash 가 같다면 역시 리턴을 해준다
    이부분은 사실 이미 처리가 된 부분이다. 하지만 나중에 이 메서드만 따로 사용하는 경우가 있어서 예외처리를 해주었다.

  4. 전달 받은 블록체인의 길이가 1이라면, 즉, 받은 블록체인내에는 GENESIS 밖에 없는 거니 리턴.

  5. 위 4가지 케이스를 모두 통과 했다면, 내 블록체인을 전달받은 블록체인으로 교체해주자!

가 로직이다.

로직 자체는 복잡하지가 않다.
그런데 이제 전달받은 블록체인으로 교체를 해주기 전에!
전달받은 블록체인에 대한 검증이 필요하다!
블록체인에대한 검증에 대한 함수는 isValidChain이다.

isValidChain(_chain: Block[]): Failable<undefined, string> {
    // TODO:  체인에대한 검증 코드
    for (let i = 1; i < _chain.length; i++) {
      const curBlock = _chain[i];
      const preBlock = _chain[i - 1];

      const isValid: Failable<Block, string> = Block.isValidNewBlock(
        curBlock,
        preBlock,
      );

      if (isValid.isError) {
        return { isError: true, error: isValid.error };
      }
    }

    return { isError: false, value: undefined };
  }

이거 또한 간단한 로직이다.
블록체인 에 대한 검증이란 즉, 체인에 담겨있는 모든 블록에대한 검증을 해주면 되는거다.
그리고 블록에대한 검증은 이미 구현이 되어있다.
바로 isValidBlock이다.

for 반복문으로 전체 블록체인안의 모든 블록에 대해 검증을 때린다.
블록을 검증하는 방법은 현재블록과 이전블록의 비교를 통해 이뤄진다.
블록 검증이 실패하면 바로 isError:true를 리턴해준다.

다시 handleChainResponse 메서드로 돌아가보자.

  handleChainResponse(_receivedChain: IBlock[]): Failable<undefined, string> {
    const isValid = this.replaceChain(_receivedChain);
    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    //전체 블록체인 바뀜!
    return { isError: false, value: undefined };
  }

위 함수의 if문을 통과한다면 내가가진 블록체인이 전달받은 블록체인으로 바뀌었다는 뜻이다.
그럼 이때 역시 내 노드와 연결된 모든 다른 노드들에게 내 블록체인이 업데이트 됐다는 것을 알려줘야 한다.

이때도 broadcast를 이용하면 된다.

  handleChainResponse(_receivedChain: IBlock[]): Failable<undefined, string> {
    const isValid = this.replaceChain(_receivedChain);
    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }
    // 추가된 부분
    const message: Message = {
      type: MessageType.receiveChain,
      payload: _receivedChain,
    };

    this.broadcast(message);
    return { isError: false, value: undefined };
  }

이렇게 해주면 내 블록체인이 업데이트 될때 마다 나와 연결된 모든 노드들이 메세지를 받을거고,
그 노드들에서 각자 검증을 진행하고, 각자의 블록체인을 업데이트 하게 될 것이다.

수업에서 직접 이부분을 서로 연결해서 블록업데이트를 해봤는데,
실제로 동작하는게 너무 신기했다.

728x90
728x90

댓글