즐코

Crypto Zombie / lesson1 - Overview 본문

BlockChain

Crypto Zombie / lesson1 - Overview

YJLEE_KR 2022. 7. 18. 23:06

크립트 좀비를 통해 solidity 데이터 타입 및 함수 등에 대해서 먼저 간단하게 정리하고자 한다.

여러 데이터 타입이 있지만 우선 기본적인 uint, bytes, struct, arrays만 짚고 넘어간다. bool은 그냥 불리언타입이라 생략한다.

다음 포스팅에선 크립토 좀비를 통해 mapping과 address 라는 살짝 특이한 데이터타입을 정리할 예정이다.  

 

1. uint

unsigned Integer 음수가 아닌 정수 

* int : 음수를 포함한 정수를 가리킨다. 

uint 뒤에 붙는 숫자는 비트를 의미한다. uint8, uint16, uint32 ~ uint256까지 8비트씩 증가한다.

이때, uint256을 uint라고 줄여 말한다.

ex) uint256 = 256비트(32바이트이자 64자리)짜리 정수

 

2. bytes

고정 크기 바이트

bytes 뒤에 붙는 숫자는 바이트를 의미한다. bytes1, bytes2... bytes32

byte === bytes1 

 

3. bytes / string

임의 길이의 데이터 (동적 크기 바이트)

string 은 utf-8 인코딩된 임의의 길이를 가진 문자열이다.

원래 solidity에 없던 자료형이었으나 memory 키워드와 함께 쓸 수 있게됨

 

4. struct

여러개의 속성을 가진 데이터를 표현할 때 쓴다. (변수의 그룹화?)

그 내부의 여러개의 속성도 데이터 타입을 명시해줘야한다.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

contract ZombieFactory {

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

    struct Zombie {
        string name;
        uint dna;
    }
}

 

- 위에서 만들어둔 struct Zombie의 인스턴스 생성 시엔 아래와 같이 struct 속성들의 타입에 맞춰서 넣어준다.

Zombie yjlee = Zombie('yjlee', 1230201020312313);

 

5. arrays

솔리디티에선 고정 배열과 동적 배열이 있다.

uint[2] : 2개의 uint 타입의 요소를 가진 고정된 길이의 배열을 의미

string[5] : 5개의 string 타입의 요소를 가진 고정된 길이의 배열을 의미

uint[] : 길이가 정해지지 않은 동적 배열

 

* public array : 솔리디티가 자동으로 getter 함수를 생성해주기 때문에 다른 컨트랙트도 이 배열을 읽어올 수가 있다.

contract ZombieFactory {

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

    struct Zombie {
        string name;
        uint dna;
    }
    
    Zombie[] public zombies;

}

 

 

 

 

접근 제한자

private vs public

 

솔리디티에서 function은 default가 public 이다. 누구나 컨트랙트 내의 함수를 호출하고 실행시킬 수 있다는 의미다.

하지만, 이럴 경우 공격에 취약하므로, 되도록이면 private으로 함수를 만들고, 네트워크 상에서 공개해도 되는 함수만 public으로 만드는 것이 좋다고 한다. 

zombies 배열에 Zombie 인스턴스를 추가하는 좀비생성 함수인 _createZombie 를 private한 함수로 만들어주었다. 

보통 private 함수는 함수명과 인자 앞에 underscore(_) 를 붙여주는게 convention이라고 하니 필수는 아니지만 따라주었다..

contract ZombieFactory {

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

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }
}

 

returns

: 함수가 리턴하는 값의 타입을 정해준다.

아래의 간단한 예시에선 리턴하는 값이 string이므로 returns (string memory) 를 붙여준다.

string greeting = "what's up!";

function sayHello() public returns (string memory){
   return greeting;
}

 

함수 제어자

view vs pure vs default

view 상태 변수를 사용, 조회는 가능하나 그 값을 바꾸지 않는 함수에 붙여준다. (읽기 전용이라고 보면 됨)
pure 상태 변수를 아예 사용하지 않는 함수에 붙여준다. (상태변수와 무관한 함수를 나타낼 때 쓴다고 보면 됨)
default (키워드 x) 상태 변수 값을 변경할때는 아무 키워드도 붙여주지 않는다.

 

 

이더리움 내장 hash 함수 keccak256

 

- 역할 : 이 해시함수는 들어온 값을 256비트의 16진수 숫자로 해시화해준다. 

- 사용법 : 중요한 건 인자로 string을 그냥 넣으면 안된다. bytes화 시켜서 넣어줘야한다.

              이 때 쓰는 메소드가 abi.encodePacked(해시화할 문자) 이다!

keccak256(abi.encodePacked("aaaab"));
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5

 

typecasting 타입 변환

uint8 a = 5;
uint b = 6;

uint8 c = a * b;
// 이 경우 에러 발생, c를 uint8로 잡아놨는데, b로 인해서 a * b 는 uint를 리턴할 것이기 때문이다. 

uint8 c = a * uint8(b); 
// b를 uint8로 typecast 해주면 에러가 발생하지 않는다.

 

위에서 가볍게 짚고 넘어간 내용을 바탕으로 크립토 좀비 실습 고고!!

 

랜덤한 좀비의 DNA 숫자를 뽑아내는 함수 만들기!

받은 string 값을 keccak256 해시함수로 돌려서 uint로 형변환 해준다.

그리고, DNA는 16 자릿수로 정해두었으므로 이 때 처음 만들어둔 dnaModulus를 사용해준다! (dnaModulus는 10**16 임)

 

왜 16 자릿수를 가져오기 위해서 10^16 으로 나눈 나머지값을 가져오는가는 작은 숫자로 예시를 들면 쉽다.

5524 / 100 = 55

5524 % 100 = 24 : 10**2 로 나눈 나머지값이 24이다. 즉, 끝 2자리만 뽑아낼 수 있다. 

따라서, 10**16 으로 나눈 나머지는 16자리수일 것이다! 

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

 

좀비를 생성해주는 public 함수 만들기!

private 함수인 _generateRandomDna 를 사용해서 16자리 DNA값을 만들어줘서 이 randDna값과 _name 값을 _createZombie 함수의 인자로 사용해주었다. 

function createRandomZombie(string memory _name) public {
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
}

 

Events

블록체인 네트워크 상에서 뭔가 액션이 발생했을 때 컨트랙트가 이를 프론트단에 공유하고 의사소통하는 방법이다.

컨트랙트가 특정 이벤트가 일어나는지 listening하고 그 특정 이벤트가 발생하면 뭔가를 실행하기 위함이다. 

관련된 이전 포스팅 : https://yjleekr.tistory.com/97 

// 이벤트 선언
event IntAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public returns (uint) {
    uint result = _x + _y;
    // 이벤트 일으키기, 이벤트 시점 작성
    emit IntAdded(_x, _y, result);
    return result;
}

프론트단은 컨트랙트를 가져와서 이벤트를 들을 수 있고, 그로 인한 결과를 가지고 뭔가를 할 수 있음

YourContract.IntAdded((err,result)=>{
    // 받은 result로 뭔가를 함
})

 

크립토 좀비에선 새로운 좀비가 생성될때마다 우리의 프론트단에서 그걸 인지하고 app 상에서 보여주었으면 한다.

 

1. 우선 상단에서 이벤트를 선언해준다.

2. 새로운 좀비 생성시의 이벤트를 원하므로 그에 해당하는 함수인 _createZombie 함수 내에서 이벤트를 fire해준다.

3. 우선 NewZombie 이벤트는 인자로 zombieId라는 걸 넘겨줘야한다.. 이 zombieId 는 zombies의 배열에서의 인덱스를 말한다. 배열.push() 메서드면 그 배열의 길이가 나온다. 인덱스는 0번째 부터이므로, 배열.push() - 1 이 그 좀비의 id가 되기때문에 관련 코드를 추가해준다. 

4. _createZombie에서 emit 이벤트명(이벤트 등록시 설정한 인자값) 로 이벤트 시점을 잡아준다. 

contract ZombieFactory {

    event NewZombie(uint zombieId, string memory name, uint dna)
	// 이벤트 선언!
    
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        // zombies.push(Zombie(_name, _dna)); 
        uint id = zombies.push(Zombie(_name, _dna)) - 1; 
        // id : zombies 배열내에서의 index 의미
        emit NewZombie(id, _name, _dna);
    }

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

    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

 

크립토 좀비가 실제로 돌아가는 코드인지 테스트해보고 싶다면 ganache 네트워크를 켜고 truffle을 사용해서 확인해보면 된다. 

 

1. ZombieFactory.sol 로 컨트랙트 코드를 만들어준다.

// contracts/ZombieFactory.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

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 memory _name, uint _dna) private {
    	uint id = zombies.push(Zombie(_name, _dna)) - 1;
        // 또는 아래와 같이 해도 된다.
        // zombies.push(Zombie(_name, _dna))
        // uint id = zombies.length - 1;
        emit NewZombie(id, _name, _dna);
    }
    
    function _generateRandomDna(string memory _str) private view returns (uint) {
    	uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }
    
    function createRandomZombie(string memory _name) public {
    	uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }
}

2. migrations 에서 배포 코드를 작성해준다.

 

3. 테스트 코드 작성

유의사항 : private 접근제한자를 붙여 작성한 함수는 테스트시엔 우선 public으로 바꿔준다. 왜냐하면 테스트 시 private으로 작성한 함수는 읽지 못하기 때문이다.

// test/ZombieFactory.test.js

const ZombieFactory = artifacts.require("ZombieFactory");

describe("ZombieFactory", () => {
    let deployed;
    let dna;
    
    it("get deployed contract", async () => {
    	deployed = await ZombieFactory.deployed();
    });
    
    it("_generateRandomDna", async () => {
    	dna = await deployed._genereateRandomDna("yjlee");
        console.log(dna.toNumber()); // 6361453942143725
        console.log(parseInt(dna.toString())); // 6361453942143725
    });
    
    it("_createZombie", async () => {
    	await deployed._createZombie("yjlee", dna);
        const arr = await deployed.zombies.call(0);
         // call 내부 인자값 넣지않으면 에러가 뜸, 인자값으로 배열 속 몇번째 값을 가져올 지 적어준다.
        conosle.log(arr); // 아래 스크린샷 참고
        console.log(arr.name, arr.dna.toNumber());// yjlee 6361453942143725
    });
    
    // 아래 test 코드를 정확히 확인해보려면 코드로 컨트랙트 코드로 돌아가서 위의 두 함수를 private으로 바꿔주고,
    // 두 private 함수의 테스트 코드를 주석처리해주고 실행하자..
    
    it("createRandomZombie", async () => {
    	const zombie = await deployed.createRandomZombie("yjlee");
        console.log(zombie);
        const zombies = await deployed.zombies.call(0);
        console.log(zombies);
    });

})

_createZombie 함수의 경우, 

1. 컨트랙트를 실행하는 것이므로 transactionReceipt 객체도 출력된다.

2. arr 즉, Zombie 배열인 zombies를 call 메소드로 가져오려면 인자로 배열 내 몇번째 값을 가져올 지 적어줘야 오류가 발생하지 않는다.

그 결과 아래와 같은 형태로 출력된다. dna 속성의 경우 BN 로 표현되기 때문에 toNumber() 를 붙여서 int 형태로 확인해볼수있다.

 

deployed.zombies.call(0)

public 함수인 createRandomZombie 를 제대로 test 해보려면 테스트를 위해 public으로 바꿔둔 private 함수들을 다시 private으로 바꿔주고 이에 해당하는 test 코드도 주석처리해줘야한다.

Comments