2021년 8월경에 제가 블록체인에 대해 공부한 내용들을 정리했습니다.

강의 : Klaytn 클레이튼 스마트계약과 탈중앙앱 강의(한양대학교)


Solidity 기본

Solidity란?

  • 이더리움, 클레이튼에서 지원하는 스마트 컨트랙트 언어.
  • 일반적인 프로그래밍 언어와 그 문법, 사용은 유사하지만 몇 가지 제약이 존재.
    ex) 포인터의 개념이 없음 → recursive type 선언 불가능.
    ex) 대신 블록체인의 주소 개념이 들어감.
  • 클레이튼 : 솔리디티의 특정 버전을 지원함.

Solidity 기본 문법

  • 솔리디티 함수는 코드 안에 변수로 선언된 상태를 변경하거나 불러옴.
  • 아래 예시에서 set, get은 함수, storedData는 상태.
  • pragma solidity : solidity 버전 지정.
    (syntax가 자주 바뀌다 보니 버전 지정은 필수임.)
  • public : public이 붙은 함수들만 실행 가능하다.
  • view : 상태를 변경하지 않는 함수임.
  • returns () : () 안의 결과값을 받을 수 있는 함수임.
pragma solidity ^0.5.6;

contract SimpleStorage {
	uint storedData;
	function set(uint x) public {
		storedData = x;
	}
	function get() public view returns (uint) {
		return storedData;
	}
}

예제 1 : 간단한 포인트(Coin) 시스템

컨트랙트 구조

  • 컨트랙트 생성자가 관리하는 포인트 시스템.
  • 포인트 시스템 고유의 주소공간을 가지고, 각 주소에 포인트 잔고를 기록함.
  • 컨트랙트 생성자는 사용자 주소에 포인트를 부여할 수 있음.
  • 사용자는 다른 사용자에게 포인트를 전송할 수 있음.

코드 #1 : 상태 지정

  • minter : 이더리움 160-bit address(주소) 타입, public이므로 다른 컨트랙트에서 minter를 읽어들일 수 있음.
    (코인을 찍어낼 수 있는, 컨트랙트 생성자.)
  • balances : key-value mapping 타입. address가 key, uint가 value.
    만약 해당 key값이 없다면 value로 0을 리턴함.
pragma solidity ^0.5.6;

contract Coin {
	address public minter;

	mapping (address => uint) public balances;
}

코드 #2 : 이벤트(Events)

  • Sent : 이용자가 다른 이용자에게 coin을 전송할 때 발생시킬 이벤트.
contract Coin {
	event Sent(address from, address to, uint amount);
}

코드 #3 : 생성자 함수(Constructor)

  • 컨트랙트가 생성될 때 딱 한 번 실행됨.
  • minter에는 컨트랙트 배포자의 주소값(msg.sender)가 들어감.
    이후, 이 컨트랙트의 minter값은 컨트랙트 배포자의 주소값으로 고정됨.
    (= 코인은 minter, 즉 컨트랙트 배포자만 찍어낼 수 있게 됨.)
contract Coin {
	constructor() public {
		minter = msg.sender;
	}
}

코드 #4 : 함수

  • mint 함수 : receiver 주소에 amount만큼의 새로운 Coin을 부여.
  • require : 입력값이 true일 때만 다음으로 진행. 뒤에 string을 추가로 넣어서 에러값에 대한 리턴 처리를 할 수 있다.(아래 참고)
  • 아래에선 함수를 실행한 사람이 minter이고, 새로 생성하는 코인의 양이 1*10^60개 미만일 때만 다음 과정을 진행한다.
  • 위 조건을 통과한 경우, receiver 주소의 포인트에 amount만큼 더한다.
contract Coin {
	function mint(address receiver, uint amount) public {
		require(msg.sender == minter);
		require(amount < 1e60);
		balances[receiver] += amount;
	}
}
  • send 함수 : sender가 receiver에게 amount만큼의 Coin을 전송.
  • emit : 정의된 이벤트를 발생시킴.
  • 잔고가 충분한지 확인 → sender 잔고 차감 → receiver의 잔고 증가 → Sent 이벤트 생성.
contract Coin {
	function send(address receiver, uint amount) public {
		require(amount <= balance[msg.sender], "Insufficient balance.");
		balances[msg.sender] -= amount;
		balances[receiver] += amount;
		emit **Sent**(msg.sender, receiver, amount);
	}
}

Contract

Contract에서 선언할 수 있는 것들

  • State Variables
  • Functions
  • Function Modifiers
  • Events
  • Struct Types
  • Enum Types

State Variables (상태변수)

State Variables = 블록체인에 영구히 저장할 값들.

  • 어떤 값들은 반드시 State Variable로 선언되어야 함(mapping 등)
  • public : 변수를 외부에서 접근 가능하게 함.
    (자동으로 해당 변수값을 돌려주는 Getter 함수가 생성됨)
    (여기에서 '외부'는 유저나 다른 Contract를 의미한다.)
  • 함수에서 선언하고 초기화한 변수들은 블록체인에 기록되지 않는다.
contract ~~~ {
	/// State Variables 예시
	uint public count = 0;
	address public lastParticipant;
}

Functions (함수)

  • external, public, internal, private 중 하나로 visibility를 설정 가능 (아래 참고)
  • payable, view, pure 등 함수 유형을 정의 가능
  • state를 변경하는 함수의 경우, TX를 통해 실행된다.
    반대로, state를 변경하지 않는 함수의 경우, TX 없이도 실행 가능하다.(call 이용)
contract ~~~ {
	/// Functions 예시
	function plus() public {
		count++;
		lastParticipant = msg.sender;
	}
}

Function Modifiers

Function Modifiers = 함수의 실행 전, 후의 성격을 정의함.
대부분의 경우, 함수의 실행조건을 정의하는데 사용.

(혹은 대부분의 함수에서 이루어지는, 반복되는 작업을 정의할 때도 사용.)

/// 투표 contract
contract Ballot {
	constructor() public { chairperson = msg.sender; }
	address chairperson;
	/// Function Modifiers 예시
	modifier onlyChair {
		require(msg.sender == chairperson, "Only the chairperson can call this function.");
		_;
	}
	function giveRightToVote(address to) public onlyChair {
		/// onlyChair Modifier에 의해, 이 함수는 chairperson이 호출했다는 것을 보장할 수 있다.
	}
}

Events

Events = EVM 로깅을 활용한 시스템.

  • 이벤트가 실행될 때마다 트랜잭션 로그에 저장
  • 저장된 로그는 컨트랙트 주소와 연동되어 클라이언트가 RPC로 조회 가능
    (클라이언트는 특정 Contract에 대한 Event Listening도 가능)

Contract 예시

contract Ballot {
	/// Event 예시
	event Voted(address voter, uint proposal);
	function vote(uint proposal) public {
		...
		/// emit 함수를 통해 Event를 발생시킴.
		emit Voted(msg.sender, proposal);
	}
}

Client 예시 (caver-js 이용)

const BallotContract = new caver.klay.Contract(abi, address);
// Ballot Contract의 Voted Event를 Listening함.
BallotContract.events.Voted(
	{ fromBlock: 0 },
	function(error, event) {
		console.log(event);
	}
).on('error', console.error);

Struct Types

Struct Types = Solidity에서 제공하지 않는 새로운 형식의 자료를 만들 때 사용.
(여러 자료를 묶어 복잡한 자료형을 만들 때 유용함.)

  • Struct의 field로 자기 자신을 넣을 수 없다는 제약이 있다.
contract Ballot {
	/// Struct Types 예시
	struct Voter {
		uint weight;
		bool voted;
		address delegate;
		uint vote;
	}
}

contract SocialMedia {
	/// Struct Types 예시
	struct Friend {
		uint id;
		mapping (uint => address) friends;
	}
}

Enum Types

Enum Types = 임의의 상수를 정의하기에 적합함.

contract Ballot {
	/// Enum Types 예시
	enum Status {
		Open,
		Closed
	}
}

Data Types

Solidity의 자료형은 일반 언어의 자료형과 조금씩 다른 부분들이 있다.

Booleans, Integers

  • Booleans = bool
  • Integers = int / int8 / int16 / ... / int256 / uint / uint8 / uint16 / ... / uint256

Address

Address = account 주소를 표현.

  • 이더리움, 클레이튼의 주소는 20바이트. 따라서 address ⇒ bytes20으로 변환 가능.
  • address VS address payable
    • address는 클레이를 직접 주고받을 수 없다. 클레이를 직접 주고받는 건 address payable만 가능하다!!!
    • address payable ⇒ address (O)
    • address ⇒ address payable (X, uint160을 거쳐서만 변환 가능)

Reference Types

Reference Types = structs, arrays, mapping과 같이 크기가 정해지지 않은 데이터를 위해 사용

  • Reference Type들은 (같은 영역을 사용하는) 변수끼리 대입할 경우 같은 값을 참조함.
    ex) 아래 코드에서, s와 t는 같은 메모리 상에 저장된 "Hello, world"라는 값을 가리킨다.
    string memory s = "Hello, world";
    string memory t = s;

※ Value Type : 크기가 정해진 데이터 타입. Value Type 변수끼리 대입하는 경우, 새로운 메모리 영역을 만들고 거기에 값을 복사한다.

  • Reference Type 데이터는 저장되는 위치를 반드시 명시해야 한다.
    memory : 함수 내에서 유효한 영역에 저장, 휘발성 데이터.
    storage : state variable 취급. 영속적으로 저장되는 영역에 저장
    calldata : external 함수 인자에 사용되는 공간
  • 서로 다른 영역을 참조하는 변수 간 대입이 발생 시, 데이터 복사
    storage ⇒ memory / calldata (이 경우, memory의 값을 변경해도 storage에는 반영되지 않는다.)
    anything ⇒ storage (이 경우, storage의 값이 바뀌어도 memory의 값은 바뀌지 않는다.)

Arrays

  • 자바스크립트의 배열과 개념은 같으나 사용법이 많이 다르다.

  • State Variable로 사용하는 경우 (저장공간 = storage) : dynamic size로 선언 가능.

/// k개의 T를 가진 배열 x를 선언
T[k] x;
/// 예시 : arr은 5개의 uint를 가진 배열
uint[5] arr;
/// x는 T를 담을 수 있는 배열, x의 크기는 변할 수 있음.
T[] x;
/// k개의 T를 담을 수 있는 dynamic size 배열 x를 선언.
T[][k] x;
/// (i+1)번째 배열의 (j+1)번째 T를 불러옴.
x[i][j];
  • 모든 유형의 데이터를 배열에 담을 수 있다. (mapping, struct 포함)
  • 배열 관련 함수들
/// 배열에 데이터를 추가함
.push(T item);

/// 배열크기를 반환
.length

/// 배열을 삭제하지 않고, 배열을 빈 상태로 초기화하는 함수.
delete(array);
  • memory 배열 : 런타임에 생성됨, new 키워드로 선언.
    dynamic size로 선언할 수 없다. (크기를 무조건 지정해줘야 한다.)

bytesN VS bytes/string VS byte[]

되도록이면 bytes를 사용할 것!

(byte[]는 배열 아이템 간 31바이트 패딩이 추가됨.)

  • 임의의 길이의 바이트 데이터를 담을 때는 bytes.
  • 임의의 길이의 데이터가 UTF-8과 같이 문자로 인코딩될수 있을 때는 string
  • 바이트 데이터의 길이가 정해져있을 때는 value type의 bytes1, ..., bytes32를 사용
  • byte[]는 지양

Mapping Types

  • 해시테이블과 유사함.

  • storage 영역에만 저장 가능. 즉, State Variable로만 선언 가능.

  • 함수 인자, public 리턴값으로 사용할 수 없음.
    (함수에서는 State Variable로 선언된 Mapping Type 변수를 사용할 수만 있다.)

/// Mapping Types 선언
mapping (K => V) table;

Contract 설계 원칙

  1. Contract는 불변하다. 즉, 한 번 배포한 것은 바꿀 수 없다!

=> 단, 배포 이후 변경사항을 적용할 수 있도록 Proxy 등을 쓸 수는 있다.
이 경우, Proxy에서는 특정 기능과 Contract를 서로 연결해두고, 만약 특정 기능에서 수정사항이 발생하면, 수정된 Contract를 배포한 뒤, Proxy에서 특정 기능과 새로운 Contract를 연결시키는 방식으로 작업한다. (다만 일반적이지 않다.)

  1. Contract는 되도록 짧게 설계하는 게 좋다.

=> 1번 원칙도 문제고, Contract가 길어지면 연산량이 많아지면서 남은 가스량을 체크해야 하는 경우가 생기기 때문에 복잡도가 크게 증가할 수 있다.
보통 1sequence를 통과하면 종료될 수 있도록 설계한다.

  1. 블록체인은 비싼 Storage이므로, 저장 공간에 제약이 있다는 것을 감안해야 한다.

=> 32바이트 크기의 데이터를 가장 많이 쓰며, 고정형 크기의 데이터를 잘 활용해야 한다.
Contract가 발생시키는 가스비의 경우, 계산 비용도 문제가 되지만, 그보다는 저장 비용이 더 문제가 된다.
1byte도 아껴쓰자!!!


Special Variables and Functions For Solidity

Blocks and Transaction Properties

  • blockhash(uint blockNumber) returns (bytes32) : 블록 해시(최근 256블록까지만 조회 가능)

  • block.number (uint) : 현재 블록 번호

  • block.timestamp (uint) : 현재 블록 타임스탬프

  • gasleft() returns (uint256) : 남은 가스량

  • msg.data (bytes calldata) : 메세지(현재 TX)에 포함된 실행 데이터(input)

  • msg.sender (address payable) : 현재 함수 실행 주체의 주소

  • msg.sig (bytes4) : calldata의 첫 4바이트 (함수 해시, 함수명을 해시한 것의 첫 4바이트를 사용함.)

  • msg.value (uint) : 메세지와 전달된 클레이(peb 단위) 양

  • now (uint) : block.timestamp와 동일

  • ~~tx.gasprice (uint) : TX gas price (25ston으로 항상 동일)~~

  • tx.origin (address payable) : TX 주체 (sender)

blockhash를 알아야 하는 경우?
: Merkle Tree를 역산할 때 필요. 특정 해시의 존재 여부를 체크할 수 있다고 한다. (더 자세한 조사 필요)
(Merkle Tree에 대한 더 자세한 정보는 여길 클릭!)

gasleft()를 알아야 하는 경우?
: 다른 Contract의 함수를 호출할 때 가스가 많이 들어감. 이 경우 남은 가스량을 체크하기도 한다.
(다만 너무 빡빡하고 복잡해지므로 남은 가스량을 체크해야 하는 경우는 줄여야 한다!)

tx.origin을 알아야 하는 경우?
: Internal Transaction을 실행시킨 주체(최초의 TX를 실행한 유저)를 찾을 때 필요하다.

※ Internal Transaction : Contract를 통해 다른 TX를 발생시키는 TX.

Error Handling

  • assert(bool condition)
    : condition이 false인 경우 실행 중인 함수가 변경한 내역을 모두 이전 상태로 되돌림. (로직 체크에 사용)

  • require(bool condition)
    : condition이 false인 경우 실행 중인 함수가 변경한 내역을 모두 이전 상태로 되돌림 (외부 변수 검증에 사용)
    (다만 assert가 쓰여야 맞는 부분도 require를 쓸 수 있다.)

  • require(bool condition, string memory message)
    : require(bool)과 동일, 오류 발생 시 message를 전달.

Cryptographic Functions

★ 주의 : 아래 함수들은 가스비가 매우 비싸니, 가급적 쓰지 말자!
따라서, 보통은 블록체인 밖에서 해시를 하고, 그 값을 블록체인에 넣는다.

  • keccak256(bytes memory) returns (bytes32)
    : 주어진 값으로 Keccak-256 해시를 생성

  • sha256(bytes memory) returns (bytes32)
    : 주어진 값으로 SHA-256 해시를 생성

  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
    : 서명(v, r, s)으로부터 account 주소를 도출 (서명 ⇒ 공개키 ⇒ account 주소).


Solidity에서 쓰이는 표현들

  • 대부분의 프로그래밍 언어가 지원하는 제어 구문들을 지원한다.
    (if, else, while, do, for, break, continue, return)
  • 단, try-catch 등의 예외처리 기능이 없다!!!
    ⇒ 따라서, 모든 함수는 반드시 '성공적으로' 동작해야만 한다!!!

    (Contract를 최대한 간결하게 짜야 하는 또 다른 이유.)

For, While

: 크게 다를 게 없다.

/// for 구문 예시
for(uint i=0; i<repeat; i++) {
	...
}

/// while 구문 예시
while(i < repeat) {
	...
}

Contract 작성 시 고려해야 할 사안들

'Contract를 만든다'의 의미?

'Contract를 만든다' = 'Contract를 배포한다' or 'Contract를 Class처럼 사용한다.'

  • Contract를 Class처럼 사용한다?
    ⇒ new 키워드를 사용해서 Contract를 생성하여 변수에 대입하면 된다.
  • 이미 만들어진 Contract를 참조하는 경우, 해당 Contract의 address값을 넘겨주면 된다.
    (단 해당 Contract의 abi값을 알고 있어야 한다.)
contract A {
	B b;
	constructor() public {
		b = new B(10);
	}
	...
}

contract B {
	uint base;
	constructor(uint _base) public {
		base = _base;
	}
	...
}

Visibility

  • external
    fallback function은 무조건 visibility를 external로 지정해줘야 한다.
    • 다른 Contract에서 호출 가능, TX를 통해 호출 가능
    • 블록 내부에서 호출 불가능
      (내부에서 쓰더라도 this.을 붙여야 한다. 예시 : f()는 안되지만 this.f()는 허용된다.)
  • public
    • 다른 Contract에서 호출 가능, TX를 통해 호출 가능
    • 블록 내부에서 호출 가능
  • internal
    visibility를 명시하지 않으면 internal로 취급된다.
    • 외부에서 호출 불가능
    • 블록 내부에서 호출 가능
    • 상속받은 Contract에서 호출 가능
  • private
    • 블록 내부에서만 호출 가능
    • 상속받은 Contract에서도 호출 불가능

Function Declarations

Function Declarations = 함수에 제약을 걸어서, 정해진 scope에서 동작할 수 있도록 설정한다.

  • pure
    : State Variable 접근 불가. 즉, State Variable에 대한 READ(X), WRITE(X)
  • view
    : State Variable 변경 불가. 즉, State Variable에 대한 READ(O), WRITE(X)
  • (none)
    : 제약 없음. 즉, State Variable에 대한 READ(O), WRITE(O)

Fallback function

Fallback function = Contract에 일치하는 함수가 없을 경우 실행 (no input/calldata)

  • 단 하나만 정의 가능함.
  • 함수명/파라미터/리턴값 없음.
  • 반드시 external로 선언해야 함.
  • Contract가 클레이를 받으려면 payable fallback function이 필요하다.
    (payable fallback이 없는 Contract가 클레이를 전송받으면 오류가 발생한다.)
contract Escrow {
	event Deposited(address sender, uint amount);

	/// Fallback function.
	/// 함수명, 파라미터, 리턴값 다 없음 + visibility는 external.
	function() external payable {
		emit Deposited(msg.sender, msg.value);
	}
}
profile
Flutter 메인의 풀스택 개발자 / 한양대 컴퓨터소프트웨어학과, HUHS의 화석

0개의 댓글