JavaScript로 구현하는 GraphQL 블록체인

곽태욱·2020년 3월 15일
0

최종 결과물

https://vote-link.herokuapp.com/

GraphQL은 페이스북에서 만든 쿼리 언어다. Structed Query Language(SQL) 같은 쿼리 언어와 마찬가지로 데이터베이스에서 데이터를 읽고 쓸 때 사용된다. 그리고 GraphQL Playground는 GraphQL문을 실행할 수 있는 환경이다.

우리는 구글에 검색어를 입력하고, 기간을 설정하고, 결과 언어를 설정해 검색할 수 있다. 구글 사이트에 검색어를 입력하면 사이트를 검색할 수 있듯이, GraphQL Playground에 GraphQL문을 입력하면 데이터베이스를 검색할 수 있다.

GraphQL 요청은 크개 QueryMutation으로 나뉜다. Query 요청은 블록체인 데이터를 읽기만 하는 과정이고, Mutation 요청은 블록체인 데이터를 읽고 수정하는 과정이다. 이 2가지 방식으로 블록체인을 검색하고, 블록을 만들고, 거래를 만드는 등의 행동을 할 수 있다.

위 주소로 접속하면 GraphQL Playground 환경에서 거래와 블록을 조회하거나 생성할 수 있다. 비트코인 모델을 기반으로 만들었고, 무료 호스팅 사이트인 Heroku에 배포했다. 언어는 Javascript를 사용했고 클래스보단 여러 함수를 조합하는 방식(함수형 프로그래밍)으로 구현했다.

GraphQL Query

아래는 GraphQL Playground의 쿼리에 대해 설명한다.

블록체인 조회

{
  blockchain {
    id
    version
    previousBlockHash
    merkleRoot
    timestamp
    bits
    nonce
    transactions {
      version
      timestamp
      inputs {
        previousTransactionHash
        outputIndex
        signature
        senderPublicKey
      }
      outputs {
        recipientPublicKeyHash
        value
      }
      memo
    }
  }
}

블록체인 전체를 조회할 수 있는 쿼리문이다. 요청 후 블록의 배열이 반환된다.

블록체인은 블록들의 집합이고, 블록은 거래들의 집합이다. 즉, 거래를 모아 블록을 만들고 그 블록을 모아 블록체인을 만든다. 그래서 블록은 크게 Block Header와 Transaction의 배열로 이루어져 있다.

Block Header는 version, previousBlockHash, merkleRoot, timestamp, bits, nonce와 같이 6가지로 이루어져 있다.

Transaction는 크게 version, timestamp, inputs, outputs, memo로 이루어져 있다.

그중 input은 previousTransactionHash, outputIndex, signature, senderPublicKey로 이루어져 있고, output은 recipientPublicKeyHash, value로 이루어져 있다.

거래와 블록 구조만 제대로 이해한다면 블록체인의 핵심 기능을 모두 이해할 수 있다.

특정 블록 조회

{
  block(id: <블록번호>) {
    id
    version
    previousBlockHash
    merkleRoot
    timestamp
    bits
    nonce
    transactions {
      version
      timestamp
      inputs {
        previousTransactionHash
        outputIndex
        signature
        senderPublicKey
      }
      outputs {
        recipientPublicKeyHash
        value
      }
      memo
    }
  }
}

특정 인덱스의 블록을 조회할 수 있는 쿼리문이다. 요청 후 블록체인 쿼리와 마찬가지로 블록이 반환되기 때문에 블록체인 쿼리문과 첫번째 문장 빼고 동일하다.

그리고 SHA256 해시함수를 거쳐서 나온 모든 해시값은 16진수 64자리 문자열이다. 저 256은 결과값의 bit수를 의미한다. (256bit = 32 bytes = 16진수 64자리)

ID

쉽게 말하면 블록의 번호를 의미한다. 이를 통해 현재 블록이 몇 번째 블록인지 알 수 있다. 사실 블록체인 보고 몇 번째 블록인지 직접 계산하면 되니까 없어도 된다. 프로그래밍의 편의를 위한 부가적인 데이터이다.

Version

쉽게 말하면 해당 블록의 버전이다.

비트코인은 지금까지 BIP(Bitcoin Improvement Proposals)라고 비트코인의 성능을 높이는 제안이 여럿 있었다. 첫 블록은 당연히 버전 1부터 시작했고, BIP34(34번째 제안)에서 블록 버전이 2로 바뀌는 등의 변화가 있었다. 버전이 다르면 블록 간 서로 호환이 되지 않기 때문에 이를 명시해 준 것이다.

Previous Block Hash

이전 블록의 해시값이다. 즉, 해당 블록보다 블록 id가 1만큼 작은 블록의 해시값이 저장된다.

Merkle Root


쉽게 말하면 블록에 담긴 모든 거래 데이터를 해싱한 결과값이다.

어렵게 말하면 블록에 담긴 모든 거래로 해시 트리를 만들었을 때 그 해시 트리의 루트값이다. 블록에 포함된 거래 개수가 홀수 개면 마지막 거래를 복사해 짝수 개로 만들고 해시 트리를 생성한다.

그림을 클릭하면 영어로 쓰여진(...) 더 자세한 설명을 볼 수 있다.

Timestamp

function getTimestamp() {
  return new Date().getTime();
}

엄밀히는 해당 블록을 만들기 시작한 시각이다. 해당 블록이 만들어진 시각으로 이해해도 무방하다. 함수도 단순히 현재 시각을 가져오는 형태로 구현한다. 이 함수의 단위는 millisecond지만 비트코인의 timestamp 시간 단위는 second다.

Bits

블록의 난이도와 관련있는 값이다. 실제 비트코인에선 난이도를 약간 어렵게 계산하지만, 여기서는 블록 해시값 앞에 나와야 하는 0의 개수(2진법 기준)로 이해하면 된다.

비트코인의 경우 이 값은 매 2016번째 블록마다 갱신된다. 즉, 0번 블록부터 2015번 블록까지는 같은 bits 값을 공유하고, 2016번 블록부터 4031번 블록까지도 같은 bits 값을 공유한다.

갱신 기준은 다음과 같다. 최근 2016개의 블록이 만들어지는데 걸린 시간이 2주보다 길면 bits값이 감소하고, 짧으면 bits값이 증가한다. 이러한 갱신을 통해 블록 생성 난이도가 조절된다.

그래서 비트코인의 경우 블록 생성 주기를 평균 10분이라고 한다. (2주/2016개 = 10분/1개)

Nonce

function getNonce(blockHeader) {
  blockHeader.nonce = 0;
  while (true) {
    if (isValidBlockHash(getBlockHash(blockHeader), blockHeader.bits))
      return blockHeader.nonce;
    blockHeader.nonce++;
  }
}

블록을 새로 만들 때 필요한 값으로서 nonce 구하는 과정은 아래와 같이 간단하다. 유효한 nonce를 구하는 과정에서 연산력이 많이 필요하기 때문에 블록을 생성하는 과정을 채굴이라고 부르기도 한다.

  1. Nonce는 0부터 시작한다.
  2. 블록 헤더로부터 블록 해시값을 구하고, 해시값 앞에 0이 'bits'개 이상인지 확인한다.
  3. 맞으면 성공, 아니면 Nonce에 1을 더하고 과정 2,3을 반복한다.

Block Header

id를 제외한 위 6가지 요소를 합쳐서 블록 헤더라고 한다. 그리고 이 6가지의 블록 헤더를 통해 블록 해시값을 구할 수 있다.

Block Hash

function getBlockHash(blockHeader) {
  return getDoubleHash(
    blockHeader.version +
      blockHeader.previousBlockHash +
      blockHeader.merkleRoot +
      blockHeader.timestamp +
      blockHeader.bits +
      blockHeader.nonce
  );
}

쉽게 말하면 블록 헤더의 해시값이다.

블록 해시값을 계산하는 코드도 위와 같이 간단하다. 블록 헤더의 6가지 데이터를 일렬로 이어 붙여 해시함수에 2번 넣으면 64자리의 문자열(16진수)이 반환되는데 이것이 블록 해시값이다.

블록 해시값은 블록 헤더로부터 누구나 계산할 수 있기 때문에 블록에 따로 저장되진 않는다.

거래 풀 조회

{
  transactionPool{
    version
    timestamp
    inputs {
      previousTransactionHash
      outputIndex
      signature
      senderPublicKey
    }
    outputs {
      recipientPublicKeyHash
      value
    }
    memo
  }
}

거래 풀은 아직 블록에 포함되지 않은 거래를 모아 놓은 배열이다. 전파 받은 거래나 자기가 생성한 거래를 거래 풀에 모아 놓고, 나중에 블록을 만들 때 여기서 유효한 거래를 뽑아서 블록에 포함시키는 용도로 활용된다.

거래에는 누가 언제 누구에게 얼마를 보낸다는 내용이 적혀있다. 거래의 형태는 위와 같고, 각 항목에 대한 의미는 아래와 같다.

Version

해당 거래가 따르는 버전이다. 블록의 버전과는 서로 아무런 관련이 없다.

Timestamp

function getTimestamp() {
  return new Date().getTime();
}

해당 거래가 생성된 시각이다. 거래를 만들 때의 시각을 가져온다. 블록의 timestamp 개념과 동일하다.

Memo

거래에 적어두는 메모용 문자열이다. 거래를 구성하는 필수 요소는 아니다.

Inputs, Outputs

거래는 inputoutput의 배열을 포함하는데

  • input엔 내 코인이 블록체인 어디에 있는지와 그 코인의 소유권을 증명하는 방법이 적혀 있고,
  • output금액과 그 금액의 소유권자가 적혀 있다.

즉, 송금은 input에 소유권이 증명된 내 코인의 위치를 적고, output에 상대방의 주소와 보낼 금액을 적는 과정이다.

블록체인의 코인(=돈)은 실물 화폐와 비슷하게 금액을 쪼갤 수 없어 잔액을 거슬러줘야 한다. 만약 상대방에게 1100원을 보내고 싶은데 자신에게 1000원과 500원 짜리밖에 없다면, 일단 1500원을 보내고 400원을 거슬러 받아야 한다.

거래는 1개 이상의 input과 1개 이상의 output으로 이루어져 있다. input이 여러 개인 이유는 자신의 여러 잔돈을 하나하나 가리켜야 하기 때문이고, output이 여러 개인 이유는 한번에 여러 사람에게 코인을 보내고 싶을 때가 있기 때문이다.

그리고 코인은 output으로부터 생겨난다. (=발행된다)

Output - Recipient Public Key Hash

해당 Output이 누구의 소유인지 알 수 있는 용도로 쓰인다. 위 그림에서의 공개키 해시에 해당한다. 일종의 계좌 번호인 셈이다. 사람들은 송금할 때 돈을 계좌 번호로 보내기 때문이다.

비트코인의 경우 이 공개키 해시값을 Base58 인코딩한 값을 주소로 사용한다. 하지만 인코딩의 경우 누구나 디코딩할 수 있기 때문에 여기선 그냥 공개키 해시를 SHA256으로 2번 해싱한 것을 주소로 사용했다.

그림을 클릭하면 비트코인 키/주소가 생성되는 과정을 시각적으로 볼 수 있다.

Output - Value

금액이다.

Input - Previous Transaction Hash

내가 소유한 코인이 어느 Transaction에 들어있는지 알려준다. 해시값은 유일하기 때문에 Transaction을 특정할 수 있다. 엄밀히 따지면 2^256분의 1 확률로 해시값이 겹칠 수는 있지만 확률이 매우 낮고, 설령 겹친다 해도 블록체인 전체 거래를 하나하나 뒤져보면 되니 상관없다.

Input - Output Index

위의 '이전 거래 해시값'으로 찾은 Transaction의 여러 output 중 어느 output이 내 소유인지 알려준다. 이렇게 previousTransactionHashoutputIndex내 코인이 블록체인 어디에 있는지 지목하는 용도로 사용된다.

Input - Sender Public Key

이 값을 통해 특정 Output의 소유권을 증명할 수 있다. 특정 Output의 소유권을 증명하는 과정은 아래와 같다.

senderPublicKey의 해시값과 특정 Output의 recipientPublicKeyHash가 동일한지 확인한다.

두 값을 비교해서 동일하면 소유권이 있는 것이고, 아니면 없는 것이다. 해시는 단방향 함수이기 떄문에 주소(=공개키 해시)로부터 공개키를 찍어서 맞추기는 어렵다.

공개키는 개인키로부터 생성되기 때문에 이 공개키를 해킹하기 위해선 개인키를 알아야 하는데, 개인키를 찍어서 맞출 확률은 2^256분의 1이다. 이는 10진수 77자리 비밀번호의 보안력과 비슷하다. 물론 은행과 다르게 비밀번호 재시도 횟수 제한은 없지만... 그 확률은 아래와 같다.

해시 연산 1번이 1FLOP이라고 가정하면, 2019년 기준 가장 빠른 슈퍼컴퓨터인 서밋 1조 개로 우주(138억년)가 1조 번 다시 태어날 때까지 계산해도 100경(10^18)분의 1 확률로 맞출 수 있다.

하지만 공개키는 1번 쓰면 누구나 볼 수 있기 때문에 1회성 비밀번호와 성격이 비슷하다. 그래서 아래 서명과 함께 소유권을 증명한다. 사실 1회성이기 때문에 공개키 소유권 증명은 굳이 없어도 되고, 비트코인을 개발한 사토시 나카모토도 보안상 공개키는 1번 쓰고 바꾸는 걸 권장한다고 말한다.

Input - Signature

우리가 은행에서 거래를 하면 그 정보가 은행 전산망으로 가는 것처럼 블록체인에서 거래를 새로 만들면 이 거래 정보를 블록을 생성하는 사람(노드)에게 보내야 한다.

하지만 블록체인은 기존 은행과 달리 프로그래밍 편의를 위해 거래 데이터를 암호화하지 않고 다른 사람에게 전달하는데 이때 다른 사람이 중간에서 거래 데이터를 악의적으로 변경할 가능성이 있다. 그래서 거래 데이터를 다른 사람이 변조하지 못하도록 거래에 거래를 만든 사람의 전자 서명을 첨부한다. 이 서명은 우리가 종이에 이름을 싸인하는 그런 서명이 아니라 개인키로부터 생성되는 16진수 문자열이다. 서명을 만드는 과정과 거래 데이터 위변조를 검증하는 과정은 아래와 같다.

서명 생성 : 개인키 + 거래 데이터 = 전자 서명 (16진수 문자열)
서명 검증 : 공개키 + 거래 데이터 + 전자 서명 = 거래 데이터 위변조 여부 (참/거짓)

위의 개인키와 공개키는 거래를 생성한 사람 것을 사용한다. 그리고 전자 서명은 거래 데이터와 현재 시각에 따라 매번 다른 문자열의 서명이 생성된다. 일종의 계좌 OTP 비밀번호인 셈이다.

서명도 개인키로부터 생성되기 때문에 이 서명을 해킹하기 위해선 개인키를 알아야 한다. 그 확률은 앞서 공개키 해킹에서 말한 확률과 같다.

거래를 생성하는 자세한 과정은 여기서 확인할 수 있다. 사이트 아래 Creating a new Transaction 부분에서 보낼 금액을 조절하는 막대기를 움직여 거래 데이터가 어떻게 변하는지 시각적으로 확인할 수 있다. (영어 주의...)

내 주소 조회

{
  me
}

현재 노드의 지갑 주소를 조회할 수 있다. 16진수로 64자리 문자열이 반환된다.

모든 지갑 주소 조회

{
  users
}

블록체인에 기록된 모든 지갑의 주소를 조회할 수 있다. 16진수 64자리 문자열의 배열이 반환된다. (테스트를 위해 배열 첫번째에 상대방 주소가 출력된다.)

내 잔액 조회

{
  myBalance
}

현재 노드의 잔액을 조회할 수 있다. 현재 제네시스 블록 생성 보상 50원이 있는 것을 확인할 수 있다.

특정 지갑 잔액 조회


{
  balance(publicKeyHash: "<지갑주소>")
}

특정 지갑의 잔액을 조회할 수 있다. 매개변수에 현재 노드의 지갑 주소를 넣으면 myBalance 쿼리와 동일한 결과가 출력된다.

GraphQL Mutation

블록 생성

블록을 생성하는 데도 순서가 있다.

거래 넣기
코인베이스 거래 넣기
머클 루트 구하기

이전 블록 해시 구하기

논스 구하기

거래 생성

만약 상대방에게 1100원을 보내고 싶고 수수료로 50원을 내고 싶은테 자신에게 1000원과 500원 짜리밖에 없다면, 일단 1500원을 보내고 350원을 거슬러 받는다.

데이터 수신

블록 수신

거래 수신

블록체인 수신

피어 추가

참고 : https://medium.com/caulink/javascript%EB%A1%9C-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-fab57b25e90b
profile
이유와 방법을 알려주는 메모장 겸 블로그. 블로그 내용에 대한 토의나 질문은 언제나 환영합니다.

1개의 댓글

comment-user-thumbnail
2020년 3월 21일

감사요 센세

답글 달기