// 에드센스

[Lottery Dapp 개발하기] 2.4까지의 분량이다. 

밀린 블로그 글써내기중이라 그간 했던 분량을 한번에 쑤셔넣는중.

이 글을 읽는 여러분들에게 도움이 되면 좋겠지만 딱히 그럴려고 쓰는 글은 아니고 혼자서 끄적이는 글.

따라서 생략된 설명이나 이걸 왜 설명? 하는 부분이 있을것이다.

 

 

가보자~

강의 출처:


1. 코딩할 것들

  • 스마트 컨트랙
    • 베팅정보를 담는 구조체
    • 베팅정보를 저장할 큐 자료구조
    • 쌓인상금_가져오기()
    • 베팅큐에서_베팅정보_가져오기()
    • 베팅하기()
      • 베팅을_베팅큐로_밀어넣기()
      • 베팅을_베팅큐에서_빼기()
  • 테스트코드 (다음글에서)
    • beforeEach
    • 베팅큐에 베팅정보가 잘 들어간경우 테스트
    • 베팅큐에 베팅정보가 못들어간경우 테스트

 

 

 


2. Lottery.sol에 코딩한 것들

베팅 정보를 담는 구조체

  struct BetInfo {
    uint256 answerBlockNumber; // 맞출 블록의 넘버
    address payable bettor; // 돈을 건 사람 주소, 특정주소에 돈을 보내려면 payable을 써줘야함
    bytes challenges; // 문제. ex) 0xab
  }

 

 

 

BetInfo 구조체를 넣어줄 큐를 구현

  // 매핑으로 큐를 구현하기 위한 변수
  uint256 private _tail;
  uint256 private _head;

  // 키는 uint, 값은 BerInfo인 매핑
  mapping(uint256 => BetInfo) private _bets;

index - value의 쌍을 mapping을 통해 구현한다.

 

 

 

상금 액수 조회하기

  uint256 private _pot;
  
  // 스마트 컨트랙의 변수를 가져와서 쓰려면 view 키워드를 쓴다
  // 더 자세히는, Storage 자료를 읽어서 보여줄때 사용. 수정할때는 사용x
  // Storage 자료는 블록에 영구적으로 기록된 값(하드디스크)
  // 반대인 Memory 자료는 임시로 저장되는 값(RAM)
  function getPot() public view returns (uint256 value) {
    return _pot;
  }

 

 

 

베팅하기

  // 이벤트 객체
  event BET(uint256 index, address bettor, uint256 amount, bytes1 challenges, uint256 answerBlockNumber);

  function bet(bytes challenges) public payable returns (bool result) {
    // 베팅할 금액(0.005ETH)이 제대로 왔는지 확인
    // require는 if역할, msg.value는 컨트랙트가 받은금액, 'Not Enough ETH'은 조건이 false때 출력할 문구
    require(msg.value == BET_AMOUNT, 'Not Enough ETH');

    // 큐에 베팅정보를 넣기
    require(pushBet(challenges), 'Fail to add a new Bew Info');

    // 이벤트 객체를 체인에 기록한다
    // (몇번째 배팅인지, 누가 배팅했는지, 얼마 배팅했는지, 어떤글자로 베팅했는지, 어떤 블록에 정답이 있는지)
    emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    return true;
  }

event와 emit

트랜잭션이 완료되면 트랜잭션 영수증을 발행한다.

영수증에는 트랜잭션 실행 동안 발생했던 모든 행동로그가 기록되어 있다.

event는 이런 로그를 만들기 위한 객체

emit은 만들어진 event 객체를 체인에 올리기.

더보기

 

 

 

 

베팅정보들을 담은 큐에서 베팅정보 1개 꺼내오기

  function getBetInfo(uint256 index)
    public
    view
    returns (
      uint256 answerBlockNumber, // 반환값1
      address bettor, // 반환값2
      bytes challenges // 반환값3
    )
  {
    BetInfo memory b = _bets[index]; // memory형 변수는 함수가 끝나면 지워짐, storage형 변수는 블록에 영영 기록됨
    answerBlockNumber = b.answerBlockNumber; // 반환값1
    bettor = b.bettor; // 반환값2
    challenges = b.challenges; // 반환값3
  }

 

 

 

베팅큐에 베팅정보 push

  function pushBet(bytes challenges) internal returns (bool) {
    BetInfo memory b; // 베팅정보를 하나 생성하고 세팅한다
    b.bettor = msg.sender; // 함수 호출한 사람
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number는 현재 이 트랜잭션이 들어가게되는 블록넘버를 가져온다
    b.challenges = challenges; // 내가 베팅한 값

    _bets[_tail] = b; // 큐에 넣고
    _tail++; // 테일 포인터 조정

    return true;
  }

 

 

 

베팅큐에서 베팅정보 1개 pop

  function popBet(uint256 index) internal returns (bool) {
    // 스마트컨트랙에서 덧셈이든, 뺄셈이든 하면 가스를 소모한다.
    // delete를 하면 가스를 돌려받는다. 왜? 이더리움 블록체인에 저장하고 있는 데이터를 더 이상 저장하지 않겠다는 것이기에
    // 즉, 상태 데이터베이스에 있는 값을 그냥 가져오겠다는 것이기에
    // 그러니 필요하지 않은 값이 있다면 delete를 해주자
    delete _bets[index];
    return true;
  }

 

 

 

 


3. 전체 코드

pragma solidity >=0.4.21 <0.6.0;

contract Lottery {
  struct BetInfo {
    uint256 answerBlockNumber; // 맞출 블록의 넘버
    address payable bettor; // 돈을 건 사람 주소, 특정주소에 돈을 보내려면 payable을 써줘야함
    bytes challenges; // 문제. ex) 0xab
  }

  // 매핑으로 큐를 구현하기 위한 변수
  uint256 private _tail;
  uint256 private _head;

  // 키는 uint, 값은 BerInfo인 매핑
  mapping(uint256 => BetInfo) private _bets;

  address public owner;

  uint256 internal constant BLOCK_LIMIT = 256; // 블록해시를 확인할 수 있는 제한
  uint256 internal constant BET_BLOCK_INTERVAL = 3; // 2번 블록에서 베팅을 하면 5번 블록에서 결과가 나온다
  uint256 internal constant BET_AMOUNT = 5 * 10**15; // 0.005ETH

  uint256 private _pot;

  // 이벤트 로그들을 한번에 모을 수 있다, BET이라는 로그들을 찍어줘
  // (몇번째 배팅인지, 누가 배팅했는지, 얼마 배팅했는지, 어떤글자로 베팅했는지, 어떤 블록에 정답이 있는지)
  event BET(uint256 index, address bettor, uint256 amount, bytes1 challenges, uint256 answerBlockNumber);

  constructor() public {
    owner = msg.sender; // msg.sender는 전역변수
  }

  function getPot() public view returns (uint256 value) {
    // 스마트 컨트랙의 변수를 가져와서 쓰려면 view 키ㅕ드를 쓴다
    return _pot;
  }

  // Bet (베팅하기)
  /*
    @dev 베팅을 한다. 유저는 0.005ETH를 보내야하고 베팅용 1byte 글자를 보낸다, 큐에 저장된 베팅 정보는 이후 distribute 함수에서 해결한다
    @param challenges 유저가 베팅하는 글자
    @return 함수가 잘 수행되었는지 확인하는 bool 값
    */
  function bet(bytes challenges) public payable returns (bool result) {
    // 돈이 제대로 왔는지 확인
    // require는 if역할, msg.value는 컨트랙트가 받은금액, 문자열은 조건이 false때 출력할 문구
    require(msg.value == BET_AMOUNT, 'Not Enough ETH');

    // 큐에 베팅정보를 넣기
    require(pushBet(challenges), 'Fail to add a new Bew Info');

    // 이벤트 로그를 찍는다
    // (몇번째 배팅인지, 누가 배팅했는지, 얼마 배팅했는지, 어떤글자로 베팅했는지, 어떤 블록에 정답이 있는지)
    emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    return true;
  }

  // 베팅한 값을 큐에 저장함

  // Distribute (검증하기)
  // 베팅한 값을 토대로 결과값을 검증
  // 검증결과가 틀리면 팟머니에 돈을 넣고, 맞으면 돈을 유저에게 준다

  // 베팅정보들을 담고있는 큐에서 베팅정보 가져오기
  function getBetInfo(uint256 index)
    public
    view
    returns (
      uint256 answerBlockNumber,
      address bettor,
      bytes challenges
    )
  {
    BetInfo memory b = _bets[index]; // memory형 변수는 함수가 끝나면 지워짐, storage형 변수는 블록에 영영 기록됨
    answerBlockNumber = b.answerBlockNumber; // 반환값1
    bettor = b.bettor; // 반환값2
    challenges = b.challenges; // 반환값3
  }

  // 큐 push
  function pushBet(bytes challenges) internal returns (bool) {
    BetInfo memory b; // 베팅정보를 하나 생성하고 세팅한다
    b.bettor = msg.sender; // 함수 호출한 사람
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number는 현재 이 트랜잭션이 들어가게되는 블록넘버를 가져온다
    b.challenges = challenges; // 내가 베팅한 값

    _bets[_tail] = b; // 큐에 넣고
    _tail++; // 테일 포인터 조정

    return true;
  }

  // 큐 pop
  function popBet(uint256 index) internal returns (bool) {
    // delete를 하면 가스를 돌려받는다. 왜? 상태데이터베이스에 저장된 값을 그냥 뽑아오겠다는 것이기에
    // 그러니 필요하지 않은 값이 있다면 delete를 해주자
    delete _bets[index];
    return true;
  }
}

정말 오~랜만에 글을 쓴다.

써야지 써야지 하면서 너무 미뤘다.

 

 

 

더 이상 글을 안쓰면 블로그를 영영 잊게될까봐 다시 자리에 앉았다.

그래도 그간 쪼잡쪼잡 이거저거 맛보기는 하고있었는데 그 중 하나인 이더리움 dapp 개발 과정을 써볼까 한다.

유튜브 강좌를 보며 따라하는 것이고.. 그저 따라할 뿐...! 아직 어렵다

 

코딩하는 나의 모습

 

내가 보며 따라하는 강좌는 요거다.

https://youtu.be/Ud3_OrxNPDg

 

 


0. What we gonna do?

스마트컨트랙트를 기반으로하는 디앱을 만들어보는 프로젝트.

디앱이란?

대충 블록체인을 활용한 애플리케이션.

Decentralized App

 

 

 


1. Lottery Dapp preview

로또게임(?)을 만들건데 스마트컨트랙을 활용하여 만들 것이다.

 

이 프로젝트에서 우리는

  • 솔리디티를 사용한 스마트컨트랙 만들기
  • 배포스크립트 만들기
  • 테스트코드 만들기
  • 프론트에 연동하기

를 하게된다.

 

 

스마트컨트랙을 작성하여 블록체인에 올린다는 것은,

자바로 백엔드 코드를 작성하여 EC2에 올린다는 것과 같다.

 

블록체인도 결국 분산된 무수히 많은 컴퓨터들이니 블록이 곧 서버역할을 하는 셈이다. 스마트컨트랙은 코드역할이고.

따라서 기존 애플리케이션의 흔한 구조인

[프론트엔드]-[백엔드]가

[프론트엔드]-[블록체인]이 되는 것이다.

 

 

단, 경우에 따라서 디앱이라도 [프론트엔드]-[백엔드]-[블록체인]처럼 백엔드를 추가하는 구조도 많은데 아무래도 블록체인상에서 코드를 실행시키기 위해서는 가스비라는 비용이 발생하기에 이런 비용적인 측면을 개선하고자 백엔드단을 추가하기도 한다고 한다.

(블록체인을 구성하는 노드(컴퓨터)들을 제공하는 사람들은 땅파서 장사하는게 아니기에.. 이들에게 가스비라는 대가를 지불하고 우리의 코드를 실행시켜달라고 하는 것)

 

 

 

 

아무튼, 서론이 길었다.

 

우리가 만들 로터리 디앱은 어떤 프로젝트냐면,

https://youtu.be/div91ADl_7k

시연영상

 

 

블록해시 맞추기 게임이다.

 

내가 만든 트랜잭션이 들어가게 되는 블록보다 3개 뒤 블록의 블록해쉬를 때려맞추는 게임(재미는 없을듯)

 

 

예를들자면

  1. 내가 0xab라고 베팅을 한다.
  2. 이 베팅은 하나의 트랜잭션으로서 블록에 포함된다.
  3. 만약 이 트랜잭션이 3번 블록에 포함됐다고 하자.
  4. 3번 블록의 3개 뒷 블록인 6번 블록의 블록해쉬값이 0xab라면 내가 이긴 것이다.
  5. 즉 6번 블록이 생성완료되고 7번블록의 생성이 시작된 시점부터 게임결과를 알 수 있다.

 

pot머니라고 부르는 베팅금액을 가져가는 규칙은 이러하다.(전부 0.005ETH씩 베팅한다)

  1. 6번 블록이 완성되었을때 유저들이 베팅한 돈을 팟머니에 쌓는다.
  2. 맞춘사람이 여러명일 경우 첫번째로 맞춘 사람이 가져간다.
  3. 두글자중(ab중에서) 하나만 맞추었다면 돈을 돌려준다.
  4. 결과값을 검증할 수 없는 경우가 발생하는 경우 돈을 돌려준다.

 

 

동작으로만 보면 그닥 복잡하진 않아보인다.

이 프로젝트의 핵심은 스마트컨트랙을 작성하고 이를 배포하여 체인과 상호작용하는 것에 있다.

 

 

 

 


2. 개발 환경

  • NodeJS : 배포와 테스트 코드를 작성하기 위해
  • VSCode : ide 이거씀
  • Truffle : 솔리디티를 컴파일하고 스마트컨트랙을 테스트, 배포할 수 있는 블록체인 개발 프레임워크이다. Truffle 설치시 solc-js가 설치된다(JS기반의 솔리디티 컴파일러)
  • ganache-cli : 간이 블록체인. 근데 이제 내 PC 메모리로 실행되는. 블록체인을 쉽게 mock을 해놓은 툴(테스트 용도), 즉 테스트와 개발에 사용하는 이더리움 RPC 클라이언트.(테스트용 노드(서버)의 느낌으로 이해하면 되려나..?)

 

ganache-cli(편의상 가나슈라고 하겠다)를 실행하면 로컬에서 블록체인 환경이 실행되고 여기서 사용할 수 있는 계정 10개가 제공된다. 

 

이 간이 블록체인을 켜기 위해서는 콘솔창에서

ganache-cli -d -m tutorial

를 실행해준다

 

-d 옵션을 주면 생성되는 니모니깅 동일하고 주소와 키값이 동일하다

-m 옵션은 지정된 HD 지갑 니모닉 값으로 초기 주소를 생성하기 위해 사용한다.

우리는 니모닉으로 tutorial을 사용했다.

보통 니모닉은 12개 단어를 사용하지만 1개만 써도 된다(?)

 

즉 -d, -m 옵션을 사용하고 니모닉 단어(tutorial)를 사용하면 항상 같은 지갑주소가 생성된다.

+ Recent posts