[Blockchain] 크립토좀비 lesson2 (Solidity)

공효은·2022년 2월 25일
0

Blockchain

목록 보기
7/10
post-thumbnail

lesson2 에서는 좀비가 먹이를 먹고 먹이의 dna와 자신의 dna로 돌연변이 좀비로 태어난다.

매핑과 주소

데이터베이스에 저장된 좀비들에게 주인을 설정하여 우리 게임을 멀티 플레이어 게임으로 만들자!

이걸하려면 mapping과 address 라는 2가지 새로운 자료형이 필요하다.

주소

이더리움 블록체인은 은행 계좌와 같은 계정들로 이루어져 있다. 계정은 이더리움 블록체인상의 통화인 이더의 잔액을 가진다. 실제 은행 계좌에서 다른 계좌로 돈을 송금할 수 있듯, 계정을 통해 다른 계정과 이더를 주고 받을 수 있다.

각 계정은 은행 계좌 번호와 같은 주소를 갖고 있다. 주소는 특정 계정을 가리키는 고유 식별자로, 다음과 같이 표현된다.

0x0cE446255506E92DF41614C46F1d6df9Cc969183

"주소는 특정 유저(혹은 스마트 컨트랙트) 가 소유한다."

따라서 주소를 우리 좀비들에 대한 소유권을 나타내는 고유 ID로 활용할 수 있다. 유저가 앱을 통해 새로운 좀비를 생성하면 좀비를 생성하는 함수를 호출한 이더리움 주소에 그 좀비에 대한 소유권을 부여 한다.

매핑

레슨 1에서 구조체와 배열을 살펴봤다. 매핑은 솔리디티에서 구조화된 데이터를 저장하는 또 다른 방법이다.

다음과 같이 매핑을 정의한다.

//금융 앱용으로, 유저의 계좌 잔액을 보유하는 uint 를 저장한다.
mapping (address => uint) public accountBalance;

//혹은 userID로 유저 이름을 저장/검색하는 데 매핑을 쓸 수도 있다.
mapping (uint => string) userIdToName;

매핑은 기본적으로 키-값(key-value) 저장소로, 데이터를 저장하고 검색하는데 이용된다. 첫번째 예시에서 키는 address 이고 값은 uint 이다. 두번째 예시에서 키는 uint 이고 값은 string dlek.

Msg.sender

솔리디티에는 모든 함수에서 이용 가능한 특정 전역 변수가 있다. 그 중의 하나가 현재 함수를 호출한 사람(혹은 스마트 컨트랙트)의 주소를 가리키는 msg.sender 이다.

솔리디티에서 함수 실행은 항상 외부 호출자가 시작한다. 컨트랙트는 누군가가 컨트랙트의 함수를 호출할 때까지 블록체인 상에서 아무것도 안하고 있다. 그러니 항상 msg.sender가 있어야한다.

msg.sender를 이용하고 mapping을 업데이트하는 예시

mapping(address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  //msg.sender에 대해 myNumber 가 저장되도록 favoriteNumber 매핑을 업데이트 한다.
  favoriteNumber[msg.sender] = _myNumber;
  //데이터를 저장하는 구문은 배열로 데이터를 저장할 때와 동일하다.
}
  function whatIsMyNumber() public view returns (uint) {
    return favoriteNumber[msg.sender];
                  
  }

이 간단한 예시에서 누구나 setMyNumber을 호출하여 본인의 주소와 연결된 우리 컨트랙트 내에 uint를 저장할 수 있다.

msg.sender를 활용하면 이더리움 블록체인의 보안성을 이용할 수 있게 된다. 즉, 누군가 다른 사람의 데이터를 변경하려면 해당 이더리움 주소와 관련된 개인키를 훔치는 것 밖에는 다른 방법이 없다.

Require

require를 활용하면 특정조건이 참이 아닐 때 함수가 에러 메시지를 발생하고 실행을 멈춘다.

function sayHiToVitalik(string _name) public returns(string){
  //_name이 "Vitalik" 인지비교한다. 참이 아닐 경우 에러 메시지를 
  //발생하고 함수를 벗어난다.
  //(참고: 솔리디티는 고유의 스트링 비교 기능을 가지고 있지 않기 때문에
  //스트링의 keccak256 해시값을 비교하여 스트링 값이 같은지 판단한다)
  require(keccak256(_name) == keccak256("Vitalik"));
  //참이면 함수 실행을 진행한다.
  return "Hi!"
}

sayHiToVitalik("Vitalik")로 이 함수를 실행하면 "Hi!"가 반환될 것이다. "Vitalik"이 아닌 다른 값으로 이 함수를 호출할 경우, 에러 메시지가 뜨고 함수가 실행되지 않는다.

그러므로 require는 함수를 실행하기 전에 참이어야 하는 특정 조건을 확인하는 데 있어서 유용하다.

솔리디티에서 값을 비교할 때 어떤 항이 먼저 오느냐는 중요하지 않다.

상속

엄청나게 긴 컨트랙트 하나를 만들기 보다는 코드를 잘 정리해서 여러 컨트랙트에 코드 로직을 나누는 것이 합리적일 떄가 있다.
이를 보다 관리하기 쉽도록 하는 솔리디티 기능이 바로 컨트랙트 상속이다.

contract Doge {
  function catchphrase() public returns (string) {
    return "So Wow cryptoDoge";
  }
}

contract BabyDoge is Doge {
  function anotherCathchphrase() public returns (string) {
    return "Such Moon BayDoge";
  }
}

BabyDoge 컨트랙트는 Doge 컨트랙트를 상속한다. 즉 BabyDoge 컨트랙트를 컴파일 해서 구축할 때, BabyDoge 컨트랙트가 catchphrase() 함수와 anotherCatchphrase() 함수에 모두 접근할 수 있다. (Doge 컨트랙트에 정의되는 다른 어떤 public함수가 정의되어도 접근이 가능함

상속 개념은 "고양이는 동물이다"의 경우처럼 부분집합 클래스가 있을때 논리적 상속을 위해 활용할 수 있다. 하지만 도일한 로직을 다수의 클래스로 분할해서 단순히 코드를 정리할 때도 활용할 수 있다.

Import

코드가 길어질때 여러 파일로 나누어 정리하면 관리하기 편하다. 보통 이런 방식으로 솔리디티 프로젝트의 긴 코드를 처리한다.

다수으 파일이 있고 어떤 파일을 다른 파일로 불러오고 싶을 때, 솔리디티는 import 라는 키워드를 이용한다.

import "./someothercontract.sol";

contract newContract is SomOtherContract{
}

이 컨트랙트와 동일한 폴더에(이게 ./가 의미 하는 바임)
someothercontract.sol이라는 파일이 있을때, 이 파일을 컴파일러가 불러오게 된다.

Storage vs Memory

솔리디티에는 변수를 저장할 수 있는 공간으로 storage 와 memory 두가지가 있다.

Storage는 블록체인 상에 "영구적으로 저장"되는 변수를 의미한다.
Memory "임시적으로 저장되는 변수"로, 컨트랙트 함수에 대한 외부호출들이 일어나는 사이에 지워지지. 두 변수는 각각 컴퓨터 하드 디스크와 RAM과 같다.

대부분의 경우 이런 키워드들을 이용할 필요가 없다. 왜냐면 솔리디티가 알어서 처리해 주기 때문이지. 상태 변수(함수 외부에 선언된 변수)는 초기 설정상 storage로 선언되어 블록체인에 영구적으로 저장되는 반면, 함수 내에 선언된 변수는 memory로 자동 선언되어서 함수 호출이 종료되면 사라진다.

하지만 이 키워드들을 사용해야 하는 때가있다. 바로 함수 내의 구조체와 배열을 처리할 때 이다.

contract SandwichFactory {
  struct Sandwich {
    string name;
    string status;
  }
  Sandwich[] sandwiches;
  
  function eatSandwich(unint _index) public {
    //Sandwich mySandwich = sandwiches[_index]     
    
    //꽤 간단해 보이나, 솔리디티는 여기서
    //storage나 memory를 명시적으로 선언해야한다는 경고 메시지를 발생한다.
    // 그러므로 storage 키워드를 활용하여 다음과 같이 선언해야 한다.
    Sandwich storage mySandwich = sandwiches[_index];
    // 이 경우 mySandwich는 저장된 sandwiches[_index] 를 가리키는 포인터 이다. 
    //그리고
    mySandwich.staus = "Eaten!"
    //이 코드는 블록체인 상에서 sandwiches[_index] 을 영구적으로 변경한다.
    //단순한 복사를 하고자 한다면 memory를 이용하면 된다.
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // 이 경우, anotherSandwich 는 단순히 메모리에 데이터를 복사하는 것이 된다.
    anotherSandwich.status = "Eaten!";
    // 이 코드는 임시 변수인 anotherSandwich를 변경하는 것으로
    // sandwiches[_index + 1] 에는 아무런 영향을 끼치지 않는다. 그러나 다음과 같이 코드를 작성 할 수 있다.
    sandwiches[_index + 1] = anotherSandwich;
    //이는 임시 변경한 내용을 블록체인 저장소에 저장하고자 하는 경우이다.

명시적으로 storage나 memory를 선언할 필요가 있는 경우가 있다.

함수 접근 제어자

솔리디티에는 public private 이와에도 internal 과 external 이라는 함수 접근 제어자가 있다.
internal 은 함수가 정의된 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능하다는 점을 제외하면 private와 동일하다.

external은 함수가 컨트랙트 바깥에서만 호출될 수 있고 컨트랙트 내의 다른 함수에 의해 호출될 수 없다는 점을 제외하면 public과 동일하다.

internal이나 external 함수를 선언하는건 private 과 public함수를 선언하는 구문과 같다.

contract Sandwich {
  uint private sandwichesEaten = 0;
  
  function eat() internal {
    sandwichesEaten++;
  }
}

contract BLT is Sandwich {
  uint private baconSandwichesEaten = 0;
  
  function eatWithBacon() public returns (string) {
    baconSandwichesEaten++;
    //eat 함수가 internal로 선언되었기 때문에 여기서 호출이 가능하다.
    eat();

다른 컨트랙트와 상호작용하기

블록체인 상에 있으면서 우리가 소유하지 않는 컨트랙트와 우리컨트랙트가 상호작용을 하려면 우선 인터페이스를 정의해야한다.

contract LuckyNumber {
  mapping(address => uint) numbers;
  
  function setNum(uint _num) public {
    numbers[msg.sender] = _num;
  }
  
  function getNum(address _myAddress) public view returns (uint){
    return numbers[_myAddress];
  }
}

이 컨트랙트는 아무나 자신의 행운의 수를 저장할 수 있는 간단한 컨트랙트 이고, 각자의 이더리움 주소와 연관이 있다. 이 주소를 이용해서 누구나 그 사람의 행운의 수를 찾아 볼 수 있다.

이제 getNum 함수를 이용하여 이 컨트랙트에 있는 데이터를 읽고자 하는 external 함수가 있다고 해 보자.

먼저, LuckyNumber 컨트랙트의 인터페이스를 정의할 필요가 있다.

contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

약간 다르지만, 인터페이스를 정의하는 것이 컨트랙트를 정의하는 것과 유사하다. 먼저, 다른 컨트랙트와 상호작용하고자 하는 함수만을 선언할 뿐(이 경우, getNum이 바로 그러한 함수이다.) 다른 함수나 상태변수를 언급하지 않는다.

다음으로, 함수 몸체를 정의하지 않는다. 중괄호 {} 를 쓰지 않고 함수 선언을 세미콜론으로 간단하게 끝낸다.

그러니 인터페이스는 컨트랙트 뼈대처럼 보인다고 할 수 있다. 컴파일러도 그렇게 인터 페이스를 인식한다.

우리의 dapp 코드에 이런 인터페이스를 포함하면 컨트랙트는 다른 컨트랙트에 저의된 함수의 특성, 호출방법, 예상되는 응답 내용에 대해 알 수 있게 된다.

인터페이스 활용하기

아래와 같이 인터페이스가 정의되면

contract NumberInterface {
	function getNum(address _myAddress) public view returns(uint)
}

다음과 같이 컨트랙트에서 인터페이스를 이용할 수 있다.

contract MyContract {
     // 이더리움 상의 FavoriteNumber 컨트랙트 주소이다.
	address NunberInterfaceAddress = 0xab38...
 
	NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  // 이제 numberContract는 다른 컨트랙트를 가리키고 있다.
  function someFunction() public {
    //이제 numberContract가 가리키고 있는 컨트랙트에서 getNum 함수를 호출할 수 있다.
    uint num = numberContract.getNum(msg.sender);
    //그리고 여기서 num 으로 무언가를 할 수 있다.
	}
}

이런 식으로 MyContract가 이더리움 블록체인상의 다른 어떤 컨트랙트와도 상호작용할 수 있다. 물론 상호작용하는 함수가 public 이나 external 로 선언되어 있어야 한다.

다수의 반환값 처리하기

어떻게 다수의 반환값을 처리하는가! (솔리디티는 다수 값을 반환할 수 있다.)

function multipleReturns() internal returns(uint a, uint b, uint c){
  return (1,2,3);
}

function processMultipleReturns() external {
  uint a;
  uint b'
  uint c;
  // 다음과 같이 다수 값을 할당한다
  (a,b,c) = multipleReturns();
}

//혹은 단 하나의 값에만 관심이 있을 경우
function getLastReturnValue() external {
  uint c;
  (,,c) = multipleReturns();
}

if 문

솔리디티에서 if 문은 자바스크립트의 if문과 동일하다.

function eatBLT(string sandwich) public {
	if(keccak256(sandwich) == keccak256("BLT")){
      eat();
    }
}

contract

zombiefactory.sol

pragma solidity ^0.4.19;

contract ZombieFactory {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

zombiefeeding.sol

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);

  // 여기에 있는 함수 정의를 변경:
  function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    // 여기에 if 문 추가
    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    // 여기에 있는 함수 호출을 변경: 
    feedAndMultiply(_zombieId, kittyDna);
  }

}

자바스크립트를 활용한 구현

우리 컨트랙트를 이더리움에 구축할 준비가 되면 ZombieFeeding 컨트랙트만 컴파일해서 구축하면 된다. 왜냐면 이 컨트랙트가 ZombieFactory를 상속하는 우리의 마지막 컨트랙트이고 두 컨트랙트에 있는 public 메소드를 모두 접근 할 수 있기 때문이다.

자바스크립트와 web3.js를 활용하여 우리의 컨트랙트와 상호작용하는 예시를 살펴보자!

var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = */ our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)

//우리 좀비의 ID와 타겟 고양이 ID를 가지고 있다고 가정하면
let zombieId = 1;
let kittyId = 1;

//크립토 키티의 이미지를 얻기 위해 웹 API에 쿼리를 할 필요가 있다.
//이 정보는 블록체인이 아닌 크립토 키티 웹 서버에 저장되어 있다.
//모든것이 블록체인에 저장되어 있으면 서버가 다운되거나 크립토키티 API가 바뀌는 것이나 
//크립토키티 회사가 크립토 좀비를 싫어해서 고양이 이미지를 로딩하는 걸 막는 등을 걱정할 필요가 없다.

let apiUrl =  "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
  let imgUrl = data.image_url
  //이미지를 제시하기 위해 무언가를 한다.
})

$(".kittyImage").click(function(e){
  //우리 컨트랙트의 feedOnKitty 메소드를 호출한다.
  ZombieFeeding.feedOnKitty(zombieId, kittyId)
})

//우리의 컨트랙트에서 발생 가능한 NewZombie 이벤트에 귀를 기울여서 이벤트 발생 시 이벤트를 제시할 수 있도록 한다.

ZombieFeeding.NewZombie(function(error,result){
  if(error) return
  //이함수는 레슨 1에서와 같이 좀비를 제시한다.
  generateZombie(result.zombieId, result.name, result.dna)
})
profile
잼나게 코딩하면서 살고 싶어요 ^O^/

0개의 댓글