크립토좀비는 솔리디티로 수집품 게임을 직접 만들어 스마트 컨트렉트를 작성해 보는 프로그램이다.
크립토좀비는 다음과 같은 좀비 공장을 가진다.
- 이 공장은 군대 내 모든 좀비의 데이터 베이스를 유지한다.
- 이 공장은 새로운 좀비를 생성하는 함수를 가진다.
- 각 좀비는 랜덤하고 독특한 외모를 가진다.
좀비의 DNA는 8356281049284737 와 같이 16자리의 정수로 표현되며, 이 숫자는 좀비가 가진 개별 특성과 매핑이 된다.
pragma solidity ^0.4.19;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
}
pragma솔리디티의 버전을 명시한다. 0.4.19 버전을 사용했다.
솔리디티는 컨드렉트 안에 싸여있다. 컨트렉트를 ZombieFactory라는 이름으로 생성했다.
컨트렉트는 자바의 클래스 개념을 떠올리면 된다.
컨트렉트 안에 unit타입의 상태변수를 선언했다.
상태변수는 컨트렉트 저장소에 영구적으로 저장된다.
즉 이더리움 블록체인에 기록이 되는 데이터다.
참고: 솔리디티에서 uint는 실제로 uint256, 즉 256비트 부호 없는 정수의 다른 표현. uint8, uint16, uint32 등과 같이 uint를 더 적은 비트로 선언할 수도 있다. 하지만 앞으로의 레슨에서 다루게 될 특수한 경우가 아니라면 일반적으로 단순히 uint를 사용.
복잡한 자료형을 만들기 위해서 구조체를 사용한다.
다음과 같이 좀비의 이름과 dna를 저장하는 구조체를 생성했다.
struct Zombie {
string name;
uint dna;
}
솔리디티는 다음과 같이 고정 길이를 가지는 정적배열과, 크기가 변하는 동적배열을 사용할 수 있다.
string[5] stringArray; // 고정 배열으로 5개의 스트링을 담을 수 있다 uint[] dynamicArray; // 동적 배열은 고정된 크기가 없으며 계속 크기가 커질 수 있다.
배열을 public으로 선언 할 수 있다.
이 배열을 사용하기위해 솔리디티는 자동적으로 getter를 생성한다.
public으로 배열을 생성하면 다른 컨트렉트들이 이 배열을 참조해서 사용할 수 있다.
...
Zombie[] public zombies;
function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
...
Zombie 라는 구조체를 선언하고, 이 구조체를 배열로 생성했다.
즉 좀비는 배열안에 여러마리가 들어갈 수 있다.
또한 public 이기 때문에 다른 컨트렉트가 좀비에 접근 할 수 있다.
function 을 사용해 함수를 선언할 수 있다.
보통 함수의 인자에는 _(언더스코프)를 사용해 전역변수와 구분한다.
private 키워드를 사용한 함수에도 _(언더스코프)를 사용한다.
값을 변경하지 않고, 조회만 하는 함수에는 view 키워드를 사용한다.
function sayHello() public view returns (string) {}
function sayHello() public view returns (string) {}
또한 함수에서 반환 받으려는 반환 값을 위와 같이 사용한다.
위 함수는 반환값을 string으로 정의했다.
함수가 앱에서 어떤 데이터도 접근하지 않는 경우 사용한다.
function _multiply(uint a, uint b) private pure returns (uint) { return a * b; }
위 함수는 어떤 것을 읽지도 않고, 다른 데이터에 접근하지도 않는다.
전달 된 인자에 대해서만 수행을 하기 때문에 pure 키워드를 사용한다.
다시 코드로 돌아가보자.
좀비의 이름과, dna를 받아서 좀비를 생성하는createZombie 함수를 선언했다.
function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
함수는 기본적으로 public을 사용해 다른 컨트렉트와 상호작용을 하지만 때에 따라 공격 취약점이 될 수 있다.
다른 컨트렉트가 좀비를 추가하는 것을 막기위해 private 함수로 선언하였다.
이더리움은 SHA3의 한 버전인 keccak256를 내장 해시 함수로 가지고 있다.
해시함수는 기본적으로 입력 스트링을 랜덤 256비트 16진수로 매핑한다.
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 keccak256("aaaab"); //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9 keccak256("aaaac");
위 예시를 보면 알 수 있듯이 입력값이 하나만 달라져도 완전히 다른 난수가 반환된다.
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
좀비의 dna를 생성하는 함수이다.
좀비의 dna는 keccak256를 사용해 난수 16진수를 생성하고 이를 uint으로 형변환하였다.
dna는 16자리 숫자이므로 % 로 계산해서 반환했다.
위 두 함수를 이용해 좀비를 생성하는 함수를 만들 수 있다.
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
위 함수는 public으로 생성했다.
name을 인자로 받아 dna를 생성하고 이어서 좀비의 이름과, dna를 인자로 가져가 좀비 배열에 추가한다.
이벤트는 컨트랙트가 블록체인 상에서 무언가 액션이 발생했을 때 수행한다.
event NewZombie(uint zombieId, string name, uint dna);
위와 같이 이벤트를 정의한다.
이벤트는 새로운 좀비가 추가되었을 떄 수행되어야하므로 _createZombie 함수에 위치한다.
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
NewZombie(id, _name, _dna);
}
id는 좀비 배열의 인덱스 숫자를 사용했다.
zombies.push() 는 새로 추가된 배열의 길이를 반환하므로 1을 하나 빼줌으로서 지금 추가하는 좀비의 인덱스를 반환하게 했다.
위 코드의 전체 소스는 다음과 같다.
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;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
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 {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
생성된 dna에 따라 좀비의 외관이 변경된다.
