크립토 좀비 1-3. 고급 솔리디티 개념

2022. 3. 30. 16:20개발 잡부/블록체인

728x90
 

#1 Solidity Tutorial & Ethereum Blockchain Programming Course | CryptoZombies

CryptoZombies is The Most Popular, Interactive Solidity Tutorial That Will Help You Learn Blockchain Programming on Ethereum by Building Your Own Fun Game with Zombies — Master Blockchain Development with Web3, Infura, Metamask & Ethereum Smart Contracts

cryptozombies.io


1. 컨트랙트의 불변성

이 챕터에서는 스마트 컨트랙트의 불변성에 대해서 알아본다.
스마트 컨트랙트는 다음의 특징을 지닌다.

1. 배포한 코드는 항상 블록체인에 영구적으로 존재한다.
2. 어떤 방식으로든 수정, 삭제가 불가능하다. (그래서 수정한다면 새로운 블록에 배포해야 한다.)
3. 불변하기 때문에 항상 같은 결과임을 확신할 수 있다.

외부 의존성

만약, 우리가 입력한 크립토 키티의 주소가 변경된다면
어떻게 할까?

코드에 주소를 박아버리면
수정이 불가능하기 때문에 해결이 되지 않는다.

그래서 외부에서 주소를 변경할 수 있게
코드를 바꿀 것이다.


정답

...

  // 1. 이 줄을 지우게:
  // 2. 여기서 대입을 빼고 그냥 선언으로 바꾸게:
  KittyInterface kittyContract;

  // 3. 여기 setKittyContractAddress 메소드를 추가하게
  function setKittyContractAddress(address _address) external {
    kittyContract = KittyInterface(_address);
  }
  
...

2. 소유 가능한 컨트랙트

이렇게 선언된 external 함수는 누구나 사용이 가능하다.
즉, 어느 누군가가 주소를 변경하는 것이 가능하다는 뜻이다.

그래서 컨트랙트를 '소유 가능'하게 하는 것이다.
특정 소유자만 컨트롤 할 수 있게 하는 것이다.


OpenZippelin의 Ownable 컨트랙트

Ownable 컨트랙트는 커뮤니티에서 검증받은 라이브러리다.

코드를 읽어보면 대략
owner가 있고,
새로 온 msg.sender가 owner인지 확인한다.
그리고 owner를 바꿔주는 함수도 존재한다.

단, modifier란 키워드가 나온다.
modifier는 다른 함수들에 대한 접근을 제어하기 위한 유사함수다.
주로 함수 실행 전에 요구사항 충족 여부를 확인하는데에 사용한다.

_;indexed 키워드는 나중에 알아보자.


컨트랙트 생성자

컨트랙트 생성자는 컨트랙트가 생성된 경우
호출이 된다.

이 경우 msg.sender는 배포한 사람이 된다.


정답

....

// 1. 여기서 import하게
import "./ownable.sol";

// 2. 상속을 추가하게:
contract ZombieFactory is Ownable {

...

3. onlyOwner 함수 제어자

ZombieFactory가 Ownable을 상속하고 있기 때문에,
ZombieFeeding또한 Ownable 속성을 사용할 수 있다.


함수 제어자

함수 제어자는 함수처럼 생겼지만,
function대신 modifier 키워드를 사용한다.

그리고 _;는 함수 제어자를 상속받은 함수로 돌아가는 키워드다.


당부의 말

이 분산화된 시스템이라는 것은 참으로 매력적이면서도 위험하다.

우리가 하는 일반적인 게임은 회사에서 데이터를 관리한다.
그래서 문제가 생겼을 때 처리를 해줄수도 있고, 악용할수도 있다.

그렇기 때문에 분산화된 코드는 소유자가 악용할 수 없도록 코드를 짜야한다.
하지만 소유자가 코드를 제어할 수 없다면,
위와 같이 주소를 변경해야할 때 대처하지 못할 것이다.

그러므로 잘 작동하도록 제어할 수만 있는 선에서
코드를 짜는 것이 중요하다.


정답

...

  // 이 함수를 수정하게:
  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }
  
...

4. 가스 (Gas)

가스란 이더리움 DApp이 사용하는 연료다.

사용자들이 DApp의 함수를 실행할 때마다 화폐를 지불해야 한다.

함수의 실행 비용은 논리구조가 얼마나 복잡한지에 달려있다.
각 연산은 소모되는 가스 비용이 있고,
연산에 소모되는 컴퓨팅 자원이 곧 비용이 된다.
(한마디로 오래 걸리면 비싸다)

그래서 코드 최적화가 중요하다.
코드가 복잡해질수록 비용이 비싸질테니.


가스가 필요한 이유

이더리움은 개별 노드가 함수의 출력값을 검증하기 위해
그 함수를 실행해야 한다.

그런데 누군가가 트롤링한다면?
이런 경우를 위해 호출할 때마다 비용이 부과되도록 했다.
그리고 일종의 '이더리움'이라는 컴퓨터의 사용료라고 봐도 된다.


가스비를 줄이는 방법

전에 배웟듯 uint에는 uint8, uint16, uint32등이 있다.
하지만 우리가 uint변수를 선언하면 종류에 관계없이 256비트로 할당된다.

그렇기 때문에 변수 선언에서는 가스비 절약이 안되지만,
구조체 내부에서는 절약이 가능하다.


정답

...

        // 여기 새 데이터를 입력하게
        uint32 level;
        uint32 readyTime;
        
...

5. 시간 단위

우선 위에서 선언한 두가지 프로퍼티를 알아보자.

level은 전투 레벨이다.
readyTime은 공격 재사용시간이다.


시간 단위 (Time units)

솔리디티는 시간을 다룰 수 있는 단위계를 기본적으로 제공한다.
now변수를 사용하면 UNIX 타임스탬프값 (256비트)을 얻을 수 있다.
(경고: 2038년 이후에는 32비트는 한계에 다다른다.)

그리고 아래의 단위들은 각각 초로 계산된 값으로 치환된다.

1 years: ...
1 weeks: 604800
1 days: 86400
1 hours: 3600
1 minutes: 60
1 seconds: 1

그리고 다음과 같이 사용할 수 있다.

uint lastUpdated;

// `lastUpdated`를 `now`로 설정
function updateTimestamp() public {
  lastUpdated = now;
}

// 마지막으로 `updateTimestamp`가 호출된 뒤 5분이 지났으면 `true`를, 5분이 아직 지나지 않았으면 `false`를 반환
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
  // return (now >= (lastUpdated + 5  * 60));과 동일
}

정답

...

    // 1. `cooldownTime`을 여기에 정의하게
    uint cooldownTime = 1 days;
    
...

        // 2. 아래 줄을 업데이트하게:
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;

...

6. 좀비 재사용 대기 시간

개발하고자 하는 주요 로직은 다음과 같다.

1. 먹이를 먹으면 좀비가 재사용 대기에 들어간다.
2. 좀비는 재사용 대기 시간이 지날 때까지 고양이를 먹을 수 없다.

구조체를 인수로 전달하기

private또는 internal 함수에 인수로서
구조체의 storage 포인터를 전달할 수 있다.
즉 함수 내부에서도 스토리지에 영향을 줄 수 있다는 얘기다.

문법은 인자를 줄 때,
자료형과 인자명 사이에 storage 키워드를 넣는다.

function _doStuff(Zombie storage _zombie) internal {
  // _zombie로 할 수 있는 것들을 처리
}

정답

...

 // 1. `_triggerCooldown` 함수를 여기에 정의하게
  function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }

  // 2. `_isReady` 함수를 여기에 정의하게
  function _isReady(Zombie storage _zombie) internal view returns (bool) {
    return (_zombie.readyTime <= now);
  }
  
...

7. Public 함수 & 보안

이러한 DApp 코드에서 가장 중요한 것은 역시 보안이다.
그리고 보안을 점검하는 가장 좋은 방안은
public과 external함수를 검사하는 것이다.

제어자가 없다면 나 말고도 누구나 호출할 수 있다는 얘기다.
이런 남용을 막는 방법은 함수를 internal로 바꾸는 것이다.


정답

...

  // 1. 이 함수를 internal로 만들게
  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    // 2. 여기에 `_isReady`를 확인하는 부분을 추가하게
    require(_isReady(myZombie));
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(_species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
    // 3. `_triggerCooldown`을 호출하게
    _triggerCooldown(myZombie);
  }
  
...

8. 함수 제어자의 또 다른 특징

이번에는 특정 레벨을 달성했을 때,
특별한 능력을 얻을 수 있게 할 것이다.


인수를 가지는 함수 제어자

말 그대로 함수 제어자 (modifier)에 인자를 넣는 방법이다.

일반 함수와 같이 인자를 받고,
인자를 넣어서 호출하면 된다.

// 인자 있는 함수 제어자 선언
modifier foo(uint bar) {}

// 인자 있는 함수 제어자 호출
function fooBar() foo(10) {}

(참고로 인자가 없다면, 호출할 때 ()를 빼도 된다.)


정답

...

  // 여기서 시작하게
  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }
    
...

9. 좀비 제어자

만들고자 하는 바는 다음과 같다.

1. 레벨 2 이상인 좀비는 이름을 바꿀 수 있다.
2. 레벨 20 이상인 좀비는 임의의 DNA를 줄 수 있다.

정답

...

  // 여기서 시작하게
  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

...

10. view 함수를 사용해 가스 절약하기

이 챕터에서는 View 함수를 이용해 가스 최적화를 하려고 한다.


View 함수는 가스를 소모하지 않는다.

오... 개꿀개꿀
View 선언이 된 함수는
로컬 이더리움 노드에 질의만 날리면 된다는 소리다.

이게 무슨 소린지 이해가 잘 안 됐는데,
우리가 장부를 가지고 있다면, 현재 데이터베이스 값들을 알 수 있다.
즉, 값을 수정해서 검증을 받아야 하는게 아니고,
현재 내 컴퓨터에 있는 장부를 읽기만 하면 되는 것이다.

단, 당연히 트랜잭션을 발생하는 다른 함수가
view 함수를 호출하면 비용이 든다.


정답

...

  // 자네의 함수를 여기에 만들게
  function getZombiesByOwner(address _owner) external view returns (uint[]) {
    
  }
  
...

11. Storage는 비싸다

storage연산과 쓰기 연산은 제일 비싼 것중에 하나다.
데이터를 쓰거나 바꿀때마다 데이터가 수정되기 때문이다.

그렇기 때문에 진짜 필요한게 아니면
storage에 쓰는 대신 memory에 쓰는게 좋다.


메모리에 배열 선언하기

메모리에 배열을 선언할 때는 이 두가지를 기억하자.

1. new 키워드를 사용한다.
2. 크기를 정해준다.
3. 변수명 앞에 memory를 붙여준다.

예제는 다음과 같다.

uint[] memory values = new uint[](3);

정답

...

    // 여기서 시작하게
    uint[] memory result = new uint[](ownerZombieCount[_owner]);

    return result;

...

12. For 반복문

우선 배열을 관리하는 것은 굉장히 어려운 일이다.
왜냐하면 솔리디티에서는 데이터를 쓸 때 비용이 크게 발생하는데,
중간에 있는 내용을 하나 삭제한다면 모든 내용을 한 칸씩 땡겨야 하기 때문이다.

그래서 우리는 기존 프로그래밍과 다른 접근방식을 사용해야 한다.
조회의 비중을 높이고 수정의 비중을 낮춰야 한다.
실제로 더 많이 컴퓨터를 사용하더라도,
수정을 하지 않으면 가스비가 들지 않기 때문이다.


for문 문법

솔리디티의 for문법은 자바스크립트와 유사하다.


정답

...

    // 여기서 시작하게
    uint counter = 0;
    
    for (uint i = 0; i < zombies.length; i++) {
      if (zombieToOwner[i] == _owner) {
        result[counter] = i;
        counter++;
      }
    }

...

후기

아주 굉장히 재미있는 챕터였다.
뭐랄까 다른 프로그래밍 언어에서 느낄수 없었던
솔리디티만의 매력이 느껴졌다.

최적화라는 분야는 정말 숙련도가 요구되는 부분이다.
센스와 예리함이 중요하기 때문에
파고들어 전문화할 부분인 것 같다.